├── 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 |
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 |
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 | };
--------------------------------------------------------------------------------