├── .gitignore ├── assets ├── stay-cool.gif ├── candidate-optimized.svg └── candidate-path.svg ├── .eslintignore ├── .babelrc ├── .prettierrc ├── rollup.config.js ├── rollup.config.dev.js ├── rollup.config.demo.js ├── rollup.config.deploy.js ├── rollup.config.prod.js ├── .eslintrc ├── LICENSE ├── package.json ├── bs-config.js ├── README.md ├── src └── index.js └── playground ├── script.js ├── style.css ├── index.html └── meanderer.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | playground/meanderer.js 4 | public -------------------------------------------------------------------------------- /assets/stay-cool.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jh3y/meanderer/HEAD/assets/stay-cool.gif -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .prettierrc 3 | yarn.lock 4 | .gitignore 5 | README.md 6 | dist 7 | assets 8 | playground/meanderer.js -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/env", {"modules": false}] 4 | ], 5 | "plugins": ["transform-class-properties"] 6 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "semi": false, 6 | "parser": "flow", 7 | "jsxBracketSameLine": true 8 | } 9 | -------------------------------------------------------------------------------- /assets/candidate-optimized.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve' 2 | import babel from 'rollup-plugin-babel' 3 | 4 | export default { 5 | input: 'src/index.js', 6 | output: { 7 | file: 'dist/meanderer.js', 8 | format: 'umd', 9 | name: 'Meanderer', 10 | }, 11 | plugins: [ 12 | resolve(), 13 | babel({ 14 | exclude: 'node_modules/**', // only transpile our source code 15 | }), 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /rollup.config.dev.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve' 2 | import babel from 'rollup-plugin-babel' 3 | 4 | export default { 5 | input: 'src/index.js', 6 | output: { 7 | file: 'playground/meanderer.js', 8 | format: 'umd', 9 | name: 'Meanderer', 10 | }, 11 | plugins: [ 12 | resolve(), 13 | babel({ 14 | exclude: 'node_modules/**', // only transpile our source code 15 | }), 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /rollup.config.demo.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve' 2 | import babel from 'rollup-plugin-babel' 3 | import { uglify } from 'rollup-plugin-uglify' 4 | 5 | export default { 6 | input: 'playground/script.js', 7 | output: { 8 | file: 'public/script.js', 9 | format: 'umd', 10 | }, 11 | plugins: [ 12 | resolve(), 13 | babel({ 14 | exclude: 'node_modules/**', // only transpile our source code 15 | }), 16 | uglify(), 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /rollup.config.deploy.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve' 2 | import babel from 'rollup-plugin-babel' 3 | import { uglify } from 'rollup-plugin-uglify' 4 | 5 | export default { 6 | input: 'src/index.js', 7 | output: { 8 | file: 'public/meanderer.js', 9 | format: 'umd', 10 | name: 'Meanderer', 11 | }, 12 | plugins: [ 13 | resolve(), 14 | babel({ 15 | exclude: 'node_modules/**', // only transpile our source code 16 | }), 17 | uglify(), 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /rollup.config.prod.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve' 2 | import babel from 'rollup-plugin-babel' 3 | import { uglify } from 'rollup-plugin-uglify' 4 | import filesize from 'rollup-plugin-filesize' 5 | 6 | export default { 7 | input: 'src/index.js', 8 | output: { 9 | file: 'dist/meanderer.min.js', 10 | format: 'umd', 11 | name: 'Meanderer', 12 | }, 13 | plugins: [ 14 | resolve(), 15 | babel({ 16 | exclude: 'node_modules/**', // only transpile our source code 17 | }), 18 | uglify(), 19 | filesize(), 20 | ], 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "node": true, 5 | "es6": true, 6 | "browser": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 7, 10 | "ecmaFeatures": { 11 | "experimentalObjectRestSpread": true, 12 | "jsx": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "extends": [ 17 | "prettier", 18 | "eslint:recommended" 19 | ], 20 | "plugins": [ 21 | "prettier" 22 | ], 23 | "rules": { 24 | "prettier/prettier": [ 25 | 2, 26 | { 27 | "trailingComma": "es5", 28 | "singleQuote": true, 29 | "printWidth": 80, 30 | "semi": false, 31 | "parser": "flow", 32 | "jsxBracketSameLine": true 33 | } 34 | ], 35 | "no-unused-vars": 2, 36 | "no-console": 2, 37 | "no-restricted-syntax": 2, 38 | "no-undef": 2, 39 | "no-useless-catch": 0, 40 | "no-misleading-character-class": 0, 41 | "no-async-promise-executor": 0, 42 | "require-atomic-updates": 0 43 | } 44 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Jhey Tompkins (jh3y) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /assets/candidate-path.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meanderer", 3 | "version": "0.0.1", 4 | "description": "micro-library for generating scaled CSS offset-paths", 5 | "main": "dist/meanderer.js", 6 | "scripts": { 7 | "precommit": "lint-staged", 8 | "build": "rollup -c rollup.config.js", 9 | "build:prod": "rollup -c rollup.config.prod.js", 10 | "dist": "yarn build && yarn build:prod", 11 | "predev": "browser-sync start --config bs-config.js", 12 | "prebuild:site": "mkdir -pv public", 13 | "build:meanderer": "rollup -c rollup.config.deploy.js", 14 | "build:scripts": "rollup -c rollup.config.demo.js", 15 | "build:markup": "html-minifier --collapse-whitespace -o public/index.html playground/index.html", 16 | "build:styles": "postcss --use autoprefixer cssnano -o public/style.css playground/style.css", 17 | "build:site": "yarn build:scripts && yarn build:styles && yarn build:markup && yarn build:meanderer", 18 | "dev": "rollup -w -c rollup.config.dev.js", 19 | "test": "echo \"Error: no test specified\" && exit 1", 20 | "lint": "eslint .", 21 | "lint--fix": "eslint --fix ." 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/jh3y/meanderer.git" 26 | }, 27 | "keywords": [ 28 | "offset-path", 29 | "motion-path", 30 | "responsive", 31 | "CSS" 32 | ], 33 | "files": [ 34 | "dist/*" 35 | ], 36 | "lint-staged": { 37 | "{src,playground}/**/*.{js,json,css}": [ 38 | "eslint", 39 | "prettier --write", 40 | "git add" 41 | ] 42 | }, 43 | "author": "jh3y", 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/jh3y/meanderer/issues" 47 | }, 48 | "homepage": "https://github.com/jh3y/meanderer#readme", 49 | "devDependencies": { 50 | "@babel/core": "^7.9.0", 51 | "@babel/preset-env": "^7.9.0", 52 | "@rollup/plugin-node-resolve": "^7.1.1", 53 | "autoprefixer": "^9.7.5", 54 | "babel-eslint": "^10.1.0", 55 | "babel-plugin-transform-class-properties": "^6.24.1", 56 | "babel-preset-env": "^1.7.0", 57 | "browser-sync": "^2.26.7", 58 | "cssnano": "^4.1.10", 59 | "d3": "^5.15.0", 60 | "eslint": "^6.8.0", 61 | "eslint-config-prettier": "^6.10.1", 62 | "eslint-plugin-prettier": "^3.1.2", 63 | "html-minifier": "^4.0.0", 64 | "husky": "^4.2.3", 65 | "lint-staged": "^10.0.9", 66 | "postcss-cli": "^7.1.0", 67 | "prettier": "^2.0.2", 68 | "prettier-eslint": "^9.0.1", 69 | "rollup": "^2.2.0", 70 | "rollup-plugin-babel": "^4.4.0", 71 | "rollup-plugin-filesize": "^6.2.1", 72 | "rollup-plugin-uglify": "^6.0.4" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /bs-config.js: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Browser-sync config file 4 | |-------------------------------------------------------------------------- 5 | | 6 | | For up-to-date information about the options: 7 | | http://www.browsersync.io/docs/options/ 8 | | 9 | | There are more options than you see here, these are just the ones that are 10 | | set internally. See the website for more info. 11 | | 12 | | 13 | */ 14 | module.exports = { 15 | ui: { 16 | port: 3001, 17 | }, 18 | files: ['./playground/**/*.*'], 19 | watchEvents: ['change'], 20 | watch: true, 21 | ignore: [], 22 | single: false, 23 | watchOptions: { 24 | ignoreInitial: true, 25 | }, 26 | server: './playground', 27 | proxy: false, 28 | port: 3000, 29 | middleware: false, 30 | serveStatic: [], 31 | ghostMode: { 32 | clicks: true, 33 | scroll: true, 34 | location: true, 35 | forms: { 36 | submit: true, 37 | inputs: true, 38 | toggles: true, 39 | }, 40 | }, 41 | logLevel: 'info', 42 | logPrefix: 'Browsersync', 43 | logConnections: false, 44 | logFileChanges: true, 45 | logSnippet: true, 46 | rewriteRules: [], 47 | open: 'local', 48 | browser: 'default', 49 | cors: false, 50 | xip: false, 51 | hostnameSuffix: false, 52 | reloadOnRestart: false, 53 | notify: true, 54 | scrollProportionally: true, 55 | scrollThrottle: 0, 56 | scrollRestoreTechnique: 'window.name', 57 | scrollElements: [], 58 | scrollElementMapping: [], 59 | reloadDelay: 0, 60 | reloadDebounce: 500, 61 | reloadThrottle: 0, 62 | plugins: [], 63 | injectChanges: true, 64 | startPath: null, 65 | minify: true, 66 | host: null, 67 | localOnly: false, 68 | codeSync: true, 69 | timestamps: true, 70 | clientEvents: [ 71 | 'scroll', 72 | 'scroll:element', 73 | 'input:text', 74 | 'input:toggles', 75 | 'form:submit', 76 | 'form:reset', 77 | 'click', 78 | ], 79 | socket: { 80 | socketIoOptions: { 81 | log: false, 82 | }, 83 | socketIoClientConfig: { 84 | reconnectionAttempts: 50, 85 | }, 86 | path: '/browser-sync/socket.io', 87 | clientPath: '/browser-sync', 88 | namespace: '/browser-sync', 89 | clients: { 90 | heartbeatTimeout: 5000, 91 | }, 92 | }, 93 | tagNames: { 94 | less: 'link', 95 | scss: 'link', 96 | css: 'link', 97 | jpg: 'img', 98 | jpeg: 'img', 99 | png: 'img', 100 | svg: 'img', 101 | gif: 'img', 102 | js: 'script', 103 | }, 104 | injectNotification: false, 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Meanderer 2 | 3 | A micro-library for scaling [CSS motion path](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Motion_Path) strings ✨ 4 | 5 | ![popsicle with "stay cool..." lettering travelling around its path](./assets/stay-cool.gif) 6 | 7 | ## Installation 8 | ### CDN 9 | ```shell 10 | https://unpkg.com/meanderer@0.0.1/dist/meanderer{.min}.js 11 | ``` 12 | ### NPM 13 | ``` 14 | npm i meanderer 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```js 20 | // Our path string 21 | const PATH = "M32.074 13.446s-2.706-2.965-4.158-4.349c-2.003-1.908-3.941-3.942-6.268-5.437C19.33..." 22 | // The bounds of our path 23 | const WIDTH = 65 24 | const HEIGHT = 30 25 | // Generate a responsive path 26 | const responsivePath = new Meanderer({ 27 | path: PATH, 28 | width: WIDTH, 29 | height: HEIGHT 30 | }) 31 | // Generate a new scaled path when required. Here we are using ResizeObserver 32 | // with a container that uses viewport units 33 | const setPath = () => { 34 | const scaledPath = responsivePath.generatePath( 35 | CONTAINER.offsetWidth, 36 | CONTAINER.offsetHeight 37 | ) 38 | // Here, we apply the path to an element through a CSS variable. 39 | // And then an element picks up on that. We could apply the motion path straight to the element though. 40 | CONTAINER.style.setProperty("--path", `"${scaledPath}"`) 41 | } 42 | // Set up our Resize Observer that will get the ball rolling 43 | const SizeObserver = new ResizeObserver(setPath) 44 | // Observe! Done! 45 | SizeObserver.observe(CONTAINER) 46 | ``` 47 | 48 | First things first. We need a `path`. 49 | Unless you're constructing one by hand, it's likely you'll be extracting one from an `SVG`. 50 | 51 | Before extracting one from an `SVG`, it's wise to run that `SVG` through an optimizer like [SVGOMG](https://jakearchibald.github.io/svgomg/)(Use the precision slider for extra gains! 💪). This will normalize coordinates, etc. removing any translations which could skew the path translation. 52 | 53 | Now you've got a `path` string, it's time to use it! 54 | 1. Create variables for the `path`, and a desired `width` and `height` for the `path` bounds. The `width` and `height` are in most cases going to be the `x2` and `y2` of your SVG's `viewBox` attribute. 55 | ```js 56 | const PATH = "M32.074 13.446s-2.706-2.965-4.158-4.349c-2.003-1.908-3.941-3.942-6.268-5.437C19.33..." 57 | // The bounds of our path 58 | const WIDTH = 65 59 | const HEIGHT = 30 60 | ``` 61 | 2. Create a new responsive path by passing those variables inside an `Object` to a __new__ `Meanderer` instance. 62 | ```js 63 | // Generate a responsive path 64 | const responsivePath = new Meanderer({ 65 | path: PATH, 66 | width: WIDTH, 67 | height: HEIGHT 68 | }) 69 | ``` 70 | 3. Use your instance to generate scaled path strings for a given `width` and `height` 👍 71 | ```js 72 | responsivePath.generatePath(200, 400) 73 | ``` 74 | 4. Pass that to your element either directly or via CSS variable, etc. 🎉 75 | 76 | 77 | ## Caveats 78 | `Meanderer` will do its best to maintain aspect ratio of your paths. If the container dimensions passed in to `generatePath` don't match the aspect ratio of the `path`, `Meanderer` will handle this. It will do this by padding out the container and centering the `path` for you. 79 | 80 | A way to enforce the correct aspect ratio for your container is to use your defined `width` and `height` in your CSS. Consider a container with a `width` of `25vmin`. You've specified a `width` and `height` of `64` and `35`. 81 | ```css 82 | .container { 83 | height: calc((64 / 35) * 25vmin); 84 | width: 25vmin; 85 | } 86 | ``` 87 | 88 | `stroke-path` isn't currently taken into consideration. There have been experiments trying it out though. They didn't seem to affect the overall experience/result though. 89 | 90 | ## Contributing 91 | I'd love some contributions if you think this micro-library could be useful for you! Leave an issue or open a PR 👍 92 | 93 | -------- 94 | 95 | MIT Licensed | Made with 💻 by @jh3y 2020 -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { line } from 'd3-shape' 2 | import { scaleLinear } from 'd3-scale' 3 | 4 | /** 5 | * Meanderer class. Accepts a path, container, height, width, and change handler. 6 | * Although it doesn't need a handler. We can just call get path and let it do that. 7 | * The checks can be handled outside. We don't need to do it inside. 8 | */ 9 | class Meanderer { 10 | container 11 | height 12 | path 13 | threshold 14 | width 15 | constructor({ height, path, threshold = 0.2, width }) { 16 | this.height = height 17 | this.path = path 18 | this.threshold = threshold 19 | this.width = width 20 | // With what we are given create internal references 21 | this.aspect_ratio = width / height 22 | // Convert the path into a data set 23 | this.path_data = this.convertPathToData(path) 24 | this.maximums = this.getMaximums(this.path_data) 25 | this.range_ratios = this.getRatios(this.maximums, width, height) 26 | } 27 | // This is relevant for when we want to interpolate points to 28 | // the container scale. We need the minimum and maximum for both X and Y 29 | getMaximums = (data) => { 30 | const X_POINTS = data.map((point) => point[0]) 31 | const Y_POINTS = data.map((point) => point[1]) 32 | return [ 33 | Math.max(...X_POINTS), // x2 34 | Math.max(...Y_POINTS), // y2 35 | ] 36 | } 37 | // Generate some ratios based on the data points and the path width and height 38 | getRatios = (maxs, width, height) => [maxs[0] / width, maxs[1] / height] 39 | 40 | /** 41 | * Initially convert the path to data. Should only be required 42 | * once as we are simply scaling it up and down. Only issue could be upscaling?? 43 | * Create high quality paths initially 44 | */ 45 | convertPathToData = (path) => { 46 | // To convert the path data to points, we need an SVG path element. 47 | const svgContainer = document.createElement('div') 48 | // To create one though, a quick way is to use innerHTML 49 | svgContainer.innerHTML = ` 50 | 51 | ` 52 | const pathElement = svgContainer.querySelector('path') 53 | // Now to gather up the path points using the SVGGeometryElement API 👍 54 | const DATA = [] 55 | // Iterate over the total length of the path pushing the x and y into 56 | // a data set for d3 to handle 👍 57 | for (let p = 0; p < pathElement.getTotalLength(); p++) { 58 | const { x, y } = pathElement.getPointAtLength(p) 59 | DATA.push([x, y]) 60 | } 61 | return DATA 62 | } 63 | 64 | /** 65 | * This is where the magic happens. 66 | * Use ratios etc. to interpolate our data set against our container bounds. 67 | */ 68 | generatePath = (containerWidth, containerHeight) => { 69 | const { 70 | height, 71 | width, 72 | aspect_ratio: aspectRatio, 73 | path_data: data, 74 | maximums: [maxWidth, maxHeight], 75 | range_ratios: [widthRatio, heightRatio], 76 | threshold, 77 | } = this 78 | const OFFSETS = [0, 0] 79 | // Get the aspect ratio defined by the container 80 | const newAspectRatio = containerWidth / containerHeight 81 | // We only need to start applying offsets if the aspect ratio of the container is off 👍 82 | // In here we need to work out which side needs the offset. It's whichever one is smallest in order to centralize. 83 | // What if the container matches the aspect ratio... 84 | if (Math.abs(newAspectRatio - aspectRatio) > threshold) { 85 | // We know the tolerance is off so we need to work out a ratio 86 | // This works flawlessly. Now we need to check for when the height is less than the width 87 | if (width < height) { 88 | const ratio = (height - width) / height 89 | OFFSETS[0] = (ratio * containerWidth) / 2 90 | } else { 91 | const ratio = (width - height) / width 92 | OFFSETS[1] = (ratio * containerHeight) / 2 93 | } 94 | } 95 | // Create two d3 scales for X and Y 96 | const xScale = scaleLinear() 97 | .domain([0, maxWidth]) 98 | .range([OFFSETS[0], containerWidth * widthRatio - OFFSETS[0]]) 99 | const yScale = scaleLinear() 100 | .domain([0, maxHeight]) 101 | .range([OFFSETS[1], containerHeight * heightRatio - OFFSETS[1]]) 102 | // Map our data points using the scales 103 | const SCALED_POINTS = data.map((POINT) => [ 104 | xScale(POINT[0]), 105 | yScale(POINT[1]), 106 | ]) 107 | return line()(SCALED_POINTS) 108 | } 109 | } 110 | 111 | export default Meanderer 112 | -------------------------------------------------------------------------------- /playground/script.js: -------------------------------------------------------------------------------- 1 | const { Meanderer } = window 2 | const CONTAINER = document.querySelector('#container') 3 | const WRAPPER = document.querySelector('.container__wrapper') 4 | const RESET = document.getElementById('reset') 5 | const RESET_DEMO = document.getElementById('reset-demo') 6 | const PATH_EL = document.querySelector('#path-rep path') 7 | // Inputs 8 | const HEIGHT_INPUT = document.querySelector('[name="height"]') 9 | const PATH_INPUT = document.querySelector('[name="path"]') 10 | const WIDTH_INPUT = document.querySelector('[name="width"]') 11 | const FORM = document.querySelector('form') 12 | // Demo config 13 | const PATH = 'M3 42C3 0 19 3 19 3l4 39S22 3 35 3s9 39 9 39' 14 | const WIDTH = 47 15 | const HEIGHT = 45 16 | 17 | HEIGHT_INPUT.value = HEIGHT 18 | PATH_INPUT.value = PATH 19 | WIDTH_INPUT.value = WIDTH 20 | 21 | let responsivePath = new Meanderer({ 22 | path: PATH, 23 | width: WIDTH, 24 | height: HEIGHT, 25 | }) 26 | 27 | // Set up responsive path handling 28 | const setPath = () => { 29 | const scaledPath = responsivePath.generatePath( 30 | CONTAINER.offsetWidth, 31 | CONTAINER.offsetHeight 32 | ) 33 | CONTAINER.style.setProperty('--path', `"${scaledPath}"`) 34 | PATH_EL.setAttribute('d', scaledPath) 35 | } 36 | const SizeObserver = new ResizeObserver(setPath) 37 | SizeObserver.observe(CONTAINER) 38 | 39 | // Set up drag and drop handling 40 | const onFileDrop = (e) => { 41 | e.preventDefault() 42 | const file = e.dataTransfer.files[0] 43 | if ( 44 | file.type === 'image/svg+xml' || 45 | file.name.slice(file.name.length - 4) === '.svg' 46 | ) { 47 | // process the file. 48 | const reader = new FileReader() 49 | reader.onloadend = (response) => { 50 | try { 51 | // file.target.result is the SVG markup we want to use. 52 | const wrapper = document.createElement('div') 53 | wrapper.innerHTML = response.target.result 54 | const svg = wrapper.querySelector('svg') 55 | const path = wrapper.querySelector('path') 56 | const viewBox = svg.getAttribute('viewBox').split(' ') // 0 0 x2 y2 57 | const pathString = path.getAttribute('d') 58 | // At this point make responsivePath a new responsive path 59 | responsivePath = new Meanderer({ 60 | path: pathString, 61 | width: viewBox[2], 62 | height: viewBox[3], 63 | }) 64 | PATH_INPUT.value = pathString 65 | HEIGHT_INPUT.value = viewBox[3] 66 | WIDTH_INPUT.value = viewBox[2] 67 | setPath() 68 | } catch (e) { 69 | throw Error('Something went wrong', e) 70 | } 71 | } 72 | reader.readAsText(file) 73 | } 74 | } 75 | // Don't do anything on drag over 76 | document.body.addEventListener('dragover', (e) => e.preventDefault()) 77 | // On drop, process file and take first path 78 | document.body.addEventListener('drop', onFileDrop) 79 | 80 | // Reset container 81 | const resetContainer = (e) => { 82 | e.preventDefault() 83 | WRAPPER.removeAttribute('style') 84 | } 85 | RESET.addEventListener('click', resetContainer) 86 | // Reset demo 87 | const resetDemo = (e) => { 88 | e.preventDefault() 89 | responsivePath = new Meanderer({ 90 | path: PATH, 91 | height: HEIGHT, 92 | width: WIDTH, 93 | }) 94 | HEIGHT_INPUT.value = HEIGHT 95 | PATH_INPUT.value = PATH 96 | WIDTH_INPUT.value = WIDTH 97 | setPath() 98 | } 99 | RESET_DEMO.addEventListener('click', resetDemo) 100 | // Handle form changes with event delegation 101 | const handleChange = (e) => { 102 | const target = e.target 103 | if (target.type === 'checkbox') { 104 | if (target.name === 'threeD') { 105 | WRAPPER.style.setProperty('--rotation', target.checked ? 75 : 0) 106 | WRAPPER.style.setProperty( 107 | '--transform-style', 108 | target.checked ? 'preserve-3d' : 'none' 109 | ) 110 | WRAPPER.style.setProperty( 111 | '--overflow', 112 | target.checked ? 'visible' : 'hidden' 113 | ) 114 | } 115 | if (target.name === 'alternate') { 116 | WRAPPER.style.setProperty( 117 | '--animation-direction', 118 | target.checked ? 'alternate' : 'normal' 119 | ) 120 | } 121 | if (target.name === 'svg') { 122 | WRAPPER.style.setProperty( 123 | '--svg-display', 124 | target.checked ? 'block' : 'none' 125 | ) 126 | } 127 | } 128 | if (target.type === 'text' || target.type === 'number') { 129 | responsivePath = new Meanderer({ 130 | path: PATH_INPUT.value, 131 | width: parseInt(WIDTH_INPUT.value, 10), 132 | height: parseInt(HEIGHT_INPUT.value), 133 | }) 134 | setPath() 135 | } 136 | } 137 | FORM.addEventListener('input', handleChange) 138 | -------------------------------------------------------------------------------- /playground/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | :root { 6 | --color: hsl(0, 0%, 90%); 7 | --bg: hsl(255, 50%, 20%); 8 | --container: hsl(0, 0%, 20%); 9 | --hue: hsl(255, 50%, 20%); 10 | } 11 | 12 | body { 13 | margin: 0; 14 | background: var(--bg); 15 | color: var(--color); 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | font-family: 'Roboto', sans-serif; 20 | } 21 | 22 | h1 { 23 | margin-bottom: 0; 24 | font-weight: bold; 25 | } 26 | h2 { 27 | font-size: 1.5rem; 28 | } 29 | #app { 30 | padding: 2rem 3rem; 31 | min-height: 100vh; 32 | display: flex; 33 | align-items: center; 34 | /* justify-content: center; */ 35 | flex-direction: column; 36 | transform-style: preserve-3d; 37 | perspective: 1000px; 38 | perspective-origin: 50% 25%; 39 | } 40 | 41 | #app > * + * { 42 | margin-bottom: 2rem; 43 | } 44 | 45 | article { 46 | max-width: 600px; 47 | } 48 | 49 | footer { 50 | font-size: 0.875rem; 51 | } 52 | 53 | .motion-element { 54 | height: 40px; 55 | width: 40px; 56 | position: absolute; 57 | top: 0%; 58 | left: 0%; 59 | offset-path: path(var(--path)); 60 | animation: travel 2s infinite var(--animation-direction, normal) linear; 61 | transform-style: var(--transform-style, 'none'); 62 | transform: translate3d(0, 0, 20px); 63 | } 64 | .motion-element__side { 65 | background: hsla(90,100%,50%,0.1); 66 | border: 2px hsl(90, 100%, 50%) solid; 67 | height: 100%; 68 | position: absolute; 69 | width: 100%; 70 | } 71 | .motion-element__side:nth-of-type(1) { 72 | transform: translate3d(0, 0, 20px); 73 | } 74 | .motion-element__side:nth-of-type(2) { 75 | transform: translate3d(0, 0, -20px); 76 | } 77 | .motion-element__side:nth-of-type(3) { 78 | transform: rotateX(90deg) translate3d(0, 0, -20px); 79 | } 80 | .motion-element__side:nth-of-type(4) { 81 | transform: rotateX(90deg) translate3d(0, 0, 20px); 82 | } 83 | .motion-element__side:nth-of-type(5) { 84 | transform: rotateY(90deg) translate3d(0, 0, 20px); 85 | } 86 | .motion-element__side:nth-of-type(6) { 87 | transform: rotateY(-90deg) translate3d(0, 0, 20px); 88 | } 89 | 90 | .container__wrapper { 91 | padding: 25px; 92 | height: 25vmin; 93 | width: 25vmin; 94 | min-width: 200px; 95 | min-height: 200px; 96 | overflow: var(--overflow, hidden); 97 | transform-style: preserve-3d; 98 | margin-bottom: 2rem; 99 | border: 4px solid var(--color); 100 | resize: both; 101 | } 102 | 103 | .container { 104 | height: 100%; 105 | width: 100%; 106 | position: relative; 107 | transform-origin: bottom center; 108 | transform-style: preserve-3d; 109 | transform: rotateX(calc(var(--rotation, 0) * 1deg)); 110 | } 111 | button { 112 | padding: 8px 16px; 113 | } 114 | details { 115 | width: 100%; 116 | } 117 | summary { 118 | margin-bottom: 1rem; 119 | padding: 1rem 0; 120 | } 121 | .container path { 122 | fill: none; 123 | stroke: hsl(255, 100%, 50%); 124 | stroke-width: 4px; 125 | transition: stroke 0.25s ease; 126 | } 127 | 128 | .repo-link { 129 | position: fixed; 130 | top: 1rem; 131 | right: 1rem; 132 | height: 44px; 133 | width: 44px; 134 | display: flex; 135 | align-items: center; 136 | justify-content: center; 137 | } 138 | .container svg { 139 | display: var(--svg-display, 'block'); 140 | } 141 | .repo-link svg { 142 | height: 24px; 143 | width: 24px; 144 | } 145 | 146 | .repo-link path { 147 | fill: white; 148 | } 149 | 150 | svg { 151 | height: 100%; 152 | width: 100%; 153 | } 154 | label { 155 | display: block; 156 | margin-bottom: 0.5rem; 157 | font-weight: bold; 158 | } 159 | input { 160 | display: block; 161 | } 162 | [type=text], 163 | [type=number] { 164 | margin: 0; 165 | padding: 8px 16px; 166 | width: 100%; 167 | } 168 | a { 169 | color: hsl(0, 100%, 100%); 170 | } 171 | p { 172 | line-height: 1.5; 173 | text-align: left; 174 | width: 100%; 175 | } 176 | form { 177 | display: grid; 178 | grid-gap: 20px; 179 | } 180 | .form-field { 181 | margin-bottom: 1.25rem; 182 | } 183 | .form-field--grid { 184 | display: grid; 185 | grid-template-columns: auto 1fr; 186 | grid-template-rows: auto auto; 187 | grid-gap: 20px 10px; 188 | } 189 | @-moz-keyframes travel { 190 | from { 191 | offset-distance: 0%; 192 | } 193 | to { 194 | offset-distance: 100%; 195 | } 196 | } 197 | @-webkit-keyframes travel { 198 | from { 199 | offset-distance: 0%; 200 | } 201 | to { 202 | offset-distance: 100%; 203 | } 204 | } 205 | @-o-keyframes travel { 206 | from { 207 | offset-distance: 0%; 208 | } 209 | to { 210 | offset-distance: 100%; 211 | } 212 | } 213 | @keyframes travel { 214 | from { 215 | offset-distance: 0%; 216 | } 217 | to { 218 | offset-distance: 100%; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /playground/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Meanderer Playground 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | GitHub icon 13 | 14 | 15 | 16 |
17 |

Meanderer.js

18 |

Responsive CSS motion paths!

19 |
20 |
21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |

Demo

36 |

37 | Drag and drop an optimized SVG file onto the page that contains a path. 38 | Clean up your SVG first with 39 | 43 | SVGOMG 44 | 45 | . Alternatively, manually enter path info into the configuration form 46 | below. 47 |

48 |

49 | Resize the container/viewport to see your motion path scale! 50 |

51 |
52 | Path configuration 53 |
54 |
55 | 56 | 61 |
62 |
63 | 64 | 69 |
70 |
71 | 72 | 77 |
78 |
79 | 80 | 86 | 87 | 92 | 93 | 98 |
99 |
100 | 103 | 106 |
107 |
108 |
109 |

Interested?

110 |

Check out the Github repo for installation and usage instructions!

111 |
112 | 115 |
116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /playground/meanderer.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (global = global || self, global.Meanderer = factory()); 5 | }(this, (function () { 'use strict'; 6 | 7 | function _classCallCheck(instance, Constructor) { 8 | if (!(instance instanceof Constructor)) { 9 | throw new TypeError("Cannot call a class as a function"); 10 | } 11 | } 12 | 13 | function _slicedToArray(arr, i) { 14 | return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); 15 | } 16 | 17 | function _toConsumableArray(arr) { 18 | return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); 19 | } 20 | 21 | function _arrayWithoutHoles(arr) { 22 | if (Array.isArray(arr)) return _arrayLikeToArray(arr); 23 | } 24 | 25 | function _arrayWithHoles(arr) { 26 | if (Array.isArray(arr)) return arr; 27 | } 28 | 29 | function _iterableToArray(iter) { 30 | if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); 31 | } 32 | 33 | function _iterableToArrayLimit(arr, i) { 34 | if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; 35 | var _arr = []; 36 | var _n = true; 37 | var _d = false; 38 | var _e = undefined; 39 | 40 | try { 41 | for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { 42 | _arr.push(_s.value); 43 | 44 | if (i && _arr.length === i) break; 45 | } 46 | } catch (err) { 47 | _d = true; 48 | _e = err; 49 | } finally { 50 | try { 51 | if (!_n && _i["return"] != null) _i["return"](); 52 | } finally { 53 | if (_d) throw _e; 54 | } 55 | } 56 | 57 | return _arr; 58 | } 59 | 60 | function _unsupportedIterableToArray(o, minLen) { 61 | if (!o) return; 62 | if (typeof o === "string") return _arrayLikeToArray(o, minLen); 63 | var n = Object.prototype.toString.call(o).slice(8, -1); 64 | if (n === "Object" && o.constructor) n = o.constructor.name; 65 | if (n === "Map" || n === "Set") return Array.from(n); 66 | if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); 67 | } 68 | 69 | function _arrayLikeToArray(arr, len) { 70 | if (len == null || len > arr.length) len = arr.length; 71 | 72 | for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; 73 | 74 | return arr2; 75 | } 76 | 77 | function _nonIterableSpread() { 78 | throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); 79 | } 80 | 81 | function _nonIterableRest() { 82 | throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); 83 | } 84 | 85 | var pi = Math.PI, 86 | tau = 2 * pi, 87 | epsilon = 1e-6, 88 | tauEpsilon = tau - epsilon; 89 | 90 | function Path() { 91 | this._x0 = this._y0 = // start of current subpath 92 | this._x1 = this._y1 = null; // end of current subpath 93 | this._ = ""; 94 | } 95 | 96 | function path() { 97 | return new Path; 98 | } 99 | 100 | Path.prototype = path.prototype = { 101 | constructor: Path, 102 | moveTo: function(x, y) { 103 | this._ += "M" + (this._x0 = this._x1 = +x) + "," + (this._y0 = this._y1 = +y); 104 | }, 105 | closePath: function() { 106 | if (this._x1 !== null) { 107 | this._x1 = this._x0, this._y1 = this._y0; 108 | this._ += "Z"; 109 | } 110 | }, 111 | lineTo: function(x, y) { 112 | this._ += "L" + (this._x1 = +x) + "," + (this._y1 = +y); 113 | }, 114 | quadraticCurveTo: function(x1, y1, x, y) { 115 | this._ += "Q" + (+x1) + "," + (+y1) + "," + (this._x1 = +x) + "," + (this._y1 = +y); 116 | }, 117 | bezierCurveTo: function(x1, y1, x2, y2, x, y) { 118 | this._ += "C" + (+x1) + "," + (+y1) + "," + (+x2) + "," + (+y2) + "," + (this._x1 = +x) + "," + (this._y1 = +y); 119 | }, 120 | arcTo: function(x1, y1, x2, y2, r) { 121 | x1 = +x1, y1 = +y1, x2 = +x2, y2 = +y2, r = +r; 122 | var x0 = this._x1, 123 | y0 = this._y1, 124 | x21 = x2 - x1, 125 | y21 = y2 - y1, 126 | x01 = x0 - x1, 127 | y01 = y0 - y1, 128 | l01_2 = x01 * x01 + y01 * y01; 129 | 130 | // Is the radius negative? Error. 131 | if (r < 0) throw new Error("negative radius: " + r); 132 | 133 | // Is this path empty? Move to (x1,y1). 134 | if (this._x1 === null) { 135 | this._ += "M" + (this._x1 = x1) + "," + (this._y1 = y1); 136 | } 137 | 138 | // Or, is (x1,y1) coincident with (x0,y0)? Do nothing. 139 | else if (!(l01_2 > epsilon)); 140 | 141 | // Or, are (x0,y0), (x1,y1) and (x2,y2) collinear? 142 | // Equivalently, is (x1,y1) coincident with (x2,y2)? 143 | // Or, is the radius zero? Line to (x1,y1). 144 | else if (!(Math.abs(y01 * x21 - y21 * x01) > epsilon) || !r) { 145 | this._ += "L" + (this._x1 = x1) + "," + (this._y1 = y1); 146 | } 147 | 148 | // Otherwise, draw an arc! 149 | else { 150 | var x20 = x2 - x0, 151 | y20 = y2 - y0, 152 | l21_2 = x21 * x21 + y21 * y21, 153 | l20_2 = x20 * x20 + y20 * y20, 154 | l21 = Math.sqrt(l21_2), 155 | l01 = Math.sqrt(l01_2), 156 | l = r * Math.tan((pi - Math.acos((l21_2 + l01_2 - l20_2) / (2 * l21 * l01))) / 2), 157 | t01 = l / l01, 158 | t21 = l / l21; 159 | 160 | // If the start tangent is not coincident with (x0,y0), line to. 161 | if (Math.abs(t01 - 1) > epsilon) { 162 | this._ += "L" + (x1 + t01 * x01) + "," + (y1 + t01 * y01); 163 | } 164 | 165 | this._ += "A" + r + "," + r + ",0,0," + (+(y01 * x20 > x01 * y20)) + "," + (this._x1 = x1 + t21 * x21) + "," + (this._y1 = y1 + t21 * y21); 166 | } 167 | }, 168 | arc: function(x, y, r, a0, a1, ccw) { 169 | x = +x, y = +y, r = +r, ccw = !!ccw; 170 | var dx = r * Math.cos(a0), 171 | dy = r * Math.sin(a0), 172 | x0 = x + dx, 173 | y0 = y + dy, 174 | cw = 1 ^ ccw, 175 | da = ccw ? a0 - a1 : a1 - a0; 176 | 177 | // Is the radius negative? Error. 178 | if (r < 0) throw new Error("negative radius: " + r); 179 | 180 | // Is this path empty? Move to (x0,y0). 181 | if (this._x1 === null) { 182 | this._ += "M" + x0 + "," + y0; 183 | } 184 | 185 | // Or, is (x0,y0) not coincident with the previous point? Line to (x0,y0). 186 | else if (Math.abs(this._x1 - x0) > epsilon || Math.abs(this._y1 - y0) > epsilon) { 187 | this._ += "L" + x0 + "," + y0; 188 | } 189 | 190 | // Is this arc empty? We’re done. 191 | if (!r) return; 192 | 193 | // Does the angle go the wrong way? Flip the direction. 194 | if (da < 0) da = da % tau + tau; 195 | 196 | // Is this a complete circle? Draw two arcs to complete the circle. 197 | if (da > tauEpsilon) { 198 | this._ += "A" + r + "," + r + ",0,1," + cw + "," + (x - dx) + "," + (y - dy) + "A" + r + "," + r + ",0,1," + cw + "," + (this._x1 = x0) + "," + (this._y1 = y0); 199 | } 200 | 201 | // Is this arc non-empty? Draw an arc! 202 | else if (da > epsilon) { 203 | this._ += "A" + r + "," + r + ",0," + (+(da >= pi)) + "," + cw + "," + (this._x1 = x + r * Math.cos(a1)) + "," + (this._y1 = y + r * Math.sin(a1)); 204 | } 205 | }, 206 | rect: function(x, y, w, h) { 207 | this._ += "M" + (this._x0 = this._x1 = +x) + "," + (this._y0 = this._y1 = +y) + "h" + (+w) + "v" + (+h) + "h" + (-w) + "Z"; 208 | }, 209 | toString: function() { 210 | return this._; 211 | } 212 | }; 213 | 214 | function constant(x) { 215 | return function constant() { 216 | return x; 217 | }; 218 | } 219 | 220 | function Linear(context) { 221 | this._context = context; 222 | } 223 | 224 | Linear.prototype = { 225 | areaStart: function() { 226 | this._line = 0; 227 | }, 228 | areaEnd: function() { 229 | this._line = NaN; 230 | }, 231 | lineStart: function() { 232 | this._point = 0; 233 | }, 234 | lineEnd: function() { 235 | if (this._line || (this._line !== 0 && this._point === 1)) this._context.closePath(); 236 | this._line = 1 - this._line; 237 | }, 238 | point: function(x, y) { 239 | x = +x, y = +y; 240 | switch (this._point) { 241 | case 0: this._point = 1; this._line ? this._context.lineTo(x, y) : this._context.moveTo(x, y); break; 242 | case 1: this._point = 2; // proceed 243 | default: this._context.lineTo(x, y); break; 244 | } 245 | } 246 | }; 247 | 248 | function curveLinear(context) { 249 | return new Linear(context); 250 | } 251 | 252 | function x(p) { 253 | return p[0]; 254 | } 255 | 256 | function y(p) { 257 | return p[1]; 258 | } 259 | 260 | function line() { 261 | var x$1 = x, 262 | y$1 = y, 263 | defined = constant(true), 264 | context = null, 265 | curve = curveLinear, 266 | output = null; 267 | 268 | function line(data) { 269 | var i, 270 | n = data.length, 271 | d, 272 | defined0 = false, 273 | buffer; 274 | 275 | if (context == null) output = curve(buffer = path()); 276 | 277 | for (i = 0; i <= n; ++i) { 278 | if (!(i < n && defined(d = data[i], i, data)) === defined0) { 279 | if (defined0 = !defined0) output.lineStart(); 280 | else output.lineEnd(); 281 | } 282 | if (defined0) output.point(+x$1(d, i, data), +y$1(d, i, data)); 283 | } 284 | 285 | if (buffer) return output = null, buffer + "" || null; 286 | } 287 | 288 | line.x = function(_) { 289 | return arguments.length ? (x$1 = typeof _ === "function" ? _ : constant(+_), line) : x$1; 290 | }; 291 | 292 | line.y = function(_) { 293 | return arguments.length ? (y$1 = typeof _ === "function" ? _ : constant(+_), line) : y$1; 294 | }; 295 | 296 | line.defined = function(_) { 297 | return arguments.length ? (defined = typeof _ === "function" ? _ : constant(!!_), line) : defined; 298 | }; 299 | 300 | line.curve = function(_) { 301 | return arguments.length ? (curve = _, context != null && (output = curve(context)), line) : curve; 302 | }; 303 | 304 | line.context = function(_) { 305 | return arguments.length ? (_ == null ? context = output = null : output = curve(context = _), line) : context; 306 | }; 307 | 308 | return line; 309 | } 310 | 311 | function ascending(a, b) { 312 | return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN; 313 | } 314 | 315 | function bisector(compare) { 316 | if (compare.length === 1) compare = ascendingComparator(compare); 317 | return { 318 | left: function(a, x, lo, hi) { 319 | if (lo == null) lo = 0; 320 | if (hi == null) hi = a.length; 321 | while (lo < hi) { 322 | var mid = lo + hi >>> 1; 323 | if (compare(a[mid], x) < 0) lo = mid + 1; 324 | else hi = mid; 325 | } 326 | return lo; 327 | }, 328 | right: function(a, x, lo, hi) { 329 | if (lo == null) lo = 0; 330 | if (hi == null) hi = a.length; 331 | while (lo < hi) { 332 | var mid = lo + hi >>> 1; 333 | if (compare(a[mid], x) > 0) hi = mid; 334 | else lo = mid + 1; 335 | } 336 | return lo; 337 | } 338 | }; 339 | } 340 | 341 | function ascendingComparator(f) { 342 | return function(d, x) { 343 | return ascending(f(d), x); 344 | }; 345 | } 346 | 347 | var ascendingBisect = bisector(ascending); 348 | var bisectRight = ascendingBisect.right; 349 | 350 | var e10 = Math.sqrt(50), 351 | e5 = Math.sqrt(10), 352 | e2 = Math.sqrt(2); 353 | 354 | function ticks(start, stop, count) { 355 | var reverse, 356 | i = -1, 357 | n, 358 | ticks, 359 | step; 360 | 361 | stop = +stop, start = +start, count = +count; 362 | if (start === stop && count > 0) return [start]; 363 | if (reverse = stop < start) n = start, start = stop, stop = n; 364 | if ((step = tickIncrement(start, stop, count)) === 0 || !isFinite(step)) return []; 365 | 366 | if (step > 0) { 367 | start = Math.ceil(start / step); 368 | stop = Math.floor(stop / step); 369 | ticks = new Array(n = Math.ceil(stop - start + 1)); 370 | while (++i < n) ticks[i] = (start + i) * step; 371 | } else { 372 | start = Math.floor(start * step); 373 | stop = Math.ceil(stop * step); 374 | ticks = new Array(n = Math.ceil(start - stop + 1)); 375 | while (++i < n) ticks[i] = (start - i) / step; 376 | } 377 | 378 | if (reverse) ticks.reverse(); 379 | 380 | return ticks; 381 | } 382 | 383 | function tickIncrement(start, stop, count) { 384 | var step = (stop - start) / Math.max(0, count), 385 | power = Math.floor(Math.log(step) / Math.LN10), 386 | error = step / Math.pow(10, power); 387 | return power >= 0 388 | ? (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1) * Math.pow(10, power) 389 | : -Math.pow(10, -power) / (error >= e10 ? 10 : error >= e5 ? 5 : error >= e2 ? 2 : 1); 390 | } 391 | 392 | function tickStep(start, stop, count) { 393 | var step0 = Math.abs(stop - start) / Math.max(0, count), 394 | step1 = Math.pow(10, Math.floor(Math.log(step0) / Math.LN10)), 395 | error = step0 / step1; 396 | if (error >= e10) step1 *= 10; 397 | else if (error >= e5) step1 *= 5; 398 | else if (error >= e2) step1 *= 2; 399 | return stop < start ? -step1 : step1; 400 | } 401 | 402 | function initRange(domain, range) { 403 | switch (arguments.length) { 404 | case 0: break; 405 | case 1: this.range(domain); break; 406 | default: this.range(range).domain(domain); break; 407 | } 408 | return this; 409 | } 410 | 411 | var prefix = "$"; 412 | 413 | function Map() {} 414 | 415 | Map.prototype = map.prototype = { 416 | constructor: Map, 417 | has: function(key) { 418 | return (prefix + key) in this; 419 | }, 420 | get: function(key) { 421 | return this[prefix + key]; 422 | }, 423 | set: function(key, value) { 424 | this[prefix + key] = value; 425 | return this; 426 | }, 427 | remove: function(key) { 428 | var property = prefix + key; 429 | return property in this && delete this[property]; 430 | }, 431 | clear: function() { 432 | for (var property in this) if (property[0] === prefix) delete this[property]; 433 | }, 434 | keys: function() { 435 | var keys = []; 436 | for (var property in this) if (property[0] === prefix) keys.push(property.slice(1)); 437 | return keys; 438 | }, 439 | values: function() { 440 | var values = []; 441 | for (var property in this) if (property[0] === prefix) values.push(this[property]); 442 | return values; 443 | }, 444 | entries: function() { 445 | var entries = []; 446 | for (var property in this) if (property[0] === prefix) entries.push({key: property.slice(1), value: this[property]}); 447 | return entries; 448 | }, 449 | size: function() { 450 | var size = 0; 451 | for (var property in this) if (property[0] === prefix) ++size; 452 | return size; 453 | }, 454 | empty: function() { 455 | for (var property in this) if (property[0] === prefix) return false; 456 | return true; 457 | }, 458 | each: function(f) { 459 | for (var property in this) if (property[0] === prefix) f(this[property], property.slice(1), this); 460 | } 461 | }; 462 | 463 | function map(object, f) { 464 | var map = new Map; 465 | 466 | // Copy constructor. 467 | if (object instanceof Map) object.each(function(value, key) { map.set(key, value); }); 468 | 469 | // Index array by numeric index or specified key function. 470 | else if (Array.isArray(object)) { 471 | var i = -1, 472 | n = object.length, 473 | o; 474 | 475 | if (f == null) while (++i < n) map.set(i, object[i]); 476 | else while (++i < n) map.set(f(o = object[i], i, object), o); 477 | } 478 | 479 | // Convert object to map. 480 | else if (object) for (var key in object) map.set(key, object[key]); 481 | 482 | return map; 483 | } 484 | 485 | function Set() {} 486 | 487 | var proto = map.prototype; 488 | 489 | Set.prototype = set.prototype = { 490 | constructor: Set, 491 | has: proto.has, 492 | add: function(value) { 493 | value += ""; 494 | this[prefix + value] = value; 495 | return this; 496 | }, 497 | remove: proto.remove, 498 | clear: proto.clear, 499 | values: proto.keys, 500 | size: proto.size, 501 | empty: proto.empty, 502 | each: proto.each 503 | }; 504 | 505 | function set(object, f) { 506 | var set = new Set; 507 | 508 | // Copy constructor. 509 | if (object instanceof Set) object.each(function(value) { set.add(value); }); 510 | 511 | // Otherwise, assume it’s an array. 512 | else if (object) { 513 | var i = -1, n = object.length; 514 | if (f == null) while (++i < n) set.add(object[i]); 515 | else while (++i < n) set.add(f(object[i], i, object)); 516 | } 517 | 518 | return set; 519 | } 520 | 521 | var array = Array.prototype; 522 | 523 | var map$1 = array.map; 524 | var slice = array.slice; 525 | 526 | function define(constructor, factory, prototype) { 527 | constructor.prototype = factory.prototype = prototype; 528 | prototype.constructor = constructor; 529 | } 530 | 531 | function extend(parent, definition) { 532 | var prototype = Object.create(parent.prototype); 533 | for (var key in definition) prototype[key] = definition[key]; 534 | return prototype; 535 | } 536 | 537 | function Color() {} 538 | 539 | var darker = 0.7; 540 | var brighter = 1 / darker; 541 | 542 | var reI = "\\s*([+-]?\\d+)\\s*", 543 | reN = "\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)\\s*", 544 | reP = "\\s*([+-]?\\d*\\.?\\d+(?:[eE][+-]?\\d+)?)%\\s*", 545 | reHex = /^#([0-9a-f]{3,8})$/, 546 | reRgbInteger = new RegExp("^rgb\\(" + [reI, reI, reI] + "\\)$"), 547 | reRgbPercent = new RegExp("^rgb\\(" + [reP, reP, reP] + "\\)$"), 548 | reRgbaInteger = new RegExp("^rgba\\(" + [reI, reI, reI, reN] + "\\)$"), 549 | reRgbaPercent = new RegExp("^rgba\\(" + [reP, reP, reP, reN] + "\\)$"), 550 | reHslPercent = new RegExp("^hsl\\(" + [reN, reP, reP] + "\\)$"), 551 | reHslaPercent = new RegExp("^hsla\\(" + [reN, reP, reP, reN] + "\\)$"); 552 | 553 | var named = { 554 | aliceblue: 0xf0f8ff, 555 | antiquewhite: 0xfaebd7, 556 | aqua: 0x00ffff, 557 | aquamarine: 0x7fffd4, 558 | azure: 0xf0ffff, 559 | beige: 0xf5f5dc, 560 | bisque: 0xffe4c4, 561 | black: 0x000000, 562 | blanchedalmond: 0xffebcd, 563 | blue: 0x0000ff, 564 | blueviolet: 0x8a2be2, 565 | brown: 0xa52a2a, 566 | burlywood: 0xdeb887, 567 | cadetblue: 0x5f9ea0, 568 | chartreuse: 0x7fff00, 569 | chocolate: 0xd2691e, 570 | coral: 0xff7f50, 571 | cornflowerblue: 0x6495ed, 572 | cornsilk: 0xfff8dc, 573 | crimson: 0xdc143c, 574 | cyan: 0x00ffff, 575 | darkblue: 0x00008b, 576 | darkcyan: 0x008b8b, 577 | darkgoldenrod: 0xb8860b, 578 | darkgray: 0xa9a9a9, 579 | darkgreen: 0x006400, 580 | darkgrey: 0xa9a9a9, 581 | darkkhaki: 0xbdb76b, 582 | darkmagenta: 0x8b008b, 583 | darkolivegreen: 0x556b2f, 584 | darkorange: 0xff8c00, 585 | darkorchid: 0x9932cc, 586 | darkred: 0x8b0000, 587 | darksalmon: 0xe9967a, 588 | darkseagreen: 0x8fbc8f, 589 | darkslateblue: 0x483d8b, 590 | darkslategray: 0x2f4f4f, 591 | darkslategrey: 0x2f4f4f, 592 | darkturquoise: 0x00ced1, 593 | darkviolet: 0x9400d3, 594 | deeppink: 0xff1493, 595 | deepskyblue: 0x00bfff, 596 | dimgray: 0x696969, 597 | dimgrey: 0x696969, 598 | dodgerblue: 0x1e90ff, 599 | firebrick: 0xb22222, 600 | floralwhite: 0xfffaf0, 601 | forestgreen: 0x228b22, 602 | fuchsia: 0xff00ff, 603 | gainsboro: 0xdcdcdc, 604 | ghostwhite: 0xf8f8ff, 605 | gold: 0xffd700, 606 | goldenrod: 0xdaa520, 607 | gray: 0x808080, 608 | green: 0x008000, 609 | greenyellow: 0xadff2f, 610 | grey: 0x808080, 611 | honeydew: 0xf0fff0, 612 | hotpink: 0xff69b4, 613 | indianred: 0xcd5c5c, 614 | indigo: 0x4b0082, 615 | ivory: 0xfffff0, 616 | khaki: 0xf0e68c, 617 | lavender: 0xe6e6fa, 618 | lavenderblush: 0xfff0f5, 619 | lawngreen: 0x7cfc00, 620 | lemonchiffon: 0xfffacd, 621 | lightblue: 0xadd8e6, 622 | lightcoral: 0xf08080, 623 | lightcyan: 0xe0ffff, 624 | lightgoldenrodyellow: 0xfafad2, 625 | lightgray: 0xd3d3d3, 626 | lightgreen: 0x90ee90, 627 | lightgrey: 0xd3d3d3, 628 | lightpink: 0xffb6c1, 629 | lightsalmon: 0xffa07a, 630 | lightseagreen: 0x20b2aa, 631 | lightskyblue: 0x87cefa, 632 | lightslategray: 0x778899, 633 | lightslategrey: 0x778899, 634 | lightsteelblue: 0xb0c4de, 635 | lightyellow: 0xffffe0, 636 | lime: 0x00ff00, 637 | limegreen: 0x32cd32, 638 | linen: 0xfaf0e6, 639 | magenta: 0xff00ff, 640 | maroon: 0x800000, 641 | mediumaquamarine: 0x66cdaa, 642 | mediumblue: 0x0000cd, 643 | mediumorchid: 0xba55d3, 644 | mediumpurple: 0x9370db, 645 | mediumseagreen: 0x3cb371, 646 | mediumslateblue: 0x7b68ee, 647 | mediumspringgreen: 0x00fa9a, 648 | mediumturquoise: 0x48d1cc, 649 | mediumvioletred: 0xc71585, 650 | midnightblue: 0x191970, 651 | mintcream: 0xf5fffa, 652 | mistyrose: 0xffe4e1, 653 | moccasin: 0xffe4b5, 654 | navajowhite: 0xffdead, 655 | navy: 0x000080, 656 | oldlace: 0xfdf5e6, 657 | olive: 0x808000, 658 | olivedrab: 0x6b8e23, 659 | orange: 0xffa500, 660 | orangered: 0xff4500, 661 | orchid: 0xda70d6, 662 | palegoldenrod: 0xeee8aa, 663 | palegreen: 0x98fb98, 664 | paleturquoise: 0xafeeee, 665 | palevioletred: 0xdb7093, 666 | papayawhip: 0xffefd5, 667 | peachpuff: 0xffdab9, 668 | peru: 0xcd853f, 669 | pink: 0xffc0cb, 670 | plum: 0xdda0dd, 671 | powderblue: 0xb0e0e6, 672 | purple: 0x800080, 673 | rebeccapurple: 0x663399, 674 | red: 0xff0000, 675 | rosybrown: 0xbc8f8f, 676 | royalblue: 0x4169e1, 677 | saddlebrown: 0x8b4513, 678 | salmon: 0xfa8072, 679 | sandybrown: 0xf4a460, 680 | seagreen: 0x2e8b57, 681 | seashell: 0xfff5ee, 682 | sienna: 0xa0522d, 683 | silver: 0xc0c0c0, 684 | skyblue: 0x87ceeb, 685 | slateblue: 0x6a5acd, 686 | slategray: 0x708090, 687 | slategrey: 0x708090, 688 | snow: 0xfffafa, 689 | springgreen: 0x00ff7f, 690 | steelblue: 0x4682b4, 691 | tan: 0xd2b48c, 692 | teal: 0x008080, 693 | thistle: 0xd8bfd8, 694 | tomato: 0xff6347, 695 | turquoise: 0x40e0d0, 696 | violet: 0xee82ee, 697 | wheat: 0xf5deb3, 698 | white: 0xffffff, 699 | whitesmoke: 0xf5f5f5, 700 | yellow: 0xffff00, 701 | yellowgreen: 0x9acd32 702 | }; 703 | 704 | define(Color, color, { 705 | copy: function(channels) { 706 | return Object.assign(new this.constructor, this, channels); 707 | }, 708 | displayable: function() { 709 | return this.rgb().displayable(); 710 | }, 711 | hex: color_formatHex, // Deprecated! Use color.formatHex. 712 | formatHex: color_formatHex, 713 | formatHsl: color_formatHsl, 714 | formatRgb: color_formatRgb, 715 | toString: color_formatRgb 716 | }); 717 | 718 | function color_formatHex() { 719 | return this.rgb().formatHex(); 720 | } 721 | 722 | function color_formatHsl() { 723 | return hslConvert(this).formatHsl(); 724 | } 725 | 726 | function color_formatRgb() { 727 | return this.rgb().formatRgb(); 728 | } 729 | 730 | function color(format) { 731 | var m, l; 732 | format = (format + "").trim().toLowerCase(); 733 | return (m = reHex.exec(format)) ? (l = m[1].length, m = parseInt(m[1], 16), l === 6 ? rgbn(m) // #ff0000 734 | : l === 3 ? new Rgb((m >> 8 & 0xf) | (m >> 4 & 0xf0), (m >> 4 & 0xf) | (m & 0xf0), ((m & 0xf) << 4) | (m & 0xf), 1) // #f00 735 | : l === 8 ? new Rgb(m >> 24 & 0xff, m >> 16 & 0xff, m >> 8 & 0xff, (m & 0xff) / 0xff) // #ff000000 736 | : l === 4 ? new Rgb((m >> 12 & 0xf) | (m >> 8 & 0xf0), (m >> 8 & 0xf) | (m >> 4 & 0xf0), (m >> 4 & 0xf) | (m & 0xf0), (((m & 0xf) << 4) | (m & 0xf)) / 0xff) // #f000 737 | : null) // invalid hex 738 | : (m = reRgbInteger.exec(format)) ? new Rgb(m[1], m[2], m[3], 1) // rgb(255, 0, 0) 739 | : (m = reRgbPercent.exec(format)) ? new Rgb(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, 1) // rgb(100%, 0%, 0%) 740 | : (m = reRgbaInteger.exec(format)) ? rgba(m[1], m[2], m[3], m[4]) // rgba(255, 0, 0, 1) 741 | : (m = reRgbaPercent.exec(format)) ? rgba(m[1] * 255 / 100, m[2] * 255 / 100, m[3] * 255 / 100, m[4]) // rgb(100%, 0%, 0%, 1) 742 | : (m = reHslPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, 1) // hsl(120, 50%, 50%) 743 | : (m = reHslaPercent.exec(format)) ? hsla(m[1], m[2] / 100, m[3] / 100, m[4]) // hsla(120, 50%, 50%, 1) 744 | : named.hasOwnProperty(format) ? rgbn(named[format]) // eslint-disable-line no-prototype-builtins 745 | : format === "transparent" ? new Rgb(NaN, NaN, NaN, 0) 746 | : null; 747 | } 748 | 749 | function rgbn(n) { 750 | return new Rgb(n >> 16 & 0xff, n >> 8 & 0xff, n & 0xff, 1); 751 | } 752 | 753 | function rgba(r, g, b, a) { 754 | if (a <= 0) r = g = b = NaN; 755 | return new Rgb(r, g, b, a); 756 | } 757 | 758 | function rgbConvert(o) { 759 | if (!(o instanceof Color)) o = color(o); 760 | if (!o) return new Rgb; 761 | o = o.rgb(); 762 | return new Rgb(o.r, o.g, o.b, o.opacity); 763 | } 764 | 765 | function rgb(r, g, b, opacity) { 766 | return arguments.length === 1 ? rgbConvert(r) : new Rgb(r, g, b, opacity == null ? 1 : opacity); 767 | } 768 | 769 | function Rgb(r, g, b, opacity) { 770 | this.r = +r; 771 | this.g = +g; 772 | this.b = +b; 773 | this.opacity = +opacity; 774 | } 775 | 776 | define(Rgb, rgb, extend(Color, { 777 | brighter: function(k) { 778 | k = k == null ? brighter : Math.pow(brighter, k); 779 | return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity); 780 | }, 781 | darker: function(k) { 782 | k = k == null ? darker : Math.pow(darker, k); 783 | return new Rgb(this.r * k, this.g * k, this.b * k, this.opacity); 784 | }, 785 | rgb: function() { 786 | return this; 787 | }, 788 | displayable: function() { 789 | return (-0.5 <= this.r && this.r < 255.5) 790 | && (-0.5 <= this.g && this.g < 255.5) 791 | && (-0.5 <= this.b && this.b < 255.5) 792 | && (0 <= this.opacity && this.opacity <= 1); 793 | }, 794 | hex: rgb_formatHex, // Deprecated! Use color.formatHex. 795 | formatHex: rgb_formatHex, 796 | formatRgb: rgb_formatRgb, 797 | toString: rgb_formatRgb 798 | })); 799 | 800 | function rgb_formatHex() { 801 | return "#" + hex(this.r) + hex(this.g) + hex(this.b); 802 | } 803 | 804 | function rgb_formatRgb() { 805 | var a = this.opacity; a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a)); 806 | return (a === 1 ? "rgb(" : "rgba(") 807 | + Math.max(0, Math.min(255, Math.round(this.r) || 0)) + ", " 808 | + Math.max(0, Math.min(255, Math.round(this.g) || 0)) + ", " 809 | + Math.max(0, Math.min(255, Math.round(this.b) || 0)) 810 | + (a === 1 ? ")" : ", " + a + ")"); 811 | } 812 | 813 | function hex(value) { 814 | value = Math.max(0, Math.min(255, Math.round(value) || 0)); 815 | return (value < 16 ? "0" : "") + value.toString(16); 816 | } 817 | 818 | function hsla(h, s, l, a) { 819 | if (a <= 0) h = s = l = NaN; 820 | else if (l <= 0 || l >= 1) h = s = NaN; 821 | else if (s <= 0) h = NaN; 822 | return new Hsl(h, s, l, a); 823 | } 824 | 825 | function hslConvert(o) { 826 | if (o instanceof Hsl) return new Hsl(o.h, o.s, o.l, o.opacity); 827 | if (!(o instanceof Color)) o = color(o); 828 | if (!o) return new Hsl; 829 | if (o instanceof Hsl) return o; 830 | o = o.rgb(); 831 | var r = o.r / 255, 832 | g = o.g / 255, 833 | b = o.b / 255, 834 | min = Math.min(r, g, b), 835 | max = Math.max(r, g, b), 836 | h = NaN, 837 | s = max - min, 838 | l = (max + min) / 2; 839 | if (s) { 840 | if (r === max) h = (g - b) / s + (g < b) * 6; 841 | else if (g === max) h = (b - r) / s + 2; 842 | else h = (r - g) / s + 4; 843 | s /= l < 0.5 ? max + min : 2 - max - min; 844 | h *= 60; 845 | } else { 846 | s = l > 0 && l < 1 ? 0 : h; 847 | } 848 | return new Hsl(h, s, l, o.opacity); 849 | } 850 | 851 | function hsl(h, s, l, opacity) { 852 | return arguments.length === 1 ? hslConvert(h) : new Hsl(h, s, l, opacity == null ? 1 : opacity); 853 | } 854 | 855 | function Hsl(h, s, l, opacity) { 856 | this.h = +h; 857 | this.s = +s; 858 | this.l = +l; 859 | this.opacity = +opacity; 860 | } 861 | 862 | define(Hsl, hsl, extend(Color, { 863 | brighter: function(k) { 864 | k = k == null ? brighter : Math.pow(brighter, k); 865 | return new Hsl(this.h, this.s, this.l * k, this.opacity); 866 | }, 867 | darker: function(k) { 868 | k = k == null ? darker : Math.pow(darker, k); 869 | return new Hsl(this.h, this.s, this.l * k, this.opacity); 870 | }, 871 | rgb: function() { 872 | var h = this.h % 360 + (this.h < 0) * 360, 873 | s = isNaN(h) || isNaN(this.s) ? 0 : this.s, 874 | l = this.l, 875 | m2 = l + (l < 0.5 ? l : 1 - l) * s, 876 | m1 = 2 * l - m2; 877 | return new Rgb( 878 | hsl2rgb(h >= 240 ? h - 240 : h + 120, m1, m2), 879 | hsl2rgb(h, m1, m2), 880 | hsl2rgb(h < 120 ? h + 240 : h - 120, m1, m2), 881 | this.opacity 882 | ); 883 | }, 884 | displayable: function() { 885 | return (0 <= this.s && this.s <= 1 || isNaN(this.s)) 886 | && (0 <= this.l && this.l <= 1) 887 | && (0 <= this.opacity && this.opacity <= 1); 888 | }, 889 | formatHsl: function() { 890 | var a = this.opacity; a = isNaN(a) ? 1 : Math.max(0, Math.min(1, a)); 891 | return (a === 1 ? "hsl(" : "hsla(") 892 | + (this.h || 0) + ", " 893 | + (this.s || 0) * 100 + "%, " 894 | + (this.l || 0) * 100 + "%" 895 | + (a === 1 ? ")" : ", " + a + ")"); 896 | } 897 | })); 898 | 899 | /* From FvD 13.37, CSS Color Module Level 3 */ 900 | function hsl2rgb(h, m1, m2) { 901 | return (h < 60 ? m1 + (m2 - m1) * h / 60 902 | : h < 180 ? m2 903 | : h < 240 ? m1 + (m2 - m1) * (240 - h) / 60 904 | : m1) * 255; 905 | } 906 | 907 | function constant$1(x) { 908 | return function() { 909 | return x; 910 | }; 911 | } 912 | 913 | function linear(a, d) { 914 | return function(t) { 915 | return a + t * d; 916 | }; 917 | } 918 | 919 | function exponential(a, b, y) { 920 | return a = Math.pow(a, y), b = Math.pow(b, y) - a, y = 1 / y, function(t) { 921 | return Math.pow(a + t * b, y); 922 | }; 923 | } 924 | 925 | function gamma(y) { 926 | return (y = +y) === 1 ? nogamma : function(a, b) { 927 | return b - a ? exponential(a, b, y) : constant$1(isNaN(a) ? b : a); 928 | }; 929 | } 930 | 931 | function nogamma(a, b) { 932 | var d = b - a; 933 | return d ? linear(a, d) : constant$1(isNaN(a) ? b : a); 934 | } 935 | 936 | var rgb$1 = (function rgbGamma(y) { 937 | var color = gamma(y); 938 | 939 | function rgb$1(start, end) { 940 | var r = color((start = rgb(start)).r, (end = rgb(end)).r), 941 | g = color(start.g, end.g), 942 | b = color(start.b, end.b), 943 | opacity = nogamma(start.opacity, end.opacity); 944 | return function(t) { 945 | start.r = r(t); 946 | start.g = g(t); 947 | start.b = b(t); 948 | start.opacity = opacity(t); 949 | return start + ""; 950 | }; 951 | } 952 | 953 | rgb$1.gamma = rgbGamma; 954 | 955 | return rgb$1; 956 | })(1); 957 | 958 | function numberArray(a, b) { 959 | if (!b) b = []; 960 | var n = a ? Math.min(b.length, a.length) : 0, 961 | c = b.slice(), 962 | i; 963 | return function(t) { 964 | for (i = 0; i < n; ++i) c[i] = a[i] * (1 - t) + b[i] * t; 965 | return c; 966 | }; 967 | } 968 | 969 | function isNumberArray(x) { 970 | return ArrayBuffer.isView(x) && !(x instanceof DataView); 971 | } 972 | 973 | function genericArray(a, b) { 974 | var nb = b ? b.length : 0, 975 | na = a ? Math.min(nb, a.length) : 0, 976 | x = new Array(na), 977 | c = new Array(nb), 978 | i; 979 | 980 | for (i = 0; i < na; ++i) x[i] = interpolateValue(a[i], b[i]); 981 | for (; i < nb; ++i) c[i] = b[i]; 982 | 983 | return function(t) { 984 | for (i = 0; i < na; ++i) c[i] = x[i](t); 985 | return c; 986 | }; 987 | } 988 | 989 | function date(a, b) { 990 | var d = new Date; 991 | return a = +a, b = +b, function(t) { 992 | return d.setTime(a * (1 - t) + b * t), d; 993 | }; 994 | } 995 | 996 | function interpolateNumber(a, b) { 997 | return a = +a, b = +b, function(t) { 998 | return a * (1 - t) + b * t; 999 | }; 1000 | } 1001 | 1002 | function object(a, b) { 1003 | var i = {}, 1004 | c = {}, 1005 | k; 1006 | 1007 | if (a === null || typeof a !== "object") a = {}; 1008 | if (b === null || typeof b !== "object") b = {}; 1009 | 1010 | for (k in b) { 1011 | if (k in a) { 1012 | i[k] = interpolateValue(a[k], b[k]); 1013 | } else { 1014 | c[k] = b[k]; 1015 | } 1016 | } 1017 | 1018 | return function(t) { 1019 | for (k in i) c[k] = i[k](t); 1020 | return c; 1021 | }; 1022 | } 1023 | 1024 | var reA = /[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g, 1025 | reB = new RegExp(reA.source, "g"); 1026 | 1027 | function zero(b) { 1028 | return function() { 1029 | return b; 1030 | }; 1031 | } 1032 | 1033 | function one(b) { 1034 | return function(t) { 1035 | return b(t) + ""; 1036 | }; 1037 | } 1038 | 1039 | function string(a, b) { 1040 | var bi = reA.lastIndex = reB.lastIndex = 0, // scan index for next number in b 1041 | am, // current match in a 1042 | bm, // current match in b 1043 | bs, // string preceding current number in b, if any 1044 | i = -1, // index in s 1045 | s = [], // string constants and placeholders 1046 | q = []; // number interpolators 1047 | 1048 | // Coerce inputs to strings. 1049 | a = a + "", b = b + ""; 1050 | 1051 | // Interpolate pairs of numbers in a & b. 1052 | while ((am = reA.exec(a)) 1053 | && (bm = reB.exec(b))) { 1054 | if ((bs = bm.index) > bi) { // a string precedes the next number in b 1055 | bs = b.slice(bi, bs); 1056 | if (s[i]) s[i] += bs; // coalesce with previous string 1057 | else s[++i] = bs; 1058 | } 1059 | if ((am = am[0]) === (bm = bm[0])) { // numbers in a & b match 1060 | if (s[i]) s[i] += bm; // coalesce with previous string 1061 | else s[++i] = bm; 1062 | } else { // interpolate non-matching numbers 1063 | s[++i] = null; 1064 | q.push({i: i, x: interpolateNumber(am, bm)}); 1065 | } 1066 | bi = reB.lastIndex; 1067 | } 1068 | 1069 | // Add remains of b. 1070 | if (bi < b.length) { 1071 | bs = b.slice(bi); 1072 | if (s[i]) s[i] += bs; // coalesce with previous string 1073 | else s[++i] = bs; 1074 | } 1075 | 1076 | // Special optimization for only a single match. 1077 | // Otherwise, interpolate each of the numbers and rejoin the string. 1078 | return s.length < 2 ? (q[0] 1079 | ? one(q[0].x) 1080 | : zero(b)) 1081 | : (b = q.length, function(t) { 1082 | for (var i = 0, o; i < b; ++i) s[(o = q[i]).i] = o.x(t); 1083 | return s.join(""); 1084 | }); 1085 | } 1086 | 1087 | function interpolateValue(a, b) { 1088 | var t = typeof b, c; 1089 | return b == null || t === "boolean" ? constant$1(b) 1090 | : (t === "number" ? interpolateNumber 1091 | : t === "string" ? ((c = color(b)) ? (b = c, rgb$1) : string) 1092 | : b instanceof color ? rgb$1 1093 | : b instanceof Date ? date 1094 | : isNumberArray(b) ? numberArray 1095 | : Array.isArray(b) ? genericArray 1096 | : typeof b.valueOf !== "function" && typeof b.toString !== "function" || isNaN(b) ? object 1097 | : interpolateNumber)(a, b); 1098 | } 1099 | 1100 | function interpolateRound(a, b) { 1101 | return a = +a, b = +b, function(t) { 1102 | return Math.round(a * (1 - t) + b * t); 1103 | }; 1104 | } 1105 | 1106 | function constant$2(x) { 1107 | return function() { 1108 | return x; 1109 | }; 1110 | } 1111 | 1112 | function number(x) { 1113 | return +x; 1114 | } 1115 | 1116 | var unit = [0, 1]; 1117 | 1118 | function identity(x) { 1119 | return x; 1120 | } 1121 | 1122 | function normalize(a, b) { 1123 | return (b -= (a = +a)) 1124 | ? function(x) { return (x - a) / b; } 1125 | : constant$2(isNaN(b) ? NaN : 0.5); 1126 | } 1127 | 1128 | function clamper(domain) { 1129 | var a = domain[0], b = domain[domain.length - 1], t; 1130 | if (a > b) t = a, a = b, b = t; 1131 | return function(x) { return Math.max(a, Math.min(b, x)); }; 1132 | } 1133 | 1134 | // normalize(a, b)(x) takes a domain value x in [a,b] and returns the corresponding parameter t in [0,1]. 1135 | // interpolate(a, b)(t) takes a parameter t in [0,1] and returns the corresponding range value x in [a,b]. 1136 | function bimap(domain, range, interpolate) { 1137 | var d0 = domain[0], d1 = domain[1], r0 = range[0], r1 = range[1]; 1138 | if (d1 < d0) d0 = normalize(d1, d0), r0 = interpolate(r1, r0); 1139 | else d0 = normalize(d0, d1), r0 = interpolate(r0, r1); 1140 | return function(x) { return r0(d0(x)); }; 1141 | } 1142 | 1143 | function polymap(domain, range, interpolate) { 1144 | var j = Math.min(domain.length, range.length) - 1, 1145 | d = new Array(j), 1146 | r = new Array(j), 1147 | i = -1; 1148 | 1149 | // Reverse descending domains. 1150 | if (domain[j] < domain[0]) { 1151 | domain = domain.slice().reverse(); 1152 | range = range.slice().reverse(); 1153 | } 1154 | 1155 | while (++i < j) { 1156 | d[i] = normalize(domain[i], domain[i + 1]); 1157 | r[i] = interpolate(range[i], range[i + 1]); 1158 | } 1159 | 1160 | return function(x) { 1161 | var i = bisectRight(domain, x, 1, j) - 1; 1162 | return r[i](d[i](x)); 1163 | }; 1164 | } 1165 | 1166 | function copy(source, target) { 1167 | return target 1168 | .domain(source.domain()) 1169 | .range(source.range()) 1170 | .interpolate(source.interpolate()) 1171 | .clamp(source.clamp()) 1172 | .unknown(source.unknown()); 1173 | } 1174 | 1175 | function transformer() { 1176 | var domain = unit, 1177 | range = unit, 1178 | interpolate = interpolateValue, 1179 | transform, 1180 | untransform, 1181 | unknown, 1182 | clamp = identity, 1183 | piecewise, 1184 | output, 1185 | input; 1186 | 1187 | function rescale() { 1188 | piecewise = Math.min(domain.length, range.length) > 2 ? polymap : bimap; 1189 | output = input = null; 1190 | return scale; 1191 | } 1192 | 1193 | function scale(x) { 1194 | return isNaN(x = +x) ? unknown : (output || (output = piecewise(domain.map(transform), range, interpolate)))(transform(clamp(x))); 1195 | } 1196 | 1197 | scale.invert = function(y) { 1198 | return clamp(untransform((input || (input = piecewise(range, domain.map(transform), interpolateNumber)))(y))); 1199 | }; 1200 | 1201 | scale.domain = function(_) { 1202 | return arguments.length ? (domain = map$1.call(_, number), clamp === identity || (clamp = clamper(domain)), rescale()) : domain.slice(); 1203 | }; 1204 | 1205 | scale.range = function(_) { 1206 | return arguments.length ? (range = slice.call(_), rescale()) : range.slice(); 1207 | }; 1208 | 1209 | scale.rangeRound = function(_) { 1210 | return range = slice.call(_), interpolate = interpolateRound, rescale(); 1211 | }; 1212 | 1213 | scale.clamp = function(_) { 1214 | return arguments.length ? (clamp = _ ? clamper(domain) : identity, scale) : clamp !== identity; 1215 | }; 1216 | 1217 | scale.interpolate = function(_) { 1218 | return arguments.length ? (interpolate = _, rescale()) : interpolate; 1219 | }; 1220 | 1221 | scale.unknown = function(_) { 1222 | return arguments.length ? (unknown = _, scale) : unknown; 1223 | }; 1224 | 1225 | return function(t, u) { 1226 | transform = t, untransform = u; 1227 | return rescale(); 1228 | }; 1229 | } 1230 | 1231 | function continuous(transform, untransform) { 1232 | return transformer()(transform, untransform); 1233 | } 1234 | 1235 | // Computes the decimal coefficient and exponent of the specified number x with 1236 | // significant digits p, where x is positive and p is in [1, 21] or undefined. 1237 | // For example, formatDecimal(1.23) returns ["123", 0]. 1238 | function formatDecimal(x, p) { 1239 | if ((i = (x = p ? x.toExponential(p - 1) : x.toExponential()).indexOf("e")) < 0) return null; // NaN, ±Infinity 1240 | var i, coefficient = x.slice(0, i); 1241 | 1242 | // The string returned by toExponential either has the form \d\.\d+e[-+]\d+ 1243 | // (e.g., 1.2e+3) or the form \de[-+]\d+ (e.g., 1e+3). 1244 | return [ 1245 | coefficient.length > 1 ? coefficient[0] + coefficient.slice(2) : coefficient, 1246 | +x.slice(i + 1) 1247 | ]; 1248 | } 1249 | 1250 | function exponent(x) { 1251 | return x = formatDecimal(Math.abs(x)), x ? x[1] : NaN; 1252 | } 1253 | 1254 | function formatGroup(grouping, thousands) { 1255 | return function(value, width) { 1256 | var i = value.length, 1257 | t = [], 1258 | j = 0, 1259 | g = grouping[0], 1260 | length = 0; 1261 | 1262 | while (i > 0 && g > 0) { 1263 | if (length + g + 1 > width) g = Math.max(1, width - length); 1264 | t.push(value.substring(i -= g, i + g)); 1265 | if ((length += g + 1) > width) break; 1266 | g = grouping[j = (j + 1) % grouping.length]; 1267 | } 1268 | 1269 | return t.reverse().join(thousands); 1270 | }; 1271 | } 1272 | 1273 | function formatNumerals(numerals) { 1274 | return function(value) { 1275 | return value.replace(/[0-9]/g, function(i) { 1276 | return numerals[+i]; 1277 | }); 1278 | }; 1279 | } 1280 | 1281 | // [[fill]align][sign][symbol][0][width][,][.precision][~][type] 1282 | var re = /^(?:(.)?([<>=^]))?([+\-( ])?([$#])?(0)?(\d+)?(,)?(\.\d+)?(~)?([a-z%])?$/i; 1283 | 1284 | function formatSpecifier(specifier) { 1285 | if (!(match = re.exec(specifier))) throw new Error("invalid format: " + specifier); 1286 | var match; 1287 | return new FormatSpecifier({ 1288 | fill: match[1], 1289 | align: match[2], 1290 | sign: match[3], 1291 | symbol: match[4], 1292 | zero: match[5], 1293 | width: match[6], 1294 | comma: match[7], 1295 | precision: match[8] && match[8].slice(1), 1296 | trim: match[9], 1297 | type: match[10] 1298 | }); 1299 | } 1300 | 1301 | formatSpecifier.prototype = FormatSpecifier.prototype; // instanceof 1302 | 1303 | function FormatSpecifier(specifier) { 1304 | this.fill = specifier.fill === undefined ? " " : specifier.fill + ""; 1305 | this.align = specifier.align === undefined ? ">" : specifier.align + ""; 1306 | this.sign = specifier.sign === undefined ? "-" : specifier.sign + ""; 1307 | this.symbol = specifier.symbol === undefined ? "" : specifier.symbol + ""; 1308 | this.zero = !!specifier.zero; 1309 | this.width = specifier.width === undefined ? undefined : +specifier.width; 1310 | this.comma = !!specifier.comma; 1311 | this.precision = specifier.precision === undefined ? undefined : +specifier.precision; 1312 | this.trim = !!specifier.trim; 1313 | this.type = specifier.type === undefined ? "" : specifier.type + ""; 1314 | } 1315 | 1316 | FormatSpecifier.prototype.toString = function() { 1317 | return this.fill 1318 | + this.align 1319 | + this.sign 1320 | + this.symbol 1321 | + (this.zero ? "0" : "") 1322 | + (this.width === undefined ? "" : Math.max(1, this.width | 0)) 1323 | + (this.comma ? "," : "") 1324 | + (this.precision === undefined ? "" : "." + Math.max(0, this.precision | 0)) 1325 | + (this.trim ? "~" : "") 1326 | + this.type; 1327 | }; 1328 | 1329 | // Trims insignificant zeros, e.g., replaces 1.2000k with 1.2k. 1330 | function formatTrim(s) { 1331 | out: for (var n = s.length, i = 1, i0 = -1, i1; i < n; ++i) { 1332 | switch (s[i]) { 1333 | case ".": i0 = i1 = i; break; 1334 | case "0": if (i0 === 0) i0 = i; i1 = i; break; 1335 | default: if (!+s[i]) break out; if (i0 > 0) i0 = 0; break; 1336 | } 1337 | } 1338 | return i0 > 0 ? s.slice(0, i0) + s.slice(i1 + 1) : s; 1339 | } 1340 | 1341 | var prefixExponent; 1342 | 1343 | function formatPrefixAuto(x, p) { 1344 | var d = formatDecimal(x, p); 1345 | if (!d) return x + ""; 1346 | var coefficient = d[0], 1347 | exponent = d[1], 1348 | i = exponent - (prefixExponent = Math.max(-8, Math.min(8, Math.floor(exponent / 3))) * 3) + 1, 1349 | n = coefficient.length; 1350 | return i === n ? coefficient 1351 | : i > n ? coefficient + new Array(i - n + 1).join("0") 1352 | : i > 0 ? coefficient.slice(0, i) + "." + coefficient.slice(i) 1353 | : "0." + new Array(1 - i).join("0") + formatDecimal(x, Math.max(0, p + i - 1))[0]; // less than 1y! 1354 | } 1355 | 1356 | function formatRounded(x, p) { 1357 | var d = formatDecimal(x, p); 1358 | if (!d) return x + ""; 1359 | var coefficient = d[0], 1360 | exponent = d[1]; 1361 | return exponent < 0 ? "0." + new Array(-exponent).join("0") + coefficient 1362 | : coefficient.length > exponent + 1 ? coefficient.slice(0, exponent + 1) + "." + coefficient.slice(exponent + 1) 1363 | : coefficient + new Array(exponent - coefficient.length + 2).join("0"); 1364 | } 1365 | 1366 | var formatTypes = { 1367 | "%": function(x, p) { return (x * 100).toFixed(p); }, 1368 | "b": function(x) { return Math.round(x).toString(2); }, 1369 | "c": function(x) { return x + ""; }, 1370 | "d": function(x) { return Math.round(x).toString(10); }, 1371 | "e": function(x, p) { return x.toExponential(p); }, 1372 | "f": function(x, p) { return x.toFixed(p); }, 1373 | "g": function(x, p) { return x.toPrecision(p); }, 1374 | "o": function(x) { return Math.round(x).toString(8); }, 1375 | "p": function(x, p) { return formatRounded(x * 100, p); }, 1376 | "r": formatRounded, 1377 | "s": formatPrefixAuto, 1378 | "X": function(x) { return Math.round(x).toString(16).toUpperCase(); }, 1379 | "x": function(x) { return Math.round(x).toString(16); } 1380 | }; 1381 | 1382 | function identity$1(x) { 1383 | return x; 1384 | } 1385 | 1386 | var map$2 = Array.prototype.map, 1387 | prefixes = ["y","z","a","f","p","n","µ","m","","k","M","G","T","P","E","Z","Y"]; 1388 | 1389 | function formatLocale(locale) { 1390 | var group = locale.grouping === undefined || locale.thousands === undefined ? identity$1 : formatGroup(map$2.call(locale.grouping, Number), locale.thousands + ""), 1391 | currencyPrefix = locale.currency === undefined ? "" : locale.currency[0] + "", 1392 | currencySuffix = locale.currency === undefined ? "" : locale.currency[1] + "", 1393 | decimal = locale.decimal === undefined ? "." : locale.decimal + "", 1394 | numerals = locale.numerals === undefined ? identity$1 : formatNumerals(map$2.call(locale.numerals, String)), 1395 | percent = locale.percent === undefined ? "%" : locale.percent + "", 1396 | minus = locale.minus === undefined ? "-" : locale.minus + "", 1397 | nan = locale.nan === undefined ? "NaN" : locale.nan + ""; 1398 | 1399 | function newFormat(specifier) { 1400 | specifier = formatSpecifier(specifier); 1401 | 1402 | var fill = specifier.fill, 1403 | align = specifier.align, 1404 | sign = specifier.sign, 1405 | symbol = specifier.symbol, 1406 | zero = specifier.zero, 1407 | width = specifier.width, 1408 | comma = specifier.comma, 1409 | precision = specifier.precision, 1410 | trim = specifier.trim, 1411 | type = specifier.type; 1412 | 1413 | // The "n" type is an alias for ",g". 1414 | if (type === "n") comma = true, type = "g"; 1415 | 1416 | // The "" type, and any invalid type, is an alias for ".12~g". 1417 | else if (!formatTypes[type]) precision === undefined && (precision = 12), trim = true, type = "g"; 1418 | 1419 | // If zero fill is specified, padding goes after sign and before digits. 1420 | if (zero || (fill === "0" && align === "=")) zero = true, fill = "0", align = "="; 1421 | 1422 | // Compute the prefix and suffix. 1423 | // For SI-prefix, the suffix is lazily computed. 1424 | var prefix = symbol === "$" ? currencyPrefix : symbol === "#" && /[boxX]/.test(type) ? "0" + type.toLowerCase() : "", 1425 | suffix = symbol === "$" ? currencySuffix : /[%p]/.test(type) ? percent : ""; 1426 | 1427 | // What format function should we use? 1428 | // Is this an integer type? 1429 | // Can this type generate exponential notation? 1430 | var formatType = formatTypes[type], 1431 | maybeSuffix = /[defgprs%]/.test(type); 1432 | 1433 | // Set the default precision if not specified, 1434 | // or clamp the specified precision to the supported range. 1435 | // For significant precision, it must be in [1, 21]. 1436 | // For fixed precision, it must be in [0, 20]. 1437 | precision = precision === undefined ? 6 1438 | : /[gprs]/.test(type) ? Math.max(1, Math.min(21, precision)) 1439 | : Math.max(0, Math.min(20, precision)); 1440 | 1441 | function format(value) { 1442 | var valuePrefix = prefix, 1443 | valueSuffix = suffix, 1444 | i, n, c; 1445 | 1446 | if (type === "c") { 1447 | valueSuffix = formatType(value) + valueSuffix; 1448 | value = ""; 1449 | } else { 1450 | value = +value; 1451 | 1452 | // Perform the initial formatting. 1453 | var valueNegative = value < 0; 1454 | value = isNaN(value) ? nan : formatType(Math.abs(value), precision); 1455 | 1456 | // Trim insignificant zeros. 1457 | if (trim) value = formatTrim(value); 1458 | 1459 | // If a negative value rounds to zero during formatting, treat as positive. 1460 | if (valueNegative && +value === 0) valueNegative = false; 1461 | 1462 | // Compute the prefix and suffix. 1463 | valuePrefix = (valueNegative ? (sign === "(" ? sign : minus) : sign === "-" || sign === "(" ? "" : sign) + valuePrefix; 1464 | 1465 | valueSuffix = (type === "s" ? prefixes[8 + prefixExponent / 3] : "") + valueSuffix + (valueNegative && sign === "(" ? ")" : ""); 1466 | 1467 | // Break the formatted value into the integer “value” part that can be 1468 | // grouped, and fractional or exponential “suffix” part that is not. 1469 | if (maybeSuffix) { 1470 | i = -1, n = value.length; 1471 | while (++i < n) { 1472 | if (c = value.charCodeAt(i), 48 > c || c > 57) { 1473 | valueSuffix = (c === 46 ? decimal + value.slice(i + 1) : value.slice(i)) + valueSuffix; 1474 | value = value.slice(0, i); 1475 | break; 1476 | } 1477 | } 1478 | } 1479 | } 1480 | 1481 | // If the fill character is not "0", grouping is applied before padding. 1482 | if (comma && !zero) value = group(value, Infinity); 1483 | 1484 | // Compute the padding. 1485 | var length = valuePrefix.length + value.length + valueSuffix.length, 1486 | padding = length < width ? new Array(width - length + 1).join(fill) : ""; 1487 | 1488 | // If the fill character is "0", grouping is applied after padding. 1489 | if (comma && zero) value = group(padding + value, padding.length ? width - valueSuffix.length : Infinity), padding = ""; 1490 | 1491 | // Reconstruct the final output based on the desired alignment. 1492 | switch (align) { 1493 | case "<": value = valuePrefix + value + valueSuffix + padding; break; 1494 | case "=": value = valuePrefix + padding + value + valueSuffix; break; 1495 | case "^": value = padding.slice(0, length = padding.length >> 1) + valuePrefix + value + valueSuffix + padding.slice(length); break; 1496 | default: value = padding + valuePrefix + value + valueSuffix; break; 1497 | } 1498 | 1499 | return numerals(value); 1500 | } 1501 | 1502 | format.toString = function() { 1503 | return specifier + ""; 1504 | }; 1505 | 1506 | return format; 1507 | } 1508 | 1509 | function formatPrefix(specifier, value) { 1510 | var f = newFormat((specifier = formatSpecifier(specifier), specifier.type = "f", specifier)), 1511 | e = Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3, 1512 | k = Math.pow(10, -e), 1513 | prefix = prefixes[8 + e / 3]; 1514 | return function(value) { 1515 | return f(k * value) + prefix; 1516 | }; 1517 | } 1518 | 1519 | return { 1520 | format: newFormat, 1521 | formatPrefix: formatPrefix 1522 | }; 1523 | } 1524 | 1525 | var locale; 1526 | var format; 1527 | var formatPrefix; 1528 | 1529 | defaultLocale({ 1530 | decimal: ".", 1531 | thousands: ",", 1532 | grouping: [3], 1533 | currency: ["$", ""], 1534 | minus: "-" 1535 | }); 1536 | 1537 | function defaultLocale(definition) { 1538 | locale = formatLocale(definition); 1539 | format = locale.format; 1540 | formatPrefix = locale.formatPrefix; 1541 | return locale; 1542 | } 1543 | 1544 | function precisionFixed(step) { 1545 | return Math.max(0, -exponent(Math.abs(step))); 1546 | } 1547 | 1548 | function precisionPrefix(step, value) { 1549 | return Math.max(0, Math.max(-8, Math.min(8, Math.floor(exponent(value) / 3))) * 3 - exponent(Math.abs(step))); 1550 | } 1551 | 1552 | function precisionRound(step, max) { 1553 | step = Math.abs(step), max = Math.abs(max) - step; 1554 | return Math.max(0, exponent(max) - exponent(step)) + 1; 1555 | } 1556 | 1557 | function tickFormat(start, stop, count, specifier) { 1558 | var step = tickStep(start, stop, count), 1559 | precision; 1560 | specifier = formatSpecifier(specifier == null ? ",f" : specifier); 1561 | switch (specifier.type) { 1562 | case "s": { 1563 | var value = Math.max(Math.abs(start), Math.abs(stop)); 1564 | if (specifier.precision == null && !isNaN(precision = precisionPrefix(step, value))) specifier.precision = precision; 1565 | return formatPrefix(specifier, value); 1566 | } 1567 | case "": 1568 | case "e": 1569 | case "g": 1570 | case "p": 1571 | case "r": { 1572 | if (specifier.precision == null && !isNaN(precision = precisionRound(step, Math.max(Math.abs(start), Math.abs(stop))))) specifier.precision = precision - (specifier.type === "e"); 1573 | break; 1574 | } 1575 | case "f": 1576 | case "%": { 1577 | if (specifier.precision == null && !isNaN(precision = precisionFixed(step))) specifier.precision = precision - (specifier.type === "%") * 2; 1578 | break; 1579 | } 1580 | } 1581 | return format(specifier); 1582 | } 1583 | 1584 | function linearish(scale) { 1585 | var domain = scale.domain; 1586 | 1587 | scale.ticks = function(count) { 1588 | var d = domain(); 1589 | return ticks(d[0], d[d.length - 1], count == null ? 10 : count); 1590 | }; 1591 | 1592 | scale.tickFormat = function(count, specifier) { 1593 | var d = domain(); 1594 | return tickFormat(d[0], d[d.length - 1], count == null ? 10 : count, specifier); 1595 | }; 1596 | 1597 | scale.nice = function(count) { 1598 | if (count == null) count = 10; 1599 | 1600 | var d = domain(), 1601 | i0 = 0, 1602 | i1 = d.length - 1, 1603 | start = d[i0], 1604 | stop = d[i1], 1605 | step; 1606 | 1607 | if (stop < start) { 1608 | step = start, start = stop, stop = step; 1609 | step = i0, i0 = i1, i1 = step; 1610 | } 1611 | 1612 | step = tickIncrement(start, stop, count); 1613 | 1614 | if (step > 0) { 1615 | start = Math.floor(start / step) * step; 1616 | stop = Math.ceil(stop / step) * step; 1617 | step = tickIncrement(start, stop, count); 1618 | } else if (step < 0) { 1619 | start = Math.ceil(start * step) / step; 1620 | stop = Math.floor(stop * step) / step; 1621 | step = tickIncrement(start, stop, count); 1622 | } 1623 | 1624 | if (step > 0) { 1625 | d[i0] = Math.floor(start / step) * step; 1626 | d[i1] = Math.ceil(stop / step) * step; 1627 | domain(d); 1628 | } else if (step < 0) { 1629 | d[i0] = Math.ceil(start * step) / step; 1630 | d[i1] = Math.floor(stop * step) / step; 1631 | domain(d); 1632 | } 1633 | 1634 | return scale; 1635 | }; 1636 | 1637 | return scale; 1638 | } 1639 | 1640 | function linear$1() { 1641 | var scale = continuous(identity, identity); 1642 | 1643 | scale.copy = function() { 1644 | return copy(scale, linear$1()); 1645 | }; 1646 | 1647 | initRange.apply(scale, arguments); 1648 | 1649 | return linearish(scale); 1650 | } 1651 | 1652 | /** 1653 | * Meanderer class. Accepts a path, container, height, width, and change handler. 1654 | * Although it doesn't need a handler. We can just call get path and let it do that. 1655 | * The checks can be handled outside. We don't need to do it inside. 1656 | */ 1657 | 1658 | var Meanderer = function Meanderer(_ref) { 1659 | var height = _ref.height, 1660 | path = _ref.path, 1661 | _ref$threshold = _ref.threshold, 1662 | threshold = _ref$threshold === void 0 ? 0.2 : _ref$threshold, 1663 | width = _ref.width; 1664 | 1665 | _classCallCheck(this, Meanderer); 1666 | 1667 | _initialiseProps.call(this); 1668 | 1669 | this.height = height; 1670 | this.path = path; 1671 | this.threshold = threshold; 1672 | this.width = width; // With what we are given create internal references 1673 | 1674 | this.aspect_ratio = width / height; // Convert the path into a data set 1675 | 1676 | this.path_data = this.convertPathToData(path); 1677 | this.maximums = this.getMaximums(this.path_data); 1678 | this.range_ratios = this.getRatios(this.maximums, width, height); 1679 | } // This is relevant for when we want to interpolate points to 1680 | // the container scale. We need the minimum and maximum for both X and Y 1681 | ; 1682 | 1683 | var _initialiseProps = function _initialiseProps() { 1684 | var _this = this; 1685 | 1686 | this.getMaximums = function (data) { 1687 | var X_POINTS = data.map(function (point) { 1688 | return point[0]; 1689 | }); 1690 | var Y_POINTS = data.map(function (point) { 1691 | return point[1]; 1692 | }); 1693 | return [Math.max.apply(Math, _toConsumableArray(X_POINTS)), // x2 1694 | Math.max.apply(Math, _toConsumableArray(Y_POINTS)) // y2 1695 | ]; 1696 | }; 1697 | 1698 | this.getRatios = function (maxs, width, height) { 1699 | return [maxs[0] / width, maxs[1] / height]; 1700 | }; 1701 | 1702 | this.convertPathToData = function (path) { 1703 | // To convert the path data to points, we need an SVG path element. 1704 | var svgContainer = document.createElement("div"); // To create one though, a quick way is to use innerHTML 1705 | 1706 | svgContainer.innerHTML = "\n \n "); 1707 | var pathElement = svgContainer.querySelector("path"); // Now to gather up the path points using the SVGGeometryElement API 👍 1708 | 1709 | var DATA = []; // Iterate over the total length of the path pushing the x and y into 1710 | // a data set for d3 to handle 👍 1711 | 1712 | for (var p = 0; p < pathElement.getTotalLength(); p++) { 1713 | var _pathElement$getPoint = pathElement.getPointAtLength(p), 1714 | x = _pathElement$getPoint.x, 1715 | y = _pathElement$getPoint.y; 1716 | 1717 | DATA.push([x, y]); 1718 | } 1719 | 1720 | return DATA; 1721 | }; 1722 | 1723 | this.generatePath = function (containerWidth, containerHeight) { 1724 | var height = _this.height, 1725 | width = _this.width, 1726 | aspectRatio = _this.aspect_ratio, 1727 | data = _this.path_data, 1728 | _this$maximums = _slicedToArray(_this.maximums, 2), 1729 | maxWidth = _this$maximums[0], 1730 | maxHeight = _this$maximums[1], 1731 | _this$range_ratios = _slicedToArray(_this.range_ratios, 2), 1732 | widthRatio = _this$range_ratios[0], 1733 | heightRatio = _this$range_ratios[1], 1734 | threshold = _this.threshold; 1735 | 1736 | var OFFSETS = [0, 0]; // Get the aspect ratio defined by the container 1737 | 1738 | var newAspectRatio = containerWidth / containerHeight; // We only need to start applying offsets if the aspect ratio of the container is off 👍 1739 | // In here we need to work out which side needs the offset. It's whichever one is smallest in order to centralize. 1740 | // What if the container matches the aspect ratio... 1741 | 1742 | if (Math.abs(newAspectRatio - aspectRatio) > threshold) { 1743 | // We know the tolerance is off so we need to work out a ratio 1744 | // This works flawlessly. Now we need to check for when the height is less than the width 1745 | if (width < height) { 1746 | var ratio = (height - width) / height; 1747 | OFFSETS[0] = ratio * containerWidth / 2; 1748 | } else { 1749 | var _ratio = (width - height) / width; 1750 | 1751 | OFFSETS[1] = _ratio * containerHeight / 2; 1752 | } 1753 | } // Create two d3 scales for X and Y 1754 | 1755 | 1756 | var xScale = linear$1().domain([0, maxWidth]).range([OFFSETS[0], containerWidth * widthRatio - OFFSETS[0]]); 1757 | var yScale = linear$1().domain([0, maxHeight]).range([OFFSETS[1], containerHeight * heightRatio - OFFSETS[1]]); // Map our data points using the scales 1758 | 1759 | var SCALED_POINTS = data.map(function (POINT) { 1760 | return [xScale(POINT[0]), yScale(POINT[1])]; 1761 | }); 1762 | return line()(SCALED_POINTS); 1763 | }; 1764 | }; 1765 | 1766 | return Meanderer; 1767 | 1768 | }))); 1769 | --------------------------------------------------------------------------------