├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .overcommit.yml ├── .projections.json ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── karma.conf.js ├── package.json ├── spec ├── .eslintrc.js └── pie_chart_spec.jsx ├── src └── pie_chart.jsx └── tests.webpack.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-react-jsx", 4 | "add-module-exports" 5 | ], 6 | "presets": ["es2015"] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: 'eslint-config-brigade/react', 3 | ecmaFeatures: { 4 | modules: true, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.svg 2 | .eslintignore 3 | .eslintrc.js 4 | .overcommit.yml 5 | .projections.json 6 | CHANGELOG.md 7 | karma.conf.js 8 | spec 9 | src 10 | tests.webpack.js 11 | webpack.conf.js 12 | -------------------------------------------------------------------------------- /.overcommit.yml: -------------------------------------------------------------------------------- 1 | CommitMsg: 2 | HardTabs: 3 | enabled: true 4 | 5 | PreCommit: 6 | ALL: 7 | exclude: 8 | - 'node_modules/**/*' 9 | 10 | EsLint: 11 | enabled: true 12 | required_executable: './node_modules/.bin/eslint' 13 | include: 14 | - '**/*.js' 15 | - '**/*.jsx' 16 | 17 | HardTabs: 18 | enabled: true 19 | description: 'Checking for hard tabs' 20 | 21 | JsonSyntax: 22 | enabled: true 23 | 24 | MergeConflicts: 25 | enabled: true 26 | 27 | TrailingWhitespace: 28 | enabled: true 29 | 30 | YamlSyntax: 31 | enabled: true 32 | -------------------------------------------------------------------------------- /.projections.json: -------------------------------------------------------------------------------- 1 | { 2 | "src/*.jsx": { 3 | "alternate": "spec/{}_spec.jsx", 4 | "type": "source" 5 | }, 6 | "spec/*_spec.jsx": { 7 | "alternate": "src/{}.jsx", 8 | "type": "test" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_install: 5 | - npm install -g npm@2 6 | before_script: 7 | - export DISPLAY=:99.0 8 | - sh -e /etc/init.d/xvfb start 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## master (unreleased) 2 | 3 | ## 0.5.0 4 | - Add `borderWidth` and `borderColor` props 5 | 6 | ## 0.4.1 7 | - Use `prop-types` package instead of `React.PropTypes` 8 | 9 | ## 0.4.0 10 | - Relax dependency on React to allow v15 11 | 12 | ## 0.3.1 13 | - Make bundled pie-chart.js easier to import 14 | 15 | ## 0.3.0 16 | 17 | - Upgrade Babel from 5 to 6 18 | - Convert to ES2015 module 19 | - Convert to ES2015 class 20 | 21 | ## 0.2.2 22 | 23 | - Fix centering when rendering as a circle 24 | 25 | ## 0.2.1 26 | 27 | - Update for React 0.14 28 | - Move `react` from `dependencies` to `peerDependencies` 29 | 30 | ## 0.2.0 31 | 32 | - Reduce jaggedness from thin segments 33 | - Remove size prop 34 | 35 | ## 0.1.1 36 | 37 | - Fix npm dependencies 38 | 39 | ## 0.1.0 40 | 41 | - Initial release 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Brigade 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Simple Pie Chart 2 | 3 | [![npm version](https://badge.fury.io/js/react-simple-pie-chart.svg)](http://badge.fury.io/js/react-simple-pie-chart) 4 | [![Build Status](https://travis-ci.org/brigade/react-simple-pie-chart.svg?branch=master)](https://travis-ci.org/brigade/react-simple-pie-chart) 5 | 6 | Need a simple `` pie chart and don't want to bring in any heavy 7 | dependencies? You've come to the right place. 8 | 9 | [Demo](http://jsfiddle.net/qgxyw3mp/3/) 10 | 11 | ![Example pie 12 | chart](http://brigade.github.io/react-simple-pie-chart/example-pie-chart.svg) 13 | 14 | ## Installation 15 | 16 | ### npm 17 | 18 | ```bash 19 | npm install react-simple-pie-chart --save 20 | ``` 21 | 22 | ## Usage 23 | 24 | ```javascript 25 | import PieChart from 'react-simple-pie-chart'; 26 | ``` 27 | 28 | ```javascript 29 | 41 | ``` 42 | 43 | ## Code of conduct 44 | 45 | This project adheres to the [Open Code of Conduct][code-of-conduct]. By 46 | participating, you are expected to honor this code. 47 | 48 | [code-of-conduct]: https://github.com/brigade/code-of-conduct 49 | 50 | ## License 51 | 52 | [MIT][mit-license] 53 | 54 | [mit-license]: ./LICENSE 55 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* global process */ 2 | // Karma configuration 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path that will be used to resolve all patterns (eg. files, exclude) 7 | basePath: '', 8 | 9 | // frameworks to use 10 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 11 | frameworks: ['jasmine'], 12 | 13 | // list of files / patterns to load in the browser 14 | files: [ 15 | 'tests.webpack.js' 16 | ], 17 | 18 | // list of files to exclude 19 | exclude: [ 20 | ], 21 | 22 | // preprocess matching files before serving them to the browser 23 | // available preprocessors: 24 | // https://npmjs.org/browse/keyword/karma-preprocessor 25 | preprocessors: { 26 | 'tests.webpack.js': ['webpack'] 27 | }, 28 | 29 | webpack: { 30 | module: { 31 | loaders: [ 32 | { 33 | test: /\.jsx?$/, 34 | loaders: ['babel-loader?cacheDirectory=true'], 35 | exclude: /node_modules/ 36 | } 37 | ] 38 | } 39 | }, 40 | 41 | webpackMiddleware: { 42 | noInfo: true 43 | }, 44 | 45 | // test results reporter to use 46 | // possible values: 'dots', 'progress' 47 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 48 | reporters: ['progress'], 49 | 50 | // web server port 51 | port: 9876, 52 | 53 | // enable / disable colors in the output (reporters and logs) 54 | colors: true, 55 | 56 | // level of logging 57 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || 58 | // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 59 | logLevel: config.LOG_INFO, 60 | 61 | // enable / disable watching file and executing tests whenever any file 62 | // changes 63 | autoWatch: true, 64 | 65 | // start these browsers 66 | // available browser launchers: 67 | // https://npmjs.org/browse/keyword/karma-launcher 68 | browsers: process.env.CONTINUOUS_INTEGRATION === 'true' ? 69 | ['Firefox'] : ['Chrome'], 70 | 71 | // Continuous Integration mode 72 | // if true, Karma captures browsers, runs the tests and exits 73 | singleRun: process.env.CONTINUOUS_INTEGRATION === 'true' 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-simple-pie-chart", 3 | "version": "0.5.0", 4 | "description": "A React component to generate simple pie charts.", 5 | "main": "build/npm/pie_chart.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/brigade/react-simple-pie-chart.git" 9 | }, 10 | "homepage": "https://github.com/brigade/react-simple-pie-chart", 11 | "bugs": { 12 | "url": "https://github.com/brigade/react-simple-pie-chart/issues" 13 | }, 14 | "scripts": { 15 | "build-npm": "rm -rf build/npm && mkdir -p build/npm && babel src/pie_chart.jsx --out-file build/npm/pie_chart.js", 16 | "test": "eslint . --ext .js,.jsx && ./node_modules/.bin/karma start", 17 | "prepublish": "npm run build-npm" 18 | }, 19 | "author": "Brigade Engineering", 20 | "contributors": [ 21 | "Joe Lencioni" 22 | ], 23 | "license": "MIT", 24 | "peerDependencies": { 25 | "react": ">=0.13" 26 | }, 27 | "devDependencies": { 28 | "babel": "^6.0.0", 29 | "babel-cli": "^6.0.0", 30 | "babel-core": "^6.0.0", 31 | "babel-loader": "^6.0.0", 32 | "babel-plugin-add-module-exports": "^0.1.2", 33 | "babel-plugin-transform-react-jsx": "^6.0.0", 34 | "babel-preset-es2015": "^6.0.0", 35 | "eslint": "^1.3.1", 36 | "eslint-config-brigade": "^1.6.0", 37 | "eslint-plugin-react": "^3.3.1", 38 | "jasmine-core": "^2.1.3", 39 | "karma": "^0.13.9", 40 | "karma-chrome-launcher": "^0.2.0", 41 | "karma-cli": "^0.1.0", 42 | "karma-firefox-launcher": "^0.1.6", 43 | "karma-jasmine": "^0.3.6", 44 | "karma-webpack": "^1.7.0", 45 | "prop-types": "^15.5.8", 46 | "react": "^0.14.0", 47 | "react-addons-test-utils": "^0.14.0", 48 | "react-dom": "^0.14.0", 49 | "webpack": "^1.12.0" 50 | }, 51 | "keywords": [ 52 | "react", 53 | "component", 54 | "react-component", 55 | "charts", 56 | "charting", 57 | "data visualization", 58 | "pie chart" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /spec/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globals: { 3 | afterEach: false, 4 | beforeEach: false, 5 | describe: false, 6 | expect: false, 7 | it: false, 8 | jasmine: false, 9 | spyOn: false, 10 | xit: false, 11 | }, 12 | 13 | rules: { 14 | 'max-nested-callbacks': [2, 4], 15 | 'react/prop-types': 0, 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /spec/pie_chart_spec.jsx: -------------------------------------------------------------------------------- 1 | import PieChart from '../src/pie_chart.jsx'; 2 | import React from 'react'; 3 | import TestUtils from 'react-addons-test-utils'; 4 | import ReactDOM from 'react-dom'; 5 | 6 | let div; 7 | 8 | const renderAttached = function(component) { 9 | div = document.createElement('div'); 10 | document.body.appendChild(div); 11 | const renderedComponent = ReactDOM.render(component, div); 12 | return renderedComponent; 13 | }; 14 | 15 | describe('', function() { 16 | beforeEach(() => { 17 | this.props = { 18 | slices: [] 19 | }; 20 | this.subject = () => renderAttached(); 21 | }); 22 | 23 | it('is an svg', () => { 24 | expect(ReactDOM.findDOMNode(this.subject()).tagName.toLowerCase()) 25 | .toEqual('svg'); 26 | }); 27 | 28 | describe('with two slices with positive values', () => { 29 | beforeEach(() => { 30 | this.props.slices = [ 31 | { 32 | color: '#f00', 33 | value: 10, 34 | }, 35 | { 36 | color: '#0f0', 37 | value: 20, 38 | }, 39 | ]; 40 | }); 41 | 42 | it('renders zero circles', () => { 43 | const circles = 44 | TestUtils.scryRenderedDOMComponentsWithTag(this.subject(), 'circle'); 45 | expect(circles.length).toEqual(0); 46 | }); 47 | 48 | it('renders two paths', () => { 49 | const paths = 50 | TestUtils.scryRenderedDOMComponentsWithTag(this.subject(), 'path'); 51 | expect(paths.length).toEqual(2); 52 | }); 53 | }); 54 | 55 | describe('with three slices with positive values', () => { 56 | beforeEach(() => { 57 | this.props.slices = [ 58 | { 59 | color: '#f00', 60 | value: 10, 61 | }, 62 | { 63 | color: '#0f0', 64 | value: 20, 65 | }, 66 | { 67 | color: '#00f', 68 | value: 30, 69 | }, 70 | ]; 71 | }); 72 | 73 | it('renders zero circles', () => { 74 | const circles = 75 | TestUtils.scryRenderedDOMComponentsWithTag(this.subject(), 'circle'); 76 | expect(circles.length).toEqual(0); 77 | }); 78 | 79 | it('renders three paths', () => { 80 | const paths = 81 | TestUtils.scryRenderedDOMComponentsWithTag(this.subject(), 'path'); 82 | expect(paths.length).toEqual(3); 83 | }); 84 | }); 85 | 86 | describe('with a slice that is at 100%', () => { 87 | beforeEach(() => { 88 | this.props.slices = [ 89 | { 90 | color: '#f00', 91 | value: 0, 92 | }, 93 | { 94 | color: '#0f0', 95 | value: 20, 96 | }, 97 | ]; 98 | }); 99 | 100 | it('renders a circle', () => { 101 | const circle = 102 | TestUtils.findRenderedDOMComponentWithTag(this.subject(), 'circle'); 103 | expect(circle).toBeTruthy(); 104 | }); 105 | 106 | describe('the circle', () => { 107 | beforeEach(() => { 108 | this.circle = 109 | TestUtils.findRenderedDOMComponentWithTag(this.subject(), 'circle'); 110 | }); 111 | 112 | it('has the correct color', () => { 113 | expect(this.circle.getAttribute('fill')).toEqual('#0f0'); 114 | }); 115 | }); 116 | 117 | it('renders zero paths', () => { 118 | const paths = 119 | TestUtils.scryRenderedDOMComponentsWithTag(this.subject(), 'path'); 120 | expect(paths.length).toEqual(0); 121 | }); 122 | }); 123 | 124 | describe('with a border around the pie', () => { 125 | const color = '#000000'; 126 | const width = 1; 127 | 128 | beforeEach(() => { 129 | this.props.slices = []; 130 | this.props.borderWidth = width; 131 | this.props.borderColor = color; 132 | }); 133 | 134 | it(`renders a border with color ${color}`, () => { 135 | const circle = 136 | TestUtils.findRenderedDOMComponentWithTag(this.subject(), 'circle'); 137 | expect(circle.getAttribute('stroke')).toEqual(color); 138 | }); 139 | 140 | it(`renders a border of strength ${width}`, () => { 141 | const circle = 142 | TestUtils.findRenderedDOMComponentWithTag(this.subject(), 'circle'); 143 | expect(circle.getAttribute('stroke-width')).toEqual(String(width)); 144 | }); 145 | }); 146 | }); 147 | -------------------------------------------------------------------------------- /src/pie_chart.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const size = 100; 5 | const radCircumference = Math.PI * 2; 6 | const center = size / 2; 7 | const radius = center - 1; // padding to prevent clipping 8 | 9 | /** 10 | * @param {Object[]} slices 11 | * @return {Object[]} 12 | */ 13 | function renderPaths(slices) { 14 | const total = slices.reduce((totalValue, { value }) => totalValue + value, 0); 15 | 16 | let radSegment = 0; 17 | let lastX = radius; 18 | let lastY = 0; 19 | 20 | return slices.map(({ color, value }, index) => { 21 | // Should we just draw a circle? 22 | if (value === total) { 23 | return ( 24 | 31 | ); 32 | } 33 | 34 | if (value === 0) { 35 | return; 36 | } 37 | 38 | const valuePercentage = value / total; 39 | 40 | // Should the arc go the long way round? 41 | const longArc = (valuePercentage <= 0.5) ? 0 : 1; 42 | 43 | radSegment += valuePercentage * radCircumference; 44 | const nextX = Math.cos(radSegment) * radius; 45 | const nextY = Math.sin(radSegment) * radius; 46 | 47 | // d is a string that describes the path of the slice. 48 | // The weirdly placed minus signs [eg, (-(lastY))] are due to the fact 49 | // that our calculations are for a graph with positive Y values going up, 50 | // but on the screen positive Y values go down. 51 | const d = [ 52 | `M ${center},${center}`, 53 | `l ${lastX},${-lastY}`, 54 | `a${radius},${radius}`, 55 | '0', 56 | `${longArc},0`, 57 | `${nextX - lastX},${-(nextY - lastY)}`, 58 | 'z', 59 | ].join(' '); 60 | 61 | lastX = nextX; 62 | lastY = nextY; 63 | 64 | return ; 65 | }); 66 | } 67 | 68 | /** 69 | * Generates an SVG pie chart. 70 | * @see {http://wiki.scribus.net/canvas/Making_a_Pie_Chart} 71 | */ 72 | export default class PieChart extends React.Component { 73 | /** 74 | * @return {Object} 75 | */ 76 | render() { 77 | const border = this.props.borderWidth > 0 ? ( 78 | 86 | ) : null; 87 | 88 | return ( 89 | 90 | 91 | {renderPaths(this.props.slices)} 92 | 93 | {border} 94 | 95 | ); 96 | } 97 | } 98 | 99 | PieChart.propTypes = { 100 | slices: PropTypes.arrayOf(PropTypes.shape({ 101 | color: PropTypes.string.isRequired, // hex color 102 | value: PropTypes.number.isRequired, 103 | })).isRequired, 104 | borderColor: PropTypes.string, 105 | borderWidth: PropTypes.number, 106 | }; 107 | 108 | PieChart.defaultProps = { 109 | borderColor: '#FFFFFF', 110 | borderWidth: 0 111 | }; 112 | -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | const context = require.context('./spec', true, /_spec\.jsx?$/); 2 | context.keys().forEach(context); 3 | --------------------------------------------------------------------------------