├── .babelrc ├── .gitignore ├── .npmignore ├── LICENSE.md ├── README.md ├── docs ├── App.js ├── AreaDemo.js ├── BarDemo.js ├── BarsLine.js ├── Controls.js ├── Debug.js ├── Dev.js ├── DoubleBars.js ├── Footer.js ├── Header.js ├── bundle.js ├── data.js ├── entry.js ├── index.html ├── styles.js └── util.js ├── package.json ├── src ├── Area.js ├── Axis.js ├── Bar.js ├── Chart.js ├── DataLabels.js ├── Div.js ├── Dot.js ├── Dots.js ├── Label.js ├── Line.js ├── Rule.js ├── Rules.js ├── Svg.js ├── get-scale.js ├── index.js └── withScale.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "stage-0", 5 | "react" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | dist 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | docs 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | # The MIT License (MIT) 3 | 4 | Copyright (c) 2016 Brent Jackson 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # f0 3 | 4 | **WIP** 5 | 6 | Minimal, composable, fully-fluid SVG charts for React 7 | 8 | ## Getting Started 9 | 10 | ```sh 11 | npm i -S f0 12 | ``` 13 | 14 | ```js 15 | import React from 'react' 16 | import { Line, Bars } from 'f0' 17 | 18 | const App = () => { 19 | const a = [ 20 | 4, 8, 16, 32, 64 21 | ] 22 | 23 | const b = [ 24 | 8, 2, 32, 16, 4 25 | ] 26 | 27 | return ( 28 |
29 | 30 | 31 |
32 | ) 33 | } 34 | 35 | export default App 36 | ``` 37 | 38 | ## Features 39 | 40 | - Fully fluid charts with fixed heights 41 | - Uses HTML for labels to prevent scaling issues 42 | - Bare bones API - only accepts flat arrays as data 43 | - Low-level access to components for composition 44 | 45 | ## Components 46 | 47 | ### Line 48 | 49 | ```js 50 | 53 | ``` 54 | 55 | ```js 56 | 62 | ``` 63 | 64 | ### Area 65 | 66 | ```js 67 | 70 | ``` 71 | 72 | ```js 73 | 78 | ``` 79 | 80 | ### Bar 81 | 82 | ```js 83 | 86 | ``` 87 | 88 | ```js 89 | 93 | ``` 94 | 95 | ### Svg 96 | 97 | ```js 98 | 99 | 100 | 101 | 102 | ``` 103 | 104 | ### Chart 105 | 106 | ```js 107 | 108 | 109 | 118 | 119 | ``` 120 | 121 | ### Rules 122 | 123 | ```js 124 | 125 | 126 | 127 | 128 | ``` 129 | 130 | ### Axis 131 | 132 | ```js 133 | 134 | 135 | 144 | 152 | 153 | ``` 154 | 155 | ### DataLabels 156 | 157 | ```js 158 | 159 | 160 | 161 | 162 | ``` 163 | 164 | ## Browser Support 165 | 166 | **Currently does not work in IE** 167 | 168 | The fluid style for these charts relies on SVG 1.2 vector-effect non-scaling-stroke. 169 | Modern evergreen browsers should support this feature, but charts may appear distorted in older browsers, including IE and Edge. 170 | 171 | [MIT License](/LICENSE.md) 172 | 173 | -------------------------------------------------------------------------------- /docs/App.js: -------------------------------------------------------------------------------- 1 | 2 | // Dematerialization margin 3 | 4 | import React from 'react' 5 | import chroma from 'chroma-js' 6 | import Header from './Header' 7 | import BarDemo from './BarDemo' 8 | import DoubleBars from './DoubleBars' 9 | import BarsLine from './BarsLine' 10 | import AreaDemo from './AreaDemo' 11 | import Footer from './Footer' 12 | 13 | import Dev from './Dev' 14 | import Controls from './Controls' 15 | 16 | const blue = '#0077cc' 17 | const orange = '#ff5500' 18 | const colors = { 19 | blue, 20 | orange 21 | } 22 | 23 | const rand = () => Math.round(16 * Math.random()) 24 | 25 | const getData = (length) => ( 26 | Array.from({ length }).map(rand) 27 | ) 28 | 29 | const getColors = () => { 30 | const [ h, s ] = chroma.random().hsl() 31 | const b0 = chroma.hsl([ h, s, 1 / 8 ]).hex() 32 | const b1 = chroma.hsl([ h, s, 1 / 4 ]).hex() 33 | const b2 = chroma.hsl([ h, s, 1 / 2 ]).hex() 34 | const b3 = chroma.hsl([ h, s, 3 / 4 ]).hex() 35 | const b4 = chroma.hsl([ h, s, 7 / 8 ]).hex() 36 | 37 | return [ 38 | b0, b1, b2, b3, b4 39 | ] 40 | } 41 | 42 | class App extends React.Component { 43 | state = { 44 | count: 0, 45 | colors: getColors(), 46 | logo: getData(8), 47 | data: getData(8), 48 | neg: getData(8).map(n => -1 * n), 49 | xdata: [ 50 | 3, 8, 4, 16, 51 | 48, 32, 24, 24 52 | ], 53 | xneg: [ 54 | -48, -32, -24, -24, 55 | -3, -8, -4, -16 56 | ] 57 | } 58 | 59 | randomize = () => { 60 | const logo = this.state.logo.map(rand) 61 | const data = this.state.data.map(rand) 62 | const neg = this.state.data.map(rand).map(n => -1 * n) 63 | const colors = getColors() 64 | this.setState({ 65 | logo, 66 | data, 67 | neg, 68 | colors 69 | }) 70 | } 71 | 72 | onChange = obj => { 73 | this.setState(obj) 74 | } 75 | 76 | componentDidMount () { 77 | setInterval(this.randomize, 2000) 78 | } 79 | 80 | render () { 81 | const { data } = this.state 82 | const sx = { 83 | root: { 84 | fontFamily: '-apple-system, BlinkMacSystemFont, sans-serif', 85 | } 86 | } 87 | 88 | return ( 89 |
90 |
91 | 92 | 93 | 94 | 95 |
96 | {/* 97 | 98 | 99 | */} 100 |
101 | ) 102 | } 103 | } 104 | 105 | export default App 106 | 107 | -------------------------------------------------------------------------------- /docs/AreaDemo.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { VictoryAnimation } from 'victory' 4 | import { 5 | Chart, 6 | Svg, 7 | Area 8 | } from '../src' 9 | import { fl } from './util' 10 | 11 | const AreaDemo = ({ 12 | data, 13 | logo, 14 | colors 15 | }) => { 16 | const sx = { 17 | root: { 18 | color: colors[4], 19 | backgroundColor: colors[1], 20 | transitionProperty: 'color, background-color, fill', 21 | transitionDuration: '.2s, .8s', 22 | transitionTimingFunction: 'ease-out' 23 | }, 24 | title: { 25 | display: 'flex', 26 | flexWrap: 'wrap', 27 | alignItems: 'flex-end' 28 | }, 29 | space: { 30 | flex: '1 1 auto' 31 | } 32 | } 33 | 34 | return ( 35 |
38 | 41 | {({ data, logo }) => ( 42 |
43 |

44 |
Area
45 |
46 |
47 |
Pattern buffer
48 |
{fl(data[data.length - 1])}
49 |
50 |

51 | 52 | 53 | 60 | 67 | 68 | 69 |
70 | )} 71 |
72 |
73 | ) 74 | } 75 | 76 | export default AreaDemo 77 | 78 | -------------------------------------------------------------------------------- /docs/BarDemo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { VictoryAnimation } from 'victory' 3 | import { 4 | Chart, 5 | Svg, 6 | Bar, 7 | Rules, 8 | Axis 9 | } from '../src' 10 | import { fl } from './util' 11 | 12 | const BarDemo = ({ 13 | data, 14 | colors 15 | }) => { 16 | const sx = { 17 | root: { 18 | color: colors[4], 19 | backgroundColor: colors[1], 20 | transitionProperty: 'color, background-color, fill', 21 | transitionDuration: '.2s, .8s', 22 | transitionTimingFunction: 'ease-out' 23 | }, 24 | title: { 25 | display: 'flex', 26 | flexWrap: 'wrap', 27 | alignItems: 'flex-end' 28 | }, 29 | space: { 30 | flex: '1 1 auto' 31 | } 32 | } 33 | 34 | return ( 35 |
38 | 41 | {({ data }) => ( 42 |
43 |

44 |
Bar
45 |
46 |
47 |
Telemetry
48 |
{fl(data[data.length - 1])}
49 |
50 |

51 | 52 | 53 | 54 | 59 | 60 | 71 | 72 |
73 | )} 74 |
75 |
76 | ) 77 | } 78 | 79 | export default BarDemo 80 | 81 | -------------------------------------------------------------------------------- /docs/BarsLine.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { VictoryAnimation } from 'victory' 4 | import { 5 | Svg, 6 | Bar, 7 | Line, 8 | Rules 9 | } from '../src' 10 | import { fl } from './util' 11 | 12 | const DoubleBars = ({ 13 | data, 14 | neg, 15 | colors 16 | }) => { 17 | const sx = { 18 | root: { 19 | color: colors[4], 20 | backgroundColor: colors[0], 21 | transitionProperty: 'color, background-color, fill, stroke', 22 | transitionDuration: '.2s, .8s', 23 | transitionTimingFunction: 'ease-out' 24 | }, 25 | title: { 26 | display: 'flex', 27 | flexWrap: 'wrap', 28 | alignItems: 'flex-end' 29 | }, 30 | space: { 31 | flex: '1 1 auto' 32 | } 33 | } 34 | 35 | return ( 36 |
39 | 42 | {({ data, neg }) => { 43 | const line = data.map((d, i) => d + neg[i]) 44 | 45 | return ( 46 |
47 |

48 |
Bar + Line
49 |
50 |
51 |
Tachyonic inversion
52 |
{fl(line[line.length - 1])}
53 |
54 |

55 | 56 | 63 | 72 | 82 | 83 |
84 | ) 85 | }} 86 |
87 |
88 | ) 89 | } 90 | 91 | export default DoubleBars 92 | 93 | -------------------------------------------------------------------------------- /docs/Controls.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { 4 | Slider 5 | } from 'rebass' 6 | 7 | const Controls = props => { 8 | const { 9 | data, 10 | neg, 11 | logo, 12 | onChange 13 | } = props 14 | 15 | const handleNumberChange = i => e => { 16 | const { name, value } = e.target 17 | const arr = props[name] 18 | const num = parseFloat(value) 19 | arr[i] = num 20 | onChange({ [name]: arr }) 21 | } 22 | 23 | return ( 24 |
25 | {neg.map((d, i) => ( 26 | 35 | ))} 36 |
37 | {data.map((d, i) => ( 38 | 46 | ))} 47 |
48 | ) 49 | } 50 | 51 | export default Controls 52 | 53 | -------------------------------------------------------------------------------- /docs/Debug.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const Debug = props => ( 5 |
{JSON.stringify(props, null, 2)}
6 | ) 7 | 8 | export default Debug 9 | 10 | -------------------------------------------------------------------------------- /docs/Dev.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { 4 | VictoryAnimation, 5 | VictoryAxis 6 | } from 'victory' 7 | import { 8 | Chart, 9 | Svg, 10 | Line, 11 | Area, 12 | Bar, 13 | Rules, 14 | 15 | Axis, 16 | DataLabels, 17 | 18 | XAxis, 19 | Label, 20 | 21 | Labels 22 | } from '../src' 23 | import { fl } from './util' 24 | 25 | const Dev = ({ 26 | data, 27 | neg, 28 | logo, 29 | colors 30 | }) => { 31 | const sx = { 32 | root: { 33 | padding: 48 34 | }, 35 | bars: { 36 | marginBottom: 32, 37 | transitionProperty: 'fill, stroke', 38 | transitionDuration: '.4s', 39 | transitionTimingFunction: 'ease-out' 40 | } 41 | } 42 | 43 | return ( 44 |
45 |
Dev
46 | 53 | {({ data, logo }) => ( 54 |
55 | 56 | 57 | 64 | 73 | 74 | 75 | 76 | 79 | 87 | 88 | 89 | 96 | 97 | 103 | 104 | 115 | 122 |
123 | )} 124 |
125 |
{JSON.stringify(data, null, 2)}
126 |
{JSON.stringify(neg, null, 2)}
127 |
{JSON.stringify(logo, null, 2)}
128 |
{JSON.stringify(colors, null, 2)}
129 |
130 | ) 131 | } 132 | 133 | export default Dev 134 | -------------------------------------------------------------------------------- /docs/DoubleBars.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { VictoryAnimation } from 'victory' 4 | import { 5 | Svg, 6 | Bar, 7 | Rules 8 | } from '../src' 9 | import { fl } from './util' 10 | 11 | const DoubleBars = ({ 12 | data, 13 | neg, 14 | colors 15 | }) => { 16 | const sx = { 17 | root: { 18 | color: '#fff', 19 | backgroundColor: colors[3], 20 | transitionProperty: 'color, background-color, fill', 21 | transitionDuration: '.2s, .8s', 22 | transitionTimingFunction: 'ease-out' 23 | }, 24 | title: { 25 | display: 'flex', 26 | flexWrap: 'wrap', 27 | alignItems: 'flex-end' 28 | }, 29 | space: { 30 | flex: '1 1 auto' 31 | } 32 | } 33 | return ( 34 |
37 | 40 | {({ data, neg }) => ( 41 |
42 |

43 |
Bar
44 |
45 |
46 |
Time dilation
47 |
{fl(data[data.length - 1])}
48 |
49 |

50 | 51 | 52 | 59 | 68 | 69 |
70 | )} 71 |
72 |
73 | ) 74 | } 75 | 76 | export default DoubleBars 77 | 78 | -------------------------------------------------------------------------------- /docs/Footer.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const Footer = ({ 5 | colors 6 | }) => { 7 | const sx = { 8 | root: { 9 | color: colors[4], 10 | backgroundColor: colors[0], 11 | } 12 | } 13 | 14 | return ( 15 | 21 | ) 22 | } 23 | 24 | export default Footer 25 | 26 | -------------------------------------------------------------------------------- /docs/Header.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import { VictoryAnimation } from 'victory' 4 | import { 5 | Line, 6 | Svg, 7 | Rules 8 | } from '../src' 9 | import { format } from 'd3-format' 10 | 11 | const fl = format('.2f') 12 | 13 | const Header = ({ logo, colors }) => { 14 | const sx = { 15 | root: { 16 | color: colors[4], 17 | backgroundColor: colors[2], 18 | transitionProperty: 'color, background-color, fill, stroke', 19 | transitionDuration: '.2s, .8s', 20 | transitionTimingFunction: 'ease-out' 21 | }, 22 | title: { 23 | display: 'flex', 24 | flexWrap: 'wrap', 25 | alignItems: 'flex-end', 26 | color: colors[1] // styles.b[1] 27 | }, 28 | space: { 29 | flex: '1 1 auto' 30 | } 31 | } 32 | 33 | return ( 34 |
37 | 40 | {({ data }) => ( 41 |
42 |

43 |
f0
44 |
45 |
46 |
Subspace frequency
47 |
{fl(data[data.length - 1])}
48 |
49 |

50 | 51 | 52 | 62 | 63 |
64 | )} 65 |
66 |

67 | Minimal, composable, fully-fluid SVG charts for React 68 |

69 |

*WIP*

70 | {/* 71 | 72 | 73 | */} 74 |
75 | ) 76 | } 77 | 78 | export default Header 79 | 80 | -------------------------------------------------------------------------------- /docs/data.js: -------------------------------------------------------------------------------- 1 | 2 | import pkg from '../package.json' 3 | 4 | const data = { 5 | ...pkg 6 | } 7 | 8 | export default data 9 | 10 | -------------------------------------------------------------------------------- /docs/entry.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import App from './App' 5 | 6 | ReactDOM.render(, app) 7 | 8 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | f0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | 44 |
45 | 46 | -------------------------------------------------------------------------------- /docs/styles.js: -------------------------------------------------------------------------------- 1 | 2 | const colors = { 3 | cyan: '#02ffd5', 4 | darkcyan: '#104a40', 5 | black: '#041210', 6 | } 7 | 8 | const b = [ 9 | '#001221', 10 | '#003561', 11 | '#0059a0', 12 | '#007ce0', 13 | '#219cff', 14 | '#61b8ff', 15 | '#a0d5ff', 16 | ] 17 | 18 | const styles = { 19 | colors, 20 | b, 21 | c: [ 22 | '#041210', 23 | '#104a40', 24 | '#01bf9f', 25 | '#02ffd5', 26 | ] 27 | } 28 | 29 | export default styles 30 | 31 | -------------------------------------------------------------------------------- /docs/util.js: -------------------------------------------------------------------------------- 1 | 2 | import { format } from 'd3-format' 3 | 4 | export const fl = format('.2f') 5 | 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "f0", 3 | "version": "1.0.0-b4", 4 | "description": "Minimal, composable, fully-fluid SVG charts for React", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "prepublish": "mkdir -p dist && babel src --out-dir dist", 8 | "start": "webpack-dev-server", 9 | "build": "webpack -p" 10 | }, 11 | "keywords": [ 12 | "charts", 13 | "react", 14 | "svg", 15 | "responsive", 16 | "fluid" 17 | ], 18 | "author": "Brent Jackson", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "babel-cli": "^6.16.0", 22 | "babel-core": "^6.17.0", 23 | "babel-loader": "^6.2.5", 24 | "babel-preset-es2015": "^6.16.0", 25 | "babel-preset-react": "^6.16.0", 26 | "babel-preset-stage-0": "^6.16.0", 27 | "chroma-js": "^1.2.1", 28 | "d3-format": "^1.0.2", 29 | "json-loader": "^0.5.4", 30 | "open-color": "^1.4.0", 31 | "react": "^15.3.2", 32 | "react-dom": "^15.3.2", 33 | "react-motion": "^0.4.5", 34 | "react-tween-state": "^0.1.5", 35 | "rebass": "^0.4.0-beta.8", 36 | "victory": "^0.13.0", 37 | "webpack": "^1.13.2", 38 | "webpack-dev-server": "^1.16.2" 39 | }, 40 | "dependencies": { 41 | "d3-array": "^1.0.1", 42 | "d3-scale": "^1.0.3" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Area.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import withScale from './withScale' 4 | import Svg from './Svg' 5 | 6 | const Area = ({ 7 | points = [], 8 | color = 'currentcolor', 9 | opacity = 1, 10 | padWidth, 11 | ...props 12 | }) => { 13 | if (!points.length) return null 14 | 15 | const command = i => i === 0 ? 'M' : 'L' 16 | 17 | const d = [ 18 | ...points.map(({ x, y }, i) => ( 19 | `${command(i)} ${x} ${y}` 20 | )), 21 | `V100 H0 z` 22 | ] 23 | 24 | return ( 25 | 26 | 31 | 32 | ) 33 | } 34 | 35 | export default withScale(Area) 36 | 37 | -------------------------------------------------------------------------------- /src/Axis.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import getScale from './get-scale' 4 | import Div from './Div' 5 | import Label from './Label' 6 | 7 | // Split up or handle unpadded labels 8 | 9 | const XAxis = ({ 10 | labels = [], 11 | rule, 12 | scale, 13 | padWidth, 14 | style 15 | }) => { 16 | const sx = { 17 | root: { 18 | display: 'flex', 19 | justifyContent: 'space-between', 20 | alignItems: 'flex-start', 21 | position: 'relative', 22 | minHeight: 32, 23 | marginTop: rule ? -1 : null, 24 | borderTop: rule ? '1px solid' : null, 25 | ...style 26 | }, 27 | label: { 28 | textAlign: 'center', 29 | flexBasis: padWidth + '%', 30 | paddingTop: 8, 31 | paddingBottom: 8, 32 | } 33 | } 34 | 35 | return ( 36 |
37 | {labels.map((label, i) => { 38 | const x = scale.padx(i) 39 | return ( 40 |
48 | ) 49 | } 50 | 51 | const YAxis = ({ 52 | labels = [], 53 | rule, 54 | ...style 55 | }) => { 56 | const sx = { 57 | root: { 58 | position: 'absolute', 59 | top: 0, 60 | left: 0, 61 | bottom: 0, 62 | borderLeft: rule ? '1px solid' : null, 63 | ...style 64 | }, 65 | label: { 66 | paddingLeft: rule ? 8 : null 67 | } 68 | } 69 | return ( 70 |
71 | {labels.map((label, i) => ( 72 |
75 |
79 | ))} 80 |
81 | ) 82 | } 83 | 84 | const Axis = ({ 85 | labels = [], 86 | length, 87 | x, 88 | y, 89 | ...props 90 | }) => { 91 | const { scale, padWidth } = getScale({ length: length || labels.length }) 92 | 93 | if (y) { 94 | return ( 95 | 99 | ) 100 | } 101 | 102 | return ( 103 | 109 | ) 110 | } 111 | 112 | export default Axis 113 | 114 | -------------------------------------------------------------------------------- /src/Bar.js: -------------------------------------------------------------------------------- 1 | 2 | // Rename to Bar 3 | 4 | import React from 'react' 5 | import withScale from './withScale' 6 | import Svg from './Svg' 7 | 8 | const Bar = ({ 9 | scale, 10 | padWidth, 11 | points = [], 12 | color = 'currentcolor', 13 | ...props 14 | }) => { 15 | if (!points.length) return null 16 | 17 | const bars = points.map(({ padx, y, d }, i) => { 18 | const bx = padx - padWidth / 2 19 | const hi = d === 0 20 | ? 0 21 | : d > 0 ? 100 - y : y 22 | const by = d >= 0 ? y : 0 23 | 24 | return ( 25 | 32 | ) 33 | }) 34 | 35 | return ( 36 | 41 | ) 42 | } 43 | 44 | export default withScale(Bar) 45 | 46 | -------------------------------------------------------------------------------- /src/Chart.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const Chart = ({ 5 | width = '100%', 6 | height = 256, 7 | style, 8 | ...props 9 | }) => { 10 | const sx = { 11 | position: 'relative', 12 | width, 13 | height, 14 | // minHeight: height, 15 | ...style 16 | } 17 | 18 | return ( 19 |
22 | ) 23 | } 24 | 25 | export default Chart 26 | 27 | -------------------------------------------------------------------------------- /src/DataLabels.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import withScale from './withScale' 4 | import Div from './Div' 5 | import Label from './Label' 6 | 7 | const DataLabels = ({ 8 | scale, 9 | padWidth, 10 | pad, 11 | points = [], 12 | min, 13 | max, 14 | format = n => n, 15 | ...props 16 | }) => { 17 | const sx = { 18 | root: {}, 19 | div: { 20 | width: padWidth + '%' 21 | }, 22 | label: { 23 | width: '100%', 24 | textAlign: 'center', 25 | } 26 | } 27 | 28 | const labels = points.map(({ x, padx, y, d }, i) => { 29 | const lx = padx - padWidth / 2 30 | const ly = y 31 | return ( 32 |
35 |
40 | ) 41 | }) 42 | 43 | return ( 44 |
45 | {labels} 46 |
47 | ) 48 | } 49 | 50 | export default withScale(DataLabels) 51 | 52 | -------------------------------------------------------------------------------- /src/Div.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const Div = ({ 5 | x = 0, 6 | y = 0, 7 | top, 8 | style, 9 | center, 10 | ...props 11 | }) => { 12 | const transform = center && top 13 | ? 'translate(-50%, -100%)' 14 | : center 15 | ? 'translate(-50%, 0)' 16 | : top 17 | ? 'translate(0, -100%)' 18 | : null 19 | 20 | const sx = { 21 | position: 'absolute', 22 | top: y + '%', 23 | left: x + '%', 24 | transform, 25 | ...style 26 | } 27 | 28 | return ( 29 |
33 | ) 34 | } 35 | 36 | export default Div 37 | 38 | -------------------------------------------------------------------------------- /src/Dot.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const Dot = ({ 5 | x = 0, 6 | y = 0, 7 | size = 12, 8 | color = 'currentcolor', 9 | fill, 10 | strokeWidth, 11 | style 12 | }) => { 13 | return ( 14 | 15 | 26 | {fill && ( 27 | 38 | )} 39 | 40 | ) 41 | } 42 | 43 | export default Dot 44 | 45 | -------------------------------------------------------------------------------- /src/Dots.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import Dot from './Dot' 4 | 5 | const Dots = ({ 6 | points = [], 7 | color, 8 | size, 9 | fill, 10 | strokeWidth, 11 | style 12 | }) => { 13 | return ( 14 | 15 | {points.map(({ x, y }, i) => ( 16 | 26 | ))} 27 | 28 | ) 29 | } 30 | 31 | export default Dots 32 | 33 | -------------------------------------------------------------------------------- /src/Label.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const Label = ({ 5 | style, 6 | ...props 7 | }) => { 8 | const sx = { 9 | fontSize: 12, 10 | minHeight: 16, 11 | ...style 12 | } 13 | 14 | return ( 15 |
16 | ) 17 | } 18 | 19 | export default Label 20 | 21 | -------------------------------------------------------------------------------- /src/Line.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import withScale from './withScale' 4 | import Svg from './Svg' 5 | import Dots from './Dots' 6 | 7 | const isNum = n => !isNaN(n) 8 | 9 | const Line = ({ 10 | scale, 11 | points = [], 12 | min, 13 | max, 14 | pad, 15 | padWidth, 16 | 17 | color = 'currentcolor', 18 | strokeWidth = 3, 19 | strokeLinecap = 'round', 20 | 21 | // Should this be an object? 22 | dots, 23 | dotSize, 24 | dotColor, 25 | dotFill, 26 | style = {}, 27 | children, 28 | ...rest 29 | }) => { 30 | if (!points.length) return null 31 | 32 | const command = i => i === 0 ? 'M' : 'L' 33 | 34 | // Remove after removing Dots 35 | const p = points.map(p => pad ? ({ ...p, x: p.padx }) : p) 36 | 37 | const d = points.map(({ x, padx, y }, i) => { 38 | const lx = pad ? padx : x 39 | return `${command(i)} ${lx} ${y}` 40 | }) 41 | 42 | return ( 43 | 44 | 52 | {dots && ( 53 | 60 | )} 61 | 62 | ) 63 | } 64 | 65 | export default withScale(Line) 66 | 67 | -------------------------------------------------------------------------------- /src/Rule.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const isNum = n => n !== null && !isNaN(n) 5 | 6 | const Rule = ({ 7 | x, 8 | y, 9 | strokeWidth = 1, 10 | color = 'currentcolor', 11 | opacity = 1 / 4 12 | }) => { 13 | if (!isNum(x) && !isNum(y)) return null 14 | 15 | const d = isNum(x) 16 | ? `M${x} 0 V100` 17 | : `M0 ${y} H100` 18 | 19 | return ( 20 | 28 | ) 29 | } 30 | 31 | export default Rule 32 | 33 | -------------------------------------------------------------------------------- /src/Rules.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import Svg from './Svg' 4 | import Rule from './Rule' 5 | import getScale from './get-scale' 6 | 7 | const Rules = ({ 8 | x = 0, 9 | y, 10 | pad, 11 | strokeWidth = 1, 12 | color, 13 | opacity, 14 | ...props 15 | }) => { 16 | if (x < 1 && y < 1) return null 17 | 18 | const viewBox = [ 0, 0, 100, 100 ].join(' ') 19 | 20 | const { scale } = getScale({ length: x }) 21 | const scalex = pad ? scale.padx : scale.x 22 | 23 | const xrules = Array.from({ length: x }) 24 | .map((n, i) => i) 25 | .map(n => ( 26 | 33 | )) 34 | 35 | const yrules = Array.from({ length: y }) 36 | .map((n, i) => i) 37 | .map(n => { 38 | const step = n / (y - 1) * 100 39 | return ( 40 | 47 | ) 48 | }) 49 | 50 | return ( 51 | 52 | {xrules} 53 | {yrules} 54 | 55 | ) 56 | } 57 | 58 | export default Rules 59 | 60 | -------------------------------------------------------------------------------- /src/Svg.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | 4 | const Svg = ({ 5 | viewBox = '0 0 100 100', 6 | width = '100%', 7 | height = 256, 8 | style, 9 | ...props 10 | }) => { 11 | const sx = { 12 | position: 'relative', 13 | display: 'block', 14 | margin: 0, 15 | overflow: 'visible', 16 | width, 17 | height, 18 | ...style 19 | } 20 | 21 | return ( 22 | 28 | ) 29 | } 30 | 31 | export default Svg 32 | 33 | -------------------------------------------------------------------------------- /src/get-scale.js: -------------------------------------------------------------------------------- 1 | 2 | import { scaleLinear } from 'd3-scale' 3 | import { 4 | min as d3min, 5 | max as d3max 6 | } from 'd3-array' 7 | 8 | const isNum = n => !isNaN(n) 9 | 10 | const getScale = ({ 11 | data = [], 12 | length, // Optional length 13 | min, // optional min value 14 | max, // optional max value 15 | padRatio = 2, // Ratio of bar width vs margin 16 | }) => { 17 | const datamin = d3min(data) 18 | const datamax = d3max(data) 19 | 20 | min = isNum(min) && min <= datamin ? min : datamin || 0 21 | max = isNum(max) && max >= datamax ? max : datamax || 100 22 | length = length || data.length 23 | 24 | const denominator = length * (padRatio + 1) 25 | const pad = 1 / denominator * 100 26 | const padWidth = padRatio / denominator * 100 27 | 28 | const xdomain = ([ 0, length - 1 ]) 29 | const ydomain = ([ min, max ]) 30 | const x = scaleLinear().domain(xdomain).range([ 0, 100 ]) 31 | const padx = scaleLinear().domain(xdomain).range([ padWidth / 2, 100 - padWidth / 2 ]) 32 | const y = scaleLinear().domain(ydomain).range([ 100, 0 ]) 33 | 34 | const scale = { x, padx, y } 35 | 36 | return { 37 | min, 38 | max, 39 | scale, 40 | pad, 41 | padWidth 42 | } 43 | } 44 | 45 | export default getScale 46 | 47 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | // SVG 3 | import Svg from './Svg' 4 | import Line from './Line' 5 | import Area from './Area' 6 | import Bar from './Bar' 7 | import Rules from './Rules' 8 | 9 | // HTML 10 | import Chart from './Chart' 11 | import Axis from './Axis' 12 | import DataLabels from './DataLabels' 13 | 14 | // HOC 15 | import getScale from './get-scale' 16 | import withScale from './withScale' 17 | 18 | export { default as Svg } from './Svg' 19 | export { default as Line } from './Line' 20 | export { default as Area } from './Area' 21 | export { default as Bar } from './Bar' 22 | export { default as Rules } from './Rules' 23 | 24 | export { default as Chart } from './Chart' 25 | export { default as Axis } from './Axis' 26 | export { default as DataLabels } from './DataLabels' 27 | 28 | export { default as getScale } from './get-scale' 29 | export { default as withScale } from './withScale' 30 | 31 | export default { 32 | Svg, 33 | Line, 34 | Area, 35 | Bar, 36 | Chart, 37 | Axis, 38 | Rules, 39 | DataLabels, 40 | getScale, 41 | withScale 42 | } 43 | 44 | -------------------------------------------------------------------------------- /src/withScale.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react' 3 | import getScale from './get-scale' 4 | 5 | const withScale = Comp => { 6 | const ScaledComp = ({ 7 | data = [], 8 | viewBox, 9 | ...props 10 | }) => { 11 | viewBox = viewBox || [ 0, 0, 100, 100 ].join(' ') 12 | 13 | const { 14 | min, 15 | max, 16 | scale, 17 | padWidth 18 | } = getScale({ data, ...props }) 19 | 20 | const points = data.map((d, i) => { 21 | return { 22 | d, 23 | x: scale.x(i), 24 | padx: scale.padx(i), 25 | y: scale.y(d) 26 | } 27 | }) 28 | 29 | return ( 30 | 40 | ) 41 | } 42 | 43 | return ScaledComp 44 | } 45 | 46 | export default withScale 47 | 48 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 2 | const path = require('path') 3 | 4 | module.exports = { 5 | entry: './docs/entry.js', 6 | 7 | output: { 8 | path: path.join(__dirname, 'docs'), 9 | filename: 'bundle.js' 10 | }, 11 | 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | exclude: /node_modules/, 17 | loader: 'babel' 18 | }, 19 | { 20 | test: /\.json$/, 21 | loader: 'json' 22 | } 23 | ] 24 | }, 25 | 26 | devServer: { 27 | contentBase: 'docs/' 28 | } 29 | } 30 | 31 | --------------------------------------------------------------------------------