├── src ├── App.css ├── lib │ ├── index.js │ └── GaugeChart │ │ ├── customHooks.js │ │ ├── renderChart.js │ │ ├── drawNeedle.js │ │ ├── utils.js │ │ └── index.js ├── App.test.js ├── index.js ├── index.css ├── Icon.js ├── logo.svg ├── App.js └── serviceWorker.js ├── public ├── favicon.ico ├── manifest.json └── index.html ├── .babelrc ├── dist ├── index.js └── GaugeChart │ ├── customHooks.js │ ├── renderChart.js │ ├── drawNeedle.js │ ├── utils.js │ └── index.js ├── .vscode └── launch.json ├── .github └── workflows │ └── npm-publish-github-packages.yml ├── LICENSE ├── .gitignore ├── package.json └── README.md /src/App.css: -------------------------------------------------------------------------------- 1 | .text-component { 2 | font-size: medium; 3 | color: #F5CD19; 4 | } -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | import GaugeChart from './GaugeChart'; 2 | export default GaugeChart; -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Martin36/react-gauge-chart/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | ["@babel/preset-env", {"useBuiltIns": "entry"}] 5 | ], 6 | "plugins": ["@babel/plugin-proposal-class-properties"] 7 | } 8 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = void 0; 7 | 8 | var _GaugeChart = _interopRequireDefault(require("./GaugeChart")); 9 | 10 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 11 | 12 | var _default = _GaugeChart.default; 13 | exports.default = _default; -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React Gauge Chart", 3 | "name": "React Gauge Chart Demo", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Chrome", 9 | "type": "chrome", 10 | "request": "launch", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceRoot}/src" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: http://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Roboto+Condensed'); 2 | @import url('https://fonts.googleapis.com/css?family=Roboto'); 3 | 4 | body { 5 | margin: 0; 6 | padding: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 8 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 9 | sans-serif; 10 | -webkit-font-smoothing: antialiased; 11 | -moz-osx-font-smoothing: grayscale; 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | text-align: center; 15 | font-size: calc(10px + 2vmin); 16 | color: white; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/lib/GaugeChart/customHooks.js: -------------------------------------------------------------------------------- 1 | import isEqual from "lodash/isEqual"; 2 | import { useEffect, useRef } from "react"; 3 | 4 | const isDeepEquals = (toCompare, reference) => { 5 | return isEqual(toCompare, reference); 6 | }; 7 | 8 | const useDeepCompareMemo = (dependencies) => { 9 | const ref = useRef(null); 10 | if (isDeepEquals(dependencies, ref.current)) { 11 | ref.current = dependencies; 12 | } 13 | return ref.current; 14 | }; 15 | 16 | // this function compares deeply new dependencies with old one 17 | // It works like useEffect but we are using isEqual from lodash to compares deeply 18 | const useDeepCompareEffect = (callback, dependencies) => { 19 | useEffect(callback, [useDeepCompareMemo(dependencies), callback]); 20 | }; 21 | 22 | export default useDeepCompareEffect; 23 | -------------------------------------------------------------------------------- /src/Icon.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function Icon({percent, height, width}) { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | export default Icon 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish-github-packages.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-gpr: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | permissions: 25 | contents: read 26 | packages: write 27 | steps: 28 | - uses: actions/checkout@v3 29 | - uses: actions/setup-node@v3 30 | with: 31 | node-version: 16 32 | registry-url: https://npm.pkg.github.com/ 33 | - run: npm ci 34 | - run: npm publish 35 | env: 36 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 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 | -------------------------------------------------------------------------------- /dist/GaugeChart/customHooks.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = void 0; 7 | 8 | var _isEqual = _interopRequireDefault(require("lodash/isEqual")); 9 | 10 | var _react = require("react"); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 13 | 14 | var isDeepEquals = function isDeepEquals(toCompare, reference) { 15 | return (0, _isEqual.default)(toCompare, reference); 16 | }; 17 | 18 | var useDeepCompareMemo = function useDeepCompareMemo(dependencies) { 19 | var ref = (0, _react.useRef)(null); 20 | 21 | if (isDeepEquals(dependencies, ref.current)) { 22 | ref.current = dependencies; 23 | } 24 | 25 | return ref.current; 26 | }; // this function compares deeply new dependencies with old one 27 | // It works like useEffect but we are using isEqual from lodash to compares deeply 28 | 29 | 30 | var useDeepCompareEffect = function useDeepCompareEffect(callback, dependencies) { 31 | (0, _react.useEffect)(callback, [useDeepCompareMemo(dependencies), callback]); 32 | }; 33 | 34 | var _default = useDeepCompareEffect; 35 | exports.default = _default; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 63 | 64 | # dependencies 65 | /node_modules 66 | /.pnp 67 | .pnp.js 68 | 69 | # testing 70 | /coverage 71 | 72 | # production 73 | build/ 74 | /build 75 | 76 | # misc 77 | .DS_Store 78 | .env.local 79 | .env.development.local 80 | .env.test.local 81 | .env.production.local 82 | 83 | npm-debug.log* 84 | yarn-debug.log* 85 | yarn-error.log* 86 | 87 | .npmrc 88 | token.txt 89 | .vscode -------------------------------------------------------------------------------- /dist/GaugeChart/renderChart.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.renderChart = void 0; 7 | 8 | var _utils = require("./utils"); 9 | 10 | //Renders the chart, should be called every time the window is resized 11 | var renderChart = function renderChart(resize, prevProps, width, margin, height, outerRadius, g, doughnut, arcChart, needle, pieChart, svg, props, container, arcData) { 12 | (0, _utils.updateDimensions)(props, container, margin, width, height); //Set dimensions of svg element and translations 13 | 14 | svg.current.attr("width", width.current + margin.current.left + margin.current.right).attr("height", height.current + margin.current.top + margin.current.bottom); 15 | g.current.attr("transform", "translate(" + margin.current.left + ", " + margin.current.top + ")"); //Set the radius to lesser of width or height and remove the margins 16 | //Calculate the new radius 17 | 18 | (0, _utils.calculateRadius)(width, height, outerRadius, margin, g); 19 | doughnut.current.attr("transform", "translate(" + outerRadius.current + ", " + outerRadius.current + ")"); //Setup the arc 20 | 21 | arcChart.current.outerRadius(outerRadius.current).innerRadius(outerRadius.current * (1 - props.arcWidth)).cornerRadius(props.cornerRadius).padAngle(props.arcPadding); //Remove the old stuff 22 | 23 | doughnut.current.selectAll(".arc").remove(); 24 | g.current.selectAll(".text-group").remove(); //Draw the arc 25 | 26 | var arcPaths = doughnut.current.selectAll(".arc").data(pieChart.current(arcData.current)).enter().append("g").attr("class", "arc"); 27 | arcPaths.append("path").attr("d", arcChart.current).style("fill", function (d) { 28 | return d.data.color; 29 | }); 30 | }; 31 | 32 | exports.renderChart = renderChart; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | 26 | React App 27 | 28 | 29 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/lib/GaugeChart/renderChart.js: -------------------------------------------------------------------------------- 1 | import { updateDimensions, calculateRadius } from "./utils"; 2 | 3 | //Renders the chart, should be called every time the window is resized 4 | export const renderChart = ( 5 | resize, 6 | prevProps, 7 | width, 8 | margin, 9 | height, 10 | outerRadius, 11 | g, 12 | doughnut, 13 | arcChart, 14 | needle, 15 | pieChart, 16 | svg, 17 | props, 18 | container, 19 | arcData 20 | ) => { 21 | updateDimensions(props, container, margin, width, height); 22 | //Set dimensions of svg element and translations 23 | svg.current 24 | .attr("width", width.current + margin.current.left + margin.current.right) 25 | .attr( 26 | "height", 27 | height.current + margin.current.top + margin.current.bottom 28 | ); 29 | g.current.attr( 30 | "transform", 31 | "translate(" + margin.current.left + ", " + margin.current.top + ")" 32 | ); 33 | //Set the radius to lesser of width or height and remove the margins 34 | //Calculate the new radius 35 | calculateRadius(width, height, outerRadius, margin, g); 36 | doughnut.current.attr( 37 | "transform", 38 | "translate(" + outerRadius.current + ", " + outerRadius.current + ")" 39 | ); 40 | //Setup the arc 41 | arcChart.current 42 | .outerRadius(outerRadius.current) 43 | .innerRadius(outerRadius.current * (1 - props.arcWidth)) 44 | .cornerRadius(props.cornerRadius) 45 | .padAngle(props.arcPadding); 46 | //Remove the old stuff 47 | doughnut.current.selectAll(".arc").remove(); 48 | g.current.selectAll(".text-group").remove(); 49 | //Draw the arc 50 | let arcPaths = doughnut.current 51 | .selectAll(".arc") 52 | .data(pieChart.current(arcData.current)) 53 | .enter() 54 | .append("g") 55 | .attr("class", "arc"); 56 | arcPaths 57 | .append("path") 58 | .attr("d", arcChart.current) 59 | .style("fill", function (d) { 60 | return d.data.color; 61 | }); 62 | }; 63 | 64 | 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-gauge-chart", 3 | "version": "0.5.1", 4 | "main": "dist/index", 5 | "module": "dist/index", 6 | "typings": "dist/index", 7 | "homepage": "https://martin36.github.io/react-gauge-chart/", 8 | "keywords": [ 9 | "visualization", 10 | "react", 11 | "gauge chart", 12 | "speedometer" 13 | ], 14 | "license": "MIT", 15 | "files": [ 16 | "dist", 17 | "README.md" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/Martin36/react-gauge-chart" 22 | }, 23 | "dependencies": { 24 | "d3": "^7.6.1" 25 | }, 26 | "scripts": { 27 | "start": "react-scripts start", 28 | "prebuild": "rimraf dist", 29 | "build": "NODE_ENV=production babel src/lib --out-dir dist --copy-files", 30 | "build-page": "react-scripts build", 31 | "test": "react-scripts test", 32 | "eject": "react-scripts eject", 33 | "predeploy": "npm run build-page", 34 | "deploy": "gh-pages -d build" 35 | }, 36 | "eslintConfig": { 37 | "extends": "react-app" 38 | }, 39 | "browserslist": [ 40 | ">0.2%", 41 | "not dead", 42 | "not ie <= 11", 43 | "not op_mini all" 44 | ], 45 | "devDependencies": { 46 | "@babel/cli": "^7.12.8", 47 | "@babel/core": "^7.6.2", 48 | "@babel/plugin-proposal-class-properties": "^7.4.4", 49 | "@babel/preset-env": "^7.4.4", 50 | "@babel/preset-react": "^7.0.0", 51 | "@babel/runtime": "^7.6.2", 52 | "babel-preset-react-app": "^8.0.0", 53 | "cross-env": "^5.2.1", 54 | "gh-pages": "^2.1.1", 55 | "react": "^17.0.1", 56 | "react-bootstrap": "^1.4.0", 57 | "react-dom": "^17.0.1", 58 | "react-scripts": "5.0.1", 59 | "rimraf": "^2.7.1" 60 | }, 61 | "peerDependencies": { 62 | "react": "^16.8.2 || ^17.0 || ^18.x || ^19.x", 63 | "react-dom": "^16.8.2 || ^17.0 || ^18.x || ^19.x" 64 | }, 65 | "publishConfig": { 66 | "registry": "https://registry.npmjs.org/" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /dist/GaugeChart/drawNeedle.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.drawNeedle = void 0; 7 | 8 | var _d = require("d3"); 9 | 10 | var _utils = require("./utils"); 11 | 12 | //If 'resize' is true then the animation does not play 13 | var drawNeedle = function drawNeedle(resize, prevProps, props, width, needle, container, outerRadius, g) { 14 | var percent = props.percent, 15 | needleColor = props.needleColor, 16 | needleBaseColor = props.needleBaseColor, 17 | hideText = props.hideText, 18 | animate = props.animate, 19 | needleScale = props.needleScale, 20 | textComponent = props.textComponent; 21 | var needleRadius = 15 * (width.current / 500), 22 | // Make the needle radius responsive 23 | centerPoint = [0, -needleRadius / 2]; //Remove the old stuff 24 | 25 | needle.current.selectAll("*").remove(); //Translate the needle starting point to the middle of the arc 26 | 27 | needle.current.attr("transform", "translate(" + outerRadius.current + ", " + outerRadius.current + ")"); //Draw the triangle 28 | //let pathStr = `M ${leftPoint[0]} ${leftPoint[1]} L ${topPoint[0]} ${topPoint[1]} L ${rightPoint[0]} ${rightPoint[1]}`; 29 | 30 | var prevPercent = prevProps ? prevProps.percent : 0; 31 | var pathStr = (0, _utils.calculateRotation)(prevPercent || percent, outerRadius, width, needleScale); 32 | needle.current.append("path").attr("d", pathStr).attr("fill", needleColor); //Add a circle at the bottom of needle 33 | 34 | needle.current.append("circle").attr("cx", centerPoint[0]).attr("cy", centerPoint[1]).attr("r", needleRadius).attr("fill", needleBaseColor); 35 | 36 | if (!hideText && !textComponent) { 37 | (0, _utils.addText)(percent, props, outerRadius, width, g); 38 | } //Rotate the needle 39 | 40 | 41 | if (!resize && animate) { 42 | needle.current.transition().delay(props.animDelay).ease(_d.easeElastic).duration(props.animateDuration).tween("progress", function () { 43 | var currentPercent = (0, _d.interpolateNumber)(prevPercent, percent); 44 | return function (percentOfPercent) { 45 | var progress = currentPercent(percentOfPercent); 46 | return container.current.select(".needle path").attr("d", (0, _utils.calculateRotation)(progress, outerRadius, width, needleScale)); 47 | }; 48 | }); 49 | } else { 50 | container.current.select(".needle path").attr("d", (0, _utils.calculateRotation)(percent, outerRadius, width, needleScale)); 51 | } 52 | }; 53 | 54 | exports.drawNeedle = drawNeedle; -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/lib/GaugeChart/drawNeedle.js: -------------------------------------------------------------------------------- 1 | import { 2 | easeElastic, 3 | interpolateNumber, 4 | } from "d3"; 5 | import { calculateRotation, addText } from "./utils" 6 | 7 | //If 'resize' is true then the animation does not play 8 | export const drawNeedle = ( 9 | resize, 10 | prevProps, 11 | props, 12 | width, 13 | needle, 14 | container, 15 | outerRadius, 16 | g 17 | ) => { 18 | const { 19 | percent, 20 | needleColor, 21 | needleBaseColor, 22 | hideText, 23 | animate, 24 | needleScale, 25 | textComponent, 26 | } = props; 27 | let needleRadius = 15 * (width.current / 500), // Make the needle radius responsive 28 | centerPoint = [0, -needleRadius / 2]; 29 | 30 | //Remove the old stuff 31 | needle.current.selectAll("*").remove(); 32 | 33 | //Translate the needle starting point to the middle of the arc 34 | needle.current.attr( 35 | "transform", 36 | "translate(" + outerRadius.current + ", " + outerRadius.current + ")" 37 | ); 38 | 39 | //Draw the triangle 40 | //let pathStr = `M ${leftPoint[0]} ${leftPoint[1]} L ${topPoint[0]} ${topPoint[1]} L ${rightPoint[0]} ${rightPoint[1]}`; 41 | const prevPercent = prevProps ? prevProps.percent : 0; 42 | let pathStr = calculateRotation( 43 | prevPercent || percent, 44 | outerRadius, 45 | width, 46 | needleScale 47 | ); 48 | needle.current.append("path").attr("d", pathStr).attr("fill", needleColor); 49 | //Add a circle at the bottom of needle 50 | needle.current 51 | .append("circle") 52 | .attr("cx", centerPoint[0]) 53 | .attr("cy", centerPoint[1]) 54 | .attr("r", needleRadius) 55 | .attr("fill", needleBaseColor); 56 | if (!hideText && !textComponent) { 57 | addText(percent, props, outerRadius, width, g); 58 | } 59 | //Rotate the needle 60 | if (!resize && animate) { 61 | needle.current 62 | .transition() 63 | .delay(props.animDelay) 64 | .ease(easeElastic) 65 | .duration(props.animateDuration) 66 | .tween("progress", function () { 67 | const currentPercent = interpolateNumber(prevPercent, percent); 68 | return function (percentOfPercent) { 69 | const progress = currentPercent(percentOfPercent); 70 | return container.current 71 | .select(`.needle path`) 72 | .attr( 73 | "d", 74 | calculateRotation(progress, outerRadius, width, needleScale) 75 | ); 76 | }; 77 | }); 78 | } else { 79 | container.current 80 | .select(`.needle path`) 81 | .attr("d", calculateRotation(percent, outerRadius, width, needleScale)); 82 | } 83 | }; 84 | 85 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { Container, Row, Col } from 'react-bootstrap' 3 | import './App.css' 4 | import GaugeChart from './lib' 5 | import Icon from './Icon' 6 | 7 | const App = () => { 8 | const [currentPercent, setCurrentPercent] = useState(); 9 | const [arcs, setArcs] = useState([0.5, 0.3, 0.2]) 10 | 11 | useEffect(() => { 12 | const timer = setTimeout(() => { 13 | setCurrentPercent(Math.random()); 14 | setArcs([0.1, 0.5, 0.4]) 15 | }, 3000); 16 | 17 | return () => { 18 | clearTimeout(timer); 19 | }; 20 | }, []); 21 | 22 | const chartStyle = { 23 | height: 250, 24 | } 25 | 26 | return ( 27 | <> 28 | 29 | 30 | 31 |

React Gauge Chart Demo

32 | 33 |
34 | 35 | 36 |
GaugeChart with default props
37 | 38 | 39 | 40 |
GaugeChart with 20 levels & custom needle component
41 | } 48 | customNeedleStyle={{ bottom: "9.25rem", right: "0.5rem" }} 49 | /> 50 | 51 |
52 | 53 | 54 |
GaugeChart with custom colors
55 | 62 | 63 | 64 |
GaugeChart with larger padding between elements
65 | 73 | 74 |
75 | 76 | 77 |
GaugeChart with custom arcs width
78 | 87 | 88 | 89 |
GaugeChart without animation
90 | 98 | 99 |
100 | 101 | 102 |
GaugeChart with live updates
103 | 109 | 110 | 111 |
GaugeChart with formatted text
112 | value + 'kbit/s'} 120 | /> 121 | 122 |
123 | 124 | 125 |
GaugeChart with arcs update
126 | 135 | 136 | 137 |
GaugeChart with custom text component
138 | Warning!} 147 | textComponentContainerClassName="text-component" 148 | /> 149 | 150 |
151 |
152 | 153 | ) 154 | }; 155 | 156 | export default App 157 | -------------------------------------------------------------------------------- /src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/lib/GaugeChart/utils.js: -------------------------------------------------------------------------------- 1 | 2 | import { scaleLinear, interpolateHsl } from "d3" 3 | 4 | export const calculateRadius = (width, height, outerRadius, margin, g) => { 5 | //The radius needs to be constrained by the containing div 6 | //Since it is a half circle we are dealing with the height of the div 7 | //Only needs to be half of the width, because the width needs to be 2 * radius 8 | //For the whole arc to fit 9 | 10 | //First check if it is the width or the height that is the "limiting" dimension 11 | if (width.current < 2 * height.current) { 12 | //Then the width limits the size of the chart 13 | //Set the radius to the width - the horizontal margins 14 | outerRadius.current = 15 | (width.current - margin.current.left - margin.current.right) / 2; 16 | } else { 17 | outerRadius.current = 18 | height.current - margin.current.top - margin.current.bottom; 19 | } 20 | centerGraph(width, g, outerRadius, margin); 21 | }; 22 | 23 | 24 | //Calculates new margins to make the graph centered 25 | export const centerGraph = (width, g, outerRadius, margin) => { 26 | margin.current.left = 27 | width.current / 2 - outerRadius.current + margin.current.right; 28 | g.current.attr( 29 | "transform", 30 | "translate(" + margin.current.left + ", " + margin.current.top + ")" 31 | ); 32 | }; 33 | 34 | export const updateDimensions = (props, container, margin, width, height) => { 35 | //TODO: Fix so that the container is included in the component 36 | const { marginInPercent } = props; 37 | let divDimensions = container.current.node().getBoundingClientRect(), 38 | divWidth = divDimensions.width, 39 | divHeight = divDimensions.height; 40 | 41 | //Set the new width and horizontal margins 42 | margin.current.left = divWidth * marginInPercent; 43 | margin.current.right = divWidth * marginInPercent; 44 | width.current = divWidth - margin.current.left - margin.current.right; 45 | 46 | margin.current.top = divHeight * marginInPercent; 47 | margin.current.bottom = divHeight * marginInPercent; 48 | height.current = 49 | width.current / 2 - margin.current.top - margin.current.bottom; 50 | //height.current = divHeight - margin.current.top - margin.current.bottom; 51 | }; 52 | 53 | 54 | export const calculateRotation = (percent, outerRadius, width, needleScale) => { 55 | let needleLength = outerRadius.current * needleScale, //TODO: Maybe it should be specified as a percentage of the arc radius? 56 | needleRadius = 15 * (width.current / 500), 57 | theta = percentToRad(percent), 58 | centerPoint = [0, -needleRadius / 2], 59 | topPoint = [ 60 | centerPoint[0] - needleLength * Math.cos(theta), 61 | centerPoint[1] - needleLength * Math.sin(theta), 62 | ], 63 | leftPoint = [ 64 | centerPoint[0] - needleRadius * Math.cos(theta - Math.PI / 2), 65 | centerPoint[1] - needleRadius * Math.sin(theta - Math.PI / 2), 66 | ], 67 | rightPoint = [ 68 | centerPoint[0] - needleRadius * Math.cos(theta + Math.PI / 2), 69 | centerPoint[1] - needleRadius * Math.sin(theta + Math.PI / 2), 70 | ]; 71 | let pathStr = `M ${leftPoint[0]} ${leftPoint[1]} L ${topPoint[0]} ${topPoint[1]} L ${rightPoint[0]} ${rightPoint[1]}`; 72 | return pathStr; 73 | }; 74 | 75 | //Adds text undeneath the graft to display which percentage is the current one 76 | export const addText = (percentage, props, outerRadius, width, g) => { 77 | const { formatTextValue, fontSize } = props; 78 | let textPadding = 20; 79 | const text = formatTextValue 80 | ? formatTextValue(floatingNumber(percentage)) 81 | : floatingNumber(percentage) + "%"; 82 | g.current 83 | .append("g") 84 | .attr("class", "text-group") 85 | .attr( 86 | "transform", 87 | `translate(${outerRadius.current}, ${ 88 | outerRadius.current / 2 + textPadding 89 | })` 90 | ) 91 | .append("text") 92 | .text(text) 93 | // this computation avoid text overflow. When formatted value is over 10 characters, we should reduce font size 94 | .style("font-size", () => 95 | fontSize 96 | ? fontSize 97 | : `${width.current / 11 / (text.length > 10 ? text.length / 10 : 1)}px` 98 | ) 99 | .style("fill", props.textColor) 100 | .style("text-anchor", "middle"); 101 | }; 102 | 103 | 104 | // This function update arc's datas when component is mounting or when one of arc's props is updated 105 | export const setArcData = (props, nbArcsToDisplay, colorArray, arcData) => { 106 | // We have to make a decision about number of arcs to display 107 | // If arcsLength is setted, we choose arcsLength length instead of nrOfLevels 108 | nbArcsToDisplay.current = props.arcsLength 109 | ? props.arcsLength.length 110 | : props.nrOfLevels; 111 | 112 | //Check if the number of colors equals the number of levels 113 | //Otherwise make an interpolation 114 | if (nbArcsToDisplay.current === props.colors.length) { 115 | colorArray.current = props.colors; 116 | } else { 117 | colorArray.current = getColors(props, nbArcsToDisplay); 118 | } 119 | //The data that is used to create the arc 120 | // Each arc could have hiw own value width arcsLength prop 121 | arcData.current = []; 122 | for (let i = 0; i < nbArcsToDisplay.current; i++) { 123 | let arcDatum = { 124 | value: 125 | props.arcsLength && props.arcsLength.length > i 126 | ? props.arcsLength[i] 127 | : 1, 128 | color: colorArray.current[i], 129 | }; 130 | arcData.current.push(arcDatum); 131 | } 132 | }; 133 | 134 | //Depending on the number of levels in the chart 135 | //This function returns the same number of colors 136 | const getColors = (props, nbArcsToDisplay) => { 137 | const { colors } = props; 138 | let colorScale = scaleLinear() 139 | .domain([1, nbArcsToDisplay.current]) 140 | .range([colors[0], colors[colors.length - 1]]) //Use the first and the last color as range 141 | .interpolate(interpolateHsl); 142 | let colorArray = []; 143 | for (let i = 1; i <= nbArcsToDisplay.current; i++) { 144 | colorArray.push(colorScale(i)); 145 | } 146 | return colorArray; 147 | }; 148 | 149 | 150 | 151 | const floatingNumber = (value, maxDigits = 2) => { 152 | return Math.round(value * 100 * 10 ** maxDigits) / 10 ** maxDigits; 153 | }; 154 | 155 | 156 | //Returns the angle (in rad) for the given 'percent' value where percent = 1 means 100% and is 180 degree angle 157 | const percentToRad = (percent) => { 158 | return percent * Math.PI; 159 | }; -------------------------------------------------------------------------------- /dist/GaugeChart/utils.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.setArcData = exports.addText = exports.calculateRotation = exports.updateDimensions = exports.centerGraph = exports.calculateRadius = void 0; 7 | 8 | var _d = require("d3"); 9 | 10 | var calculateRadius = function calculateRadius(width, height, outerRadius, margin, g) { 11 | //The radius needs to be constrained by the containing div 12 | //Since it is a half circle we are dealing with the height of the div 13 | //Only needs to be half of the width, because the width needs to be 2 * radius 14 | //For the whole arc to fit 15 | //First check if it is the width or the height that is the "limiting" dimension 16 | if (width.current < 2 * height.current) { 17 | //Then the width limits the size of the chart 18 | //Set the radius to the width - the horizontal margins 19 | outerRadius.current = (width.current - margin.current.left - margin.current.right) / 2; 20 | } else { 21 | outerRadius.current = height.current - margin.current.top - margin.current.bottom; 22 | } 23 | 24 | centerGraph(width, g, outerRadius, margin); 25 | }; //Calculates new margins to make the graph centered 26 | 27 | 28 | exports.calculateRadius = calculateRadius; 29 | 30 | var centerGraph = function centerGraph(width, g, outerRadius, margin) { 31 | margin.current.left = width.current / 2 - outerRadius.current + margin.current.right; 32 | g.current.attr("transform", "translate(" + margin.current.left + ", " + margin.current.top + ")"); 33 | }; 34 | 35 | exports.centerGraph = centerGraph; 36 | 37 | var updateDimensions = function updateDimensions(props, container, margin, width, height) { 38 | //TODO: Fix so that the container is included in the component 39 | var marginInPercent = props.marginInPercent; 40 | var divDimensions = container.current.node().getBoundingClientRect(), 41 | divWidth = divDimensions.width, 42 | divHeight = divDimensions.height; //Set the new width and horizontal margins 43 | 44 | margin.current.left = divWidth * marginInPercent; 45 | margin.current.right = divWidth * marginInPercent; 46 | width.current = divWidth - margin.current.left - margin.current.right; 47 | margin.current.top = divHeight * marginInPercent; 48 | margin.current.bottom = divHeight * marginInPercent; 49 | height.current = width.current / 2 - margin.current.top - margin.current.bottom; //height.current = divHeight - margin.current.top - margin.current.bottom; 50 | }; 51 | 52 | exports.updateDimensions = updateDimensions; 53 | 54 | var calculateRotation = function calculateRotation(percent, outerRadius, width, needleScale) { 55 | var needleLength = outerRadius.current * needleScale, 56 | //TODO: Maybe it should be specified as a percentage of the arc radius? 57 | needleRadius = 15 * (width.current / 500), 58 | theta = percentToRad(percent), 59 | centerPoint = [0, -needleRadius / 2], 60 | topPoint = [centerPoint[0] - needleLength * Math.cos(theta), centerPoint[1] - needleLength * Math.sin(theta)], 61 | leftPoint = [centerPoint[0] - needleRadius * Math.cos(theta - Math.PI / 2), centerPoint[1] - needleRadius * Math.sin(theta - Math.PI / 2)], 62 | rightPoint = [centerPoint[0] - needleRadius * Math.cos(theta + Math.PI / 2), centerPoint[1] - needleRadius * Math.sin(theta + Math.PI / 2)]; 63 | var pathStr = "M ".concat(leftPoint[0], " ").concat(leftPoint[1], " L ").concat(topPoint[0], " ").concat(topPoint[1], " L ").concat(rightPoint[0], " ").concat(rightPoint[1]); 64 | return pathStr; 65 | }; //Adds text undeneath the graft to display which percentage is the current one 66 | 67 | 68 | exports.calculateRotation = calculateRotation; 69 | 70 | var addText = function addText(percentage, props, outerRadius, width, g) { 71 | var formatTextValue = props.formatTextValue, 72 | fontSize = props.fontSize; 73 | var textPadding = 20; 74 | var text = formatTextValue ? formatTextValue(floatingNumber(percentage)) : floatingNumber(percentage) + "%"; 75 | g.current.append("g").attr("class", "text-group").attr("transform", "translate(".concat(outerRadius.current, ", ").concat(outerRadius.current / 2 + textPadding, ")")).append("text").text(text) // this computation avoid text overflow. When formatted value is over 10 characters, we should reduce font size 76 | .style("font-size", function () { 77 | return fontSize ? fontSize : "".concat(width.current / 11 / (text.length > 10 ? text.length / 10 : 1), "px"); 78 | }).style("fill", props.textColor).style("text-anchor", "middle"); 79 | }; // This function update arc's datas when component is mounting or when one of arc's props is updated 80 | 81 | 82 | exports.addText = addText; 83 | 84 | var setArcData = function setArcData(props, nbArcsToDisplay, colorArray, arcData) { 85 | // We have to make a decision about number of arcs to display 86 | // If arcsLength is setted, we choose arcsLength length instead of nrOfLevels 87 | nbArcsToDisplay.current = props.arcsLength ? props.arcsLength.length : props.nrOfLevels; //Check if the number of colors equals the number of levels 88 | //Otherwise make an interpolation 89 | 90 | if (nbArcsToDisplay.current === props.colors.length) { 91 | colorArray.current = props.colors; 92 | } else { 93 | colorArray.current = getColors(props, nbArcsToDisplay); 94 | } //The data that is used to create the arc 95 | // Each arc could have hiw own value width arcsLength prop 96 | 97 | 98 | arcData.current = []; 99 | 100 | for (var i = 0; i < nbArcsToDisplay.current; i++) { 101 | var arcDatum = { 102 | value: props.arcsLength && props.arcsLength.length > i ? props.arcsLength[i] : 1, 103 | color: colorArray.current[i] 104 | }; 105 | arcData.current.push(arcDatum); 106 | } 107 | }; //Depending on the number of levels in the chart 108 | //This function returns the same number of colors 109 | 110 | 111 | exports.setArcData = setArcData; 112 | 113 | var getColors = function getColors(props, nbArcsToDisplay) { 114 | var colors = props.colors; 115 | var colorScale = (0, _d.scaleLinear)().domain([1, nbArcsToDisplay.current]).range([colors[0], colors[colors.length - 1]]) //Use the first and the last color as range 116 | .interpolate(_d.interpolateHsl); 117 | var colorArray = []; 118 | 119 | for (var i = 1; i <= nbArcsToDisplay.current; i++) { 120 | colorArray.push(colorScale(i)); 121 | } 122 | 123 | return colorArray; 124 | }; 125 | 126 | var floatingNumber = function floatingNumber(value) { 127 | var maxDigits = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2; 128 | return Math.round(value * 100 * Math.pow(10, maxDigits)) / Math.pow(10, maxDigits); 129 | }; //Returns the angle (in rad) for the given 'percent' value where percent = 1 means 100% and is 180 degree angle 130 | 131 | 132 | var percentToRad = function percentToRad(percent) { 133 | return percent * Math.PI; 134 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-gauge-chart 2 | React component for displaying a gauge chart, using D3.js 3 | 4 | # Usage 5 | Install it by running `npm install react-gauge-chart`. Then to use it: 6 | 7 | ```jsx 8 | import GaugeChart from 'react-gauge-chart' 9 | 10 | 11 | ``` 12 | 13 | ## Examples 14 | 15 | Check the demo below for live examples of the charts 16 | 17 | #### To create a default chart 18 | 19 | ```jsx 20 | 21 | ``` 22 | 23 | #### Chart with 20 levels and pointer at 86% 24 | 25 | ```jsx 26 | 30 | ``` 31 | 32 | #### Chart with custom colors and larger arc width 33 | 34 | ```jsx 35 | 41 | ``` 42 | 43 | #### Chart with other corner radius and larger padding between arcs 44 | 45 | ```jsx 46 | 52 | ``` 53 | 54 | #### Chart with custom arcs width 55 | 56 | ```jsx 57 | 64 | ``` 65 | 66 | #### Chart with disabled animation 67 | 68 | ```jsx 69 | 75 | ``` 76 | 77 | # Demo 78 | https://martin36.github.io/react-gauge-chart/ 79 | 80 | # API 81 | 82 | ## 83 | 84 | ### Warning: Do not use the same `id` for multiple charts, as it will put multiple charts in the same container 85 | 86 | #### Note: If you do any updates to the props of the chart, it will rerender with a different size (it's a bug). To prevent this set a fixed height for the chart e.g. 87 | 88 | ```jsx 89 | const chartStyle = { 90 | height: 250, 91 | } 92 | 93 | 94 | 95 | ``` 96 | 97 | The props for the chart: 98 | 99 | | Name | PropType | Description | Default value | 100 | |-----------------|-----------------------------|----------------------------------------------------------------|------------------------| 101 | | id | PropTypes.string.isRequired | Used for the identification of the div surrounding the chart | | 102 | | className | PropTypes.string | Add `className` to the div container | | 103 | | style | PropTypes.object | Add `style` to the div container | { width: '100%' } | 104 | | marginInPercent | PropTypes.number | Margin for the chart inside the containing SVG element | 0.05 | 105 | | cornerRadius | PropTypes.number | Corner radius for the elements in the chart | 6 | 106 | | nrOfLevels | PropTypes.number | The number of elements displayed in the arc | 3 | 107 | | percent | PropTypes.number | The number where the pointer should point to (between 0 and 1) | 0.4 | 108 | | arcPadding | PropTypes.number | The distance between the elements in the arc | 0.05 | 109 | | arcWidth | PropTypes.number | The thickness of the arc | 0.2 | 110 | | colors | PropTypes.array | An array of colors in HEX format displayed in the arc | ["#00FF00", "#FF0000"] | 111 | | textColor | PropTypes.string | The color of the text | "#FFFFFF" | 112 | | fontSize | PropTypes.string | The font size of the text | none | 113 | | needleColor | PropTypes.string | The color of the needle triangle | "#464A4F" | 114 | | needleBaseColor | PropTypes.string | The color of the circle at the base of the needle | "#464A4F" | 115 | | hideText | PropTypes.bool | Whether or not to hide the percentage display | false | 116 | | arcsLength | PropTypes.array | An array specifying the length of each individual arc. If this prop is set, the nrOfLevels prop will have no effect | none | 117 | | animate | PropTypes.bool | Whether or not to animate the needle when loaded | true | 118 | | animDelay | PropTypes.number | Delay in ms before starting the needle animation | 500 | 119 | | animateDuration | PropTypes.number | Duration in ms for the needle animation | 3000 | 120 | | formatTextValue | PropTypes.func | Format you own text value (example: value => value+'%') | null | 121 | | textComponent | PropTypes.elements | Custom text label textComponent | null | 122 | | textComponentContainerClassName | PropTypes.string | Add `className` to the text component container | | 123 | | needleScale | PropTypes.number | Needle arc cornerRadius | 0.55 | 124 | | customNeedleComponent | PropTypes.element | Custom needle component `Note: Make sure to rotate the needle as per the percentage value` | null | 125 | | customNeedleComponentClassName | PropTypes.string | Add `className` to the custom needle container | | 126 | | customNeedleStyle | PropsTypes.object | Add `style` to custom needle container div | | 127 | | 128 | 129 | ##### Colors for the chart 130 | 131 | The colors could either be specified as an array of hex color values, such as `["#FF0000", "#00FF00", "#0000FF"]` where 132 | each arc would a color in the array (colors are assigned from left to right). If that is the case, then the **length of the array** 133 | must match the **number of levels** in the arc. 134 | If the number of colors does not match the number of levels, then the **first** and the **last** color from the colors array will 135 | be selected and the arcs will get colors that are interpolated between those. The interpolation is done using [d3.interpolateHsl](https://github.com/d3/d3-interpolate#interpolateHsl). 136 | -------------------------------------------------------------------------------- /src/lib/GaugeChart/index.js: -------------------------------------------------------------------------------- 1 | import React, { 2 | useCallback, 3 | useEffect, 4 | useRef, 5 | useMemo, 6 | useLayoutEffect, 7 | } from "react"; 8 | import { 9 | arc, 10 | pie, 11 | select, 12 | } from "d3"; 13 | import PropTypes from "prop-types"; 14 | 15 | import { setArcData } from "./utils"; 16 | import { renderChart } from "./renderChart"; 17 | import { drawNeedle } from "./drawNeedle"; 18 | 19 | import useDeepCompareEffect from "./customHooks"; 20 | /* 21 | GaugeChart creates a gauge chart using D3 22 | The chart is responsive and will have the same width as the "container" 23 | The radius of the gauge depends on the width and height of the container 24 | It will use whichever is smallest of width or height 25 | The svg element surrounding the gauge will always be square 26 | "container" is the div where the chart should be placed 27 | */ 28 | 29 | //Constants 30 | const startAngle = -Math.PI / 2; //Negative x-axis 31 | const endAngle = Math.PI / 2; //Positive x-axis 32 | 33 | const defaultStyle = { 34 | width: "100%", 35 | }; 36 | 37 | // Props that should cause an animation on update 38 | const animateNeedleProps = [ 39 | "marginInPercent", 40 | "arcPadding", 41 | "percent", 42 | "nrOfLevels", 43 | "animDelay", 44 | ]; 45 | 46 | const defaultProps = { 47 | style: defaultStyle, 48 | marginInPercent: 0.05, 49 | cornerRadius: 6, 50 | nrOfLevels: 3, 51 | percent: 0.4, 52 | arcPadding: 0.05, //The padding between arcs, in rad 53 | arcWidth: 0.2, //The width of the arc given in percent of the radius 54 | colors: ["#00FF00", "#FF0000"], //Default defined colors 55 | textColor: "#fff", 56 | needleColor: "#464A4F", 57 | needleBaseColor: "#464A4F", 58 | hideText: false, 59 | animate: true, 60 | animDelay: 500, 61 | formatTextValue: null, 62 | fontSize: null, 63 | animateDuration: 3000, 64 | textComponent: undefined, 65 | needleScale: 0.55, 66 | customNeedleComponent: null, 67 | }; 68 | 69 | const GaugeChart = (initialProps) => { 70 | const props = useMemo( 71 | () => ({ ...defaultProps, ...initialProps }), 72 | [initialProps] 73 | ); 74 | const svg = useRef({}); 75 | const g = useRef({}); 76 | const width = useRef({}); 77 | const height = useRef({}); 78 | const doughnut = useRef({}); 79 | const needle = useRef({}); 80 | const outerRadius = useRef({}); 81 | const margin = useRef({}); // = {top: 20, right: 50, bottom: 50, left: 50}, 82 | const container = useRef({}); 83 | const nbArcsToDisplay = useRef(0); 84 | const colorArray = useRef([]); 85 | const arcChart = useRef(arc()); 86 | const arcData = useRef([]); 87 | const pieChart = useRef(pie()); 88 | const prevProps = useRef(props); 89 | let selectedRef = useRef({}); 90 | 91 | const initChart = useCallback( 92 | (update, resize = false, prevProps) => { 93 | if (update) { 94 | renderChart( 95 | resize, 96 | prevProps, 97 | width, 98 | margin, 99 | height, 100 | outerRadius, 101 | g, 102 | doughnut, 103 | arcChart, 104 | needle, 105 | pieChart, 106 | svg, 107 | props, 108 | container, 109 | arcData 110 | ); 111 | !customNeedleComponent && drawNeedle( 112 | resize, 113 | prevProps, 114 | props, 115 | width, 116 | needle, 117 | container, 118 | outerRadius, 119 | g 120 | ); 121 | return; 122 | } 123 | 124 | container.current.select("svg").remove(); 125 | svg.current = container.current.append("svg"); 126 | g.current = svg.current.append("g"); //Used for margins 127 | doughnut.current = g.current.append("g").attr("class", "doughnut"); 128 | 129 | //Set up the pie generator 130 | //Each arc should be of equal length (or should they?) 131 | pieChart.current 132 | .value(function (d) { 133 | return d.value; 134 | }) 135 | //.padAngle(arcPadding) 136 | .startAngle(startAngle) 137 | .endAngle(endAngle) 138 | .sort(null); 139 | //Add the needle element 140 | needle.current = g.current.append("g").attr("class", "needle"); 141 | 142 | renderChart( 143 | resize, 144 | prevProps, 145 | width, 146 | margin, 147 | height, 148 | outerRadius, 149 | g, 150 | doughnut, 151 | arcChart, 152 | needle, 153 | pieChart, 154 | svg, 155 | props, 156 | container, 157 | arcData 158 | ); 159 | 160 | !customNeedleComponent && drawNeedle( 161 | resize, 162 | prevProps, 163 | props, 164 | width, 165 | needle, 166 | container, 167 | outerRadius, 168 | g 169 | ); 170 | }, 171 | [props] 172 | ); 173 | 174 | useLayoutEffect(() => { 175 | setArcData(props, nbArcsToDisplay, colorArray, arcData); 176 | container.current = select(selectedRef); 177 | //Initialize chart 178 | initChart(); 179 | }, [props, initChart]); 180 | 181 | useDeepCompareEffect(() => { 182 | if ( 183 | props.nrOfLevels || 184 | prevProps.current.arcsLength.every((a) => props.arcsLength.includes(a)) || 185 | prevProps.current.colors.every((a) => props.colors.includes(a)) 186 | ) { 187 | setArcData(props, nbArcsToDisplay, colorArray, arcData); 188 | } 189 | //Initialize chart 190 | // Always redraw the chart, but potentially do not animate it 191 | const resize = !animateNeedleProps.some( 192 | (key) => prevProps.current[key] !== props[key] 193 | ); 194 | initChart(true, resize, prevProps.current); 195 | prevProps.current = props; 196 | }, [ 197 | props.nrOfLevels, 198 | props.arcsLength, 199 | props.colors, 200 | props.percent, 201 | props.needleColor, 202 | props.needleBaseColor, 203 | ]); 204 | 205 | useEffect(() => { 206 | const handleResize = () => { 207 | let resize = true; 208 | 209 | renderChart( 210 | resize, 211 | prevProps, 212 | width, 213 | margin, 214 | height, 215 | outerRadius, 216 | g, 217 | doughnut, 218 | arcChart, 219 | needle, 220 | pieChart, 221 | svg, 222 | props, 223 | container, 224 | arcData 225 | ); 226 | 227 | !customNeedleComponent && drawNeedle( 228 | resize, 229 | prevProps, 230 | props, 231 | width, 232 | needle, 233 | container, 234 | outerRadius, 235 | g 236 | ); 237 | }; 238 | //Set up resize event listener to re-render the chart everytime the window is resized 239 | window.addEventListener("resize", handleResize); 240 | return () => { 241 | window.removeEventListener("resize", handleResize); 242 | }; 243 | // eslint-disable-next-line react-hooks/exhaustive-deps 244 | }, [props]); 245 | 246 | const { 247 | id, 248 | style, 249 | className, 250 | textComponent, 251 | textComponentContainerClassName, 252 | customNeedleComponent, 253 | customNeedleStyle, 254 | customNeedleComponentClassName, 255 | } = props; 256 | 257 | return ( 258 |
263 |
(selectedRef = svg)}> 264 |
268 | {textComponent} 269 |
270 |
271 | {customNeedleComponent &&
272 | {customNeedleComponent} 273 |
} 274 |
275 | ); 276 | }; 277 | 278 | export default GaugeChart; 279 | 280 | GaugeChart.propTypes = { 281 | id: PropTypes.string, 282 | className: PropTypes.string, 283 | style: PropTypes.object, 284 | marginInPercent: PropTypes.number, 285 | cornerRadius: PropTypes.number, 286 | nrOfLevels: PropTypes.number, 287 | percent: PropTypes.number, 288 | arcPadding: PropTypes.number, 289 | arcWidth: PropTypes.number, 290 | arcsLength: PropTypes.array, 291 | colors: PropTypes.array, 292 | textColor: PropTypes.string, 293 | needleColor: PropTypes.string, 294 | needleBaseColor: PropTypes.string, 295 | hideText: PropTypes.bool, 296 | animate: PropTypes.bool, 297 | formatTextValue: PropTypes.func, 298 | fontSize: PropTypes.string, 299 | animateDuration: PropTypes.number, 300 | animDelay: PropTypes.number, 301 | textComponent: PropTypes.element, 302 | textComponentContainerClassName: PropTypes.string, 303 | needleScale: PropTypes.number, 304 | customNeedleComponent: PropTypes.element, 305 | customNeedleComponentClassName: PropTypes.string, 306 | customNeedleStyle: PropTypes.object 307 | }; 308 | 309 | 310 | -------------------------------------------------------------------------------- /dist/GaugeChart/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = void 0; 7 | 8 | var _react = _interopRequireWildcard(require("react")); 9 | 10 | var _d = require("d3"); 11 | 12 | var _propTypes = _interopRequireDefault(require("prop-types")); 13 | 14 | var _utils = require("./utils"); 15 | 16 | var _renderChart = require("./renderChart"); 17 | 18 | var _drawNeedle = require("./drawNeedle"); 19 | 20 | var _customHooks = _interopRequireDefault(require("./customHooks")); 21 | 22 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 23 | 24 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 25 | 26 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 27 | 28 | function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } 29 | 30 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } 31 | 32 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 33 | 34 | /* 35 | GaugeChart creates a gauge chart using D3 36 | The chart is responsive and will have the same width as the "container" 37 | The radius of the gauge depends on the width and height of the container 38 | It will use whichever is smallest of width or height 39 | The svg element surrounding the gauge will always be square 40 | "container" is the div where the chart should be placed 41 | */ 42 | //Constants 43 | var startAngle = -Math.PI / 2; //Negative x-axis 44 | 45 | var endAngle = Math.PI / 2; //Positive x-axis 46 | 47 | var defaultStyle = { 48 | width: "100%" 49 | }; // Props that should cause an animation on update 50 | 51 | var animateNeedleProps = ["marginInPercent", "arcPadding", "percent", "nrOfLevels", "animDelay"]; 52 | var defaultProps = { 53 | style: defaultStyle, 54 | marginInPercent: 0.05, 55 | cornerRadius: 6, 56 | nrOfLevels: 3, 57 | percent: 0.4, 58 | arcPadding: 0.05, 59 | //The padding between arcs, in rad 60 | arcWidth: 0.2, 61 | //The width of the arc given in percent of the radius 62 | colors: ["#00FF00", "#FF0000"], 63 | //Default defined colors 64 | textColor: "#fff", 65 | needleColor: "#464A4F", 66 | needleBaseColor: "#464A4F", 67 | hideText: false, 68 | animate: true, 69 | animDelay: 500, 70 | formatTextValue: null, 71 | fontSize: null, 72 | animateDuration: 3000, 73 | textComponent: undefined, 74 | needleScale: 0.55, 75 | customNeedleComponent: null 76 | }; 77 | 78 | var GaugeChart = function GaugeChart(initialProps) { 79 | var props = (0, _react.useMemo)(function () { 80 | return _objectSpread(_objectSpread({}, defaultProps), initialProps); 81 | }, [initialProps]); 82 | var svg = (0, _react.useRef)({}); 83 | var g = (0, _react.useRef)({}); 84 | var width = (0, _react.useRef)({}); 85 | var height = (0, _react.useRef)({}); 86 | var doughnut = (0, _react.useRef)({}); 87 | var needle = (0, _react.useRef)({}); 88 | var outerRadius = (0, _react.useRef)({}); 89 | var margin = (0, _react.useRef)({}); // = {top: 20, right: 50, bottom: 50, left: 50}, 90 | 91 | var container = (0, _react.useRef)({}); 92 | var nbArcsToDisplay = (0, _react.useRef)(0); 93 | var colorArray = (0, _react.useRef)([]); 94 | var arcChart = (0, _react.useRef)((0, _d.arc)()); 95 | var arcData = (0, _react.useRef)([]); 96 | var pieChart = (0, _react.useRef)((0, _d.pie)()); 97 | var prevProps = (0, _react.useRef)(props); 98 | var selectedRef = (0, _react.useRef)({}); 99 | var initChart = (0, _react.useCallback)(function (update) { 100 | var resize = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; 101 | var prevProps = arguments.length > 2 ? arguments[2] : undefined; 102 | 103 | if (update) { 104 | (0, _renderChart.renderChart)(resize, prevProps, width, margin, height, outerRadius, g, doughnut, arcChart, needle, pieChart, svg, props, container, arcData); 105 | !customNeedleComponent && (0, _drawNeedle.drawNeedle)(resize, prevProps, props, width, needle, container, outerRadius, g); 106 | return; 107 | } 108 | 109 | container.current.select("svg").remove(); 110 | svg.current = container.current.append("svg"); 111 | g.current = svg.current.append("g"); //Used for margins 112 | 113 | doughnut.current = g.current.append("g").attr("class", "doughnut"); //Set up the pie generator 114 | //Each arc should be of equal length (or should they?) 115 | 116 | pieChart.current.value(function (d) { 117 | return d.value; 118 | }) //.padAngle(arcPadding) 119 | .startAngle(startAngle).endAngle(endAngle).sort(null); //Add the needle element 120 | 121 | needle.current = g.current.append("g").attr("class", "needle"); 122 | (0, _renderChart.renderChart)(resize, prevProps, width, margin, height, outerRadius, g, doughnut, arcChart, needle, pieChart, svg, props, container, arcData); 123 | !customNeedleComponent && (0, _drawNeedle.drawNeedle)(resize, prevProps, props, width, needle, container, outerRadius, g); 124 | }, [props]); 125 | (0, _react.useLayoutEffect)(function () { 126 | (0, _utils.setArcData)(props, nbArcsToDisplay, colorArray, arcData); 127 | container.current = (0, _d.select)(selectedRef); //Initialize chart 128 | 129 | initChart(); 130 | }, [props, initChart]); 131 | (0, _customHooks.default)(function () { 132 | if (props.nrOfLevels || prevProps.current.arcsLength.every(function (a) { 133 | return props.arcsLength.includes(a); 134 | }) || prevProps.current.colors.every(function (a) { 135 | return props.colors.includes(a); 136 | })) { 137 | (0, _utils.setArcData)(props, nbArcsToDisplay, colorArray, arcData); 138 | } //Initialize chart 139 | // Always redraw the chart, but potentially do not animate it 140 | 141 | 142 | var resize = !animateNeedleProps.some(function (key) { 143 | return prevProps.current[key] !== props[key]; 144 | }); 145 | initChart(true, resize, prevProps.current); 146 | prevProps.current = props; 147 | }, [props.nrOfLevels, props.arcsLength, props.colors, props.percent, props.needleColor, props.needleBaseColor]); 148 | (0, _react.useEffect)(function () { 149 | var handleResize = function handleResize() { 150 | var resize = true; 151 | (0, _renderChart.renderChart)(resize, prevProps, width, margin, height, outerRadius, g, doughnut, arcChart, needle, pieChart, svg, props, container, arcData); 152 | !customNeedleComponent && (0, _drawNeedle.drawNeedle)(resize, prevProps, props, width, needle, container, outerRadius, g); 153 | }; //Set up resize event listener to re-render the chart everytime the window is resized 154 | 155 | 156 | window.addEventListener("resize", handleResize); 157 | return function () { 158 | window.removeEventListener("resize", handleResize); 159 | }; // eslint-disable-next-line react-hooks/exhaustive-deps 160 | }, [props]); 161 | var id = props.id, 162 | style = props.style, 163 | className = props.className, 164 | textComponent = props.textComponent, 165 | textComponentContainerClassName = props.textComponentContainerClassName, 166 | customNeedleComponent = props.customNeedleComponent, 167 | customNeedleStyle = props.customNeedleStyle, 168 | customNeedleComponentClassName = props.customNeedleComponentClassName; 169 | return /*#__PURE__*/_react.default.createElement("div", { 170 | id: id, 171 | className: className, 172 | style: style 173 | }, /*#__PURE__*/_react.default.createElement("div", { 174 | ref: function ref(svg) { 175 | return selectedRef = svg; 176 | } 177 | }, /*#__PURE__*/_react.default.createElement("div", { 178 | className: textComponentContainerClassName, 179 | style: { 180 | position: "relative", 181 | top: "50%" 182 | } 183 | }, textComponent)), customNeedleComponent && /*#__PURE__*/_react.default.createElement("div", { 184 | className: customNeedleComponentClassName, 185 | style: _objectSpread({ 186 | position: "relative" 187 | }, customNeedleStyle) 188 | }, customNeedleComponent)); 189 | }; 190 | 191 | var _default = GaugeChart; 192 | exports.default = _default; 193 | GaugeChart.propTypes = { 194 | id: _propTypes.default.string, 195 | className: _propTypes.default.string, 196 | style: _propTypes.default.object, 197 | marginInPercent: _propTypes.default.number, 198 | cornerRadius: _propTypes.default.number, 199 | nrOfLevels: _propTypes.default.number, 200 | percent: _propTypes.default.number, 201 | arcPadding: _propTypes.default.number, 202 | arcWidth: _propTypes.default.number, 203 | arcsLength: _propTypes.default.array, 204 | colors: _propTypes.default.array, 205 | textColor: _propTypes.default.string, 206 | needleColor: _propTypes.default.string, 207 | needleBaseColor: _propTypes.default.string, 208 | hideText: _propTypes.default.bool, 209 | animate: _propTypes.default.bool, 210 | formatTextValue: _propTypes.default.func, 211 | fontSize: _propTypes.default.string, 212 | animateDuration: _propTypes.default.number, 213 | animDelay: _propTypes.default.number, 214 | textComponent: _propTypes.default.element, 215 | textComponentContainerClassName: _propTypes.default.string, 216 | needleScale: _propTypes.default.number, 217 | customNeedleComponent: _propTypes.default.element, 218 | customNeedleComponentClassName: _propTypes.default.string, 219 | customNeedleStyle: _propTypes.default.object 220 | }; --------------------------------------------------------------------------------