├── .github
└── workflows
│ └── workflow.yml
├── .gitignore
├── .npmignore
├── .nvmrc
├── .prettierignore
├── .scripts
├── get_gh_pages_url.js
└── publish_storybook.sh
├── .storybook
├── main.mjs
├── manager.js
├── preview-head.html
└── preview.mjs
├── .vscode
└── settings.json
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── cypress.config.ts
├── cypress
├── fixtures
│ └── example.json
├── plugins
│ └── index.js
└── support
│ ├── commands.js
│ ├── commands.ts
│ ├── component-index.html
│ ├── component.ts
│ ├── e2e.ts
│ └── index.js
├── jest.config.js
├── package.json
├── pnpm-lock.yaml
├── prettier.config.js
├── setupVitest.js
├── slim.config.js
├── speedo.gif
├── src
├── __tests__
│ ├── ForceRender.spec.jsx
│ ├── ReactSpeedometer.spec.jsx
│ ├── ReactSpeedometer.test.jsx
│ └── utils.test.jsx
├── core
│ ├── config
│ │ ├── configure.js
│ │ └── index.js
│ ├── enums.js
│ ├── render
│ │ └── index.js
│ ├── theme
│ │ └── index.js
│ ├── types
│ │ └── index.d.ts
│ └── util
│ │ ├── get-needle-transition.js
│ │ ├── index.js
│ │ └── index.test.js
├── index.d.ts
├── index.jsx
└── stories
│ ├── ReactSpeedometer.stories.jsx
│ ├── auto-refresh.jsx
│ ├── multi-speedometers.jsx
│ └── speedo-button.jsx
├── vite.config.js
└── vitest.config.js
/.github/workflows/workflow.yml:
--------------------------------------------------------------------------------
1 | name: Test Coverage
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | branches: [master]
8 |
9 | env:
10 | CYPRESS_CACHE_FOLDER: /home/runner/.cache/Cypress
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | node-version: [20.x]
19 |
20 | steps:
21 | - name: Checkout repository
22 | uses: actions/checkout@v4
23 |
24 | - name: Set pnpm as package manager
25 | uses: pnpm/action-setup@v4
26 |
27 | - name: Set up Node.js ${{ matrix.node-version }}
28 | uses: actions/setup-node@v4
29 | with:
30 | node-version: ${{ matrix.node-version }}
31 | cache: 'pnpm'
32 |
33 | - name: Get pnpm store directory
34 | shell: bash
35 | run: |
36 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
37 |
38 | - name: Setup pnpm cache
39 | uses: actions/cache@v4
40 | with:
41 | path: |
42 | ${{ env.STORE_PATH }}
43 | node_modules
44 | /home/runner/.cache/Cypress
45 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
46 | restore-keys: |
47 | ${{ runner.os }}-pnpm-store-
48 |
49 | - name: Install dependencies
50 | run: pnpm install --frozen-lockfile
51 |
52 | - name: Cypress install
53 | run: pnpm cypress install
54 |
55 | # ref: https://github.com/cypress-io/github-action
56 | - name: Setup cypress
57 | uses: cypress-io/github-action@v6
58 | with:
59 | install: false
60 | runTests: false
61 | browser: chrome
62 | headless: true
63 | # cache-key: cypress-${{ runner.os }}-cypress-${{ hashFiles('pnpm-lock.yaml') }}
64 | cache-key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
65 |
66 | - name: Run the tests and generate coverage report
67 | run: pnpm run full-test
68 |
69 | - name: Upload coverage to Codecov
70 | uses: codecov/codecov-action@v2
71 |
72 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | *.log
3 | .idea
4 | dist
5 | *.sublime-*
6 | .DS_Store
7 | *.code-workspace
8 | # jest coverage folder
9 | coverage
10 |
11 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | lib
2 | src
3 | stories
4 | .storybook
5 |
6 | .scripts
7 |
8 | .git
9 | .gitmodules
10 |
11 | .babelrc
12 | *.gif
13 | *.code-workspace
14 | .DS_Store
15 |
16 | *.config.js
17 |
18 | .prettierignore
19 | .prettierrc
20 |
21 | .travis.yml
22 |
23 | yarn.lock
24 | yarn-error.log
25 |
26 | .nvmrc
27 | .eslintrc
28 |
29 | .vscode/
30 |
31 | *.md
32 |
33 | # coverage files
34 | coverage
35 |
36 | CHANGELOG
37 | .github
38 | .circleci
39 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | lts/*
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /package.json
2 |
--------------------------------------------------------------------------------
/.scripts/get_gh_pages_url.js:
--------------------------------------------------------------------------------
1 | // IMPORTANT
2 | // ---------
3 | // This is an auto generated file with React CDK.
4 | // Do not modify this file.
5 |
6 | const parse = require('git-url-parse');
7 | var ghUrl = process.argv[2];
8 | const parsedUrl = parse(ghUrl);
9 |
10 | const ghPagesUrl = 'https://' + parsedUrl.owner + '.github.io/' + parsedUrl.name;
11 | console.log(ghPagesUrl);
12 |
--------------------------------------------------------------------------------
/.scripts/publish_storybook.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # IMPORTANT
4 | # ---------
5 | # This is an auto generated file with React CDK.
6 | # Do not modify this file.
7 |
8 | set -e # exit with nonzero exit code if anything fails
9 |
10 | # get GIT url
11 |
12 | GIT_URL=$(git config --get remote.origin.url)
13 | if [[ $GIT_URL == "" ]]; then
14 | echo "This project is not configured with a remote git repo".
15 | exit 1
16 | fi
17 |
18 | # clear and re-create the out directory
19 | rm -rf .out || exit 0;
20 | mkdir .out;
21 |
22 | # run our compile script, discussed above
23 | # build-storybook -o .out
24 | pnpm run build-storybook
25 |
26 | # go to the out directory and create a *new* Git repo
27 | cd .out
28 | git init
29 |
30 | # inside this git repo we'll pretend to be a new user
31 | git config user.name "palerdot GH Pages Bot"
32 | git config user.email "ghpages-bot@palerdot.in"
33 |
34 | # The first and only commit to this new Git repo contains all the
35 | # files present with the commit message "Deploy to GitHub Pages".
36 | git add .
37 | git commit -m "Deploy Storybook to GitHub Pages"
38 |
39 | # Force push from the current repo's master branch to the remote
40 | # repo's gh-pages branch. (All previous history on the gh-pages branch
41 | # will be lost, since we are overwriting it.) We redirect any output to
42 | # /dev/null to hide any sensitive credential data that might otherwise be exposed.
43 | git push --force --quiet $GIT_URL master:gh-pages > /dev/null 2>&1
44 | cd ..
45 | rm -rf .out
46 |
47 | echo ""
48 | echo "=> Storybook deployed to: $(node .scripts/get_gh_pages_url.js $GIT_URL)"
49 |
--------------------------------------------------------------------------------
/.storybook/main.mjs:
--------------------------------------------------------------------------------
1 | export default {
2 | features: {},
3 |
4 | // async viteFinal(config, { configType }) {
5 | // // customize the Vite config here
6 | // // https://github.com/storybookjs/builder-vite/issues/237#issuecomment-1047819614
7 | // const { dirname } = require('path')
8 | // // https://github.com/eirslett/storybook-builder-vite/issues/55
9 | // config.root = dirname(require.resolve('@storybook/builder-vite'))
10 |
11 | // return mergeConfig(config, {
12 | // define: {
13 | // ...config.define,
14 | // global: 'window',
15 | // },
16 | // })
17 | // },
18 |
19 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
20 |
21 | addons: [
22 | // '@storybook/addon-links',
23 | '@storybook/addon-essentials',
24 | // '@storybook/addon-interactions',
25 | ],
26 |
27 | framework: {
28 | name: '@storybook/react-vite',
29 | options: {},
30 | },
31 |
32 | core: {
33 | // ref: https://storybook.js.org/docs/react/configure/telemetry
34 | // 👈 Disables telemetry
35 | disableTelemetry: true,
36 | },
37 |
38 | docs: {},
39 |
40 | // typescript: {
41 | // reactDocgen: 'react-docgen-typescript',
42 | // },
43 | }
44 |
--------------------------------------------------------------------------------
/.storybook/manager.js:
--------------------------------------------------------------------------------
1 | import { addons } from '@storybook/manager-api'
2 | import { themes, create } from '@storybook/theming'
3 |
4 | import speedoTheme from '../src/core/theme'
5 |
6 | const theme = create(speedoTheme)
7 | const finalTheme = {
8 | ...themes.dark,
9 | ...theme,
10 | }
11 |
12 | addons.setConfig({
13 | showPanel: true,
14 | // theme: themes.dark,
15 | theme: finalTheme,
16 | })
17 |
--------------------------------------------------------------------------------
/.storybook/preview-head.html:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/.storybook/preview.mjs:
--------------------------------------------------------------------------------
1 | export const parameters = {}
2 | export const tags = ['autodocs']
3 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.enable": false
3 | }
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## `v3.1.0` / `v3.1.1`
4 |
5 | - Public release for `React 19`
6 |
7 | ## `v3.0.0-rc.0`
8 |
9 | - `React 19 rc` support and `Next 15` support
10 |
11 | ## 2.2.1
12 | - Add `types` export to package.json (https://github.com/palerdot/react-d3-speedometer/pull/182)
13 |
14 | ## 2.2.0
15 | - `React 18` compatible `v2.0` version generally available
16 |
17 | ## 2.1.0-rc.0 (`next`, `React 18`)
18 | - `segmentValueFormatter` function prop for customizing segment values.
19 |
20 | ## 1.0.2
21 | - `svgAriaLabel` prop for accessibility. More details - https://github.com/palerdot/react-d3-speedometer/issues/135
22 |
23 | ## 1.0.1
24 | - `PropTypes` moved to `dependencies` from `peerDependencies`. ref: https://github.com/facebook/prop-types#how-to-depend-on-this-package. sandbox: https://codesandbox.io/s/kind-breeze-esuge?file=/src/App.js
25 |
26 | ## 1.0.0 (v17 React support)
27 | - `React v17` and `v6 d3` support. **No Breaking Changes.**
28 |
29 | ## 0.14.1
30 | - `Typescript` types for `valueTextFontWeight` config/prop
31 |
32 | ## 0.14.0
33 | - `valueTextFontWeight` config/prop to control font weight of current value. ref: https://codesandbox.io/s/eloquent-morning-mnysk?file=/src/App.js
34 | - *dev changes*: prettier changes: `singleQuote: true`, `arrowParens: avoid`. `prettier` upgraded to `v2`
35 | - *dev changes*: `jest v26`
36 |
37 | ## 1.0.0-rc.0
38 | - First `1.0` Release candidate with `React 17` support
39 |
40 | ## 0.13.1
41 | - ignore `coverage` folder from npm bundle
42 |
43 | ## 0.13.0
44 | - `CustomSegmentLabelPosition`, `Transition` types for both Typescript and JS. Resolves https://github.com/palerdot/react-d3-speedometer/issues/81
45 | - `100%` Test coverage
46 | - `codecov`, `github actions` integration
47 |
48 | ## 0.12.0
49 | - removed `@babel/runtime-corejs2` as dependency. ref: https://github.com/palerdot/react-d3-speedometer/issues/76
50 |
51 | ## 0.11.0
52 | - migrated to `lodash-es` from `lodash` for better tree shaking. Exporting `types` and `themes` from core for better reusablility.
53 | - removed `lodash` dependency. ref: https://codesandbox.io/s/zen-darkness-c3ev3, https://codesandbox.io/s/gracious-swanson-1rts8
54 |
55 | ## 0.10.1
56 | - `npmignore` to reduce npm tarball/package size. Linking to github image asset for README
57 |
58 | ## 0.10.0
59 | - `customSegmentLabels` prop to display custom labels. [Live Example](https://palerdot.in/react-d3-speedometer/?path=/story/reactspeedometer--custom-segment-labels)
60 | - *bugfix*. Fixed https://github.com/palerdot/react-d3-speedometer/issues/68
61 |
62 | ## 0.9.0
63 | - `Typescript` support with typescript definition file
64 |
65 | ## 0.8.0
66 | - `paddingHorizontal`, `paddingVertical` props to configure space for label texts of bigger font sizes.
67 |
68 | ## 0.7.3
69 | - `dimensionUnit` prop to configure `width/height` ... Defaults to `px`. More context - https://developer.mozilla.org/en-US/docs/Web/SVG/Content_type#Length
70 |
71 | ## 0.7.2
72 | - `labelFontSize` and `valueTextFontSize` configurable props.
73 |
74 | ## 0.7.1
75 | - `bugfix`. Handle negative values and custom min/max values for `customSegmentStops`. Fixes - https://github.com/palerdot/react-d3-speedometer/issues/51 and https://github.com/palerdot/react-d3-speedometer/issues/52. ref - https://codesandbox.io/s/jolly-thompson-2k3d7
76 |
77 | ## 0.7.0
78 | - new `customSegmentStops` configuration to customize segments, ref: https://codesandbox.io/s/purple-cdn-eu9xf. **Complete rewrite** of core, where core is modularized and separated from lifecycle methods
79 |
80 | ## 0.6.1
81 | - removed unwanted `global` package from dependency. ref: - https://codesandbox.io/s/billowing-lake-9somf
82 |
83 | ## 0.6.0
84 | - new option/prop `segmentColors` for giving custom segment colors. Reference - https://palerdot.in/react-d3-speedometer/?path=/story/react-d3-speedometer--custom-segment-colors. ref: - https://codesandbox.io/s/relaxed-silence-c3qkb
85 |
86 | ## 0.5.6
87 | - `maxSegmentLabels` now takes `0` as valid value. Addresses - https://github.com/palerdot/react-d3-speedometer/issues/43
88 |
89 | ## 0.5.5
90 | - `babel 7` runtime issue fixed. Affected versions - `0.5.0` to `0.5.4`
91 |
92 | ## 0.5.0
93 | - `maxSegmentLabels` prop to limit labels for segment. Useful for displaying gradient like effect. Example - https://palerdot.in/react-d3-speedometer/?path=/story/react-d3-speedometer--gradient-effect-with-large-number-of-segments-and-maxsegmentlabels-config
94 | - `currentValuePlaceholderStyle` to configure custom placeholder for current value. Example - https://palerdot.in/react-d3-speedometer/?path=/story/react-d3-speedometer--custom-current-value-placeholder-style-for-eg-value
95 |
96 | ## 0.4.2
97 | - `needleHeightRatio` for controlling the height of the needle. Takes a float between 0 and 1 and controls the height.
98 |
99 | ## 0.4.1
100 | - Handle `d3` ticks behaviour when we get a single tick. Refer https://github.com/d3/d3-scale/issues/149 for more info
101 |
102 | ## 0.4.0
103 | - Bumped d3 version to 5.x
104 | - Handled specific `d3-scale` behaviour (https://github.com/d3/d3-scale/issues/149). Fixes https://github.com/palerdot/react-d3-speedometer/issues/27
105 |
106 | ## 0.3.3
107 | - Handling invalid `needleTransition` prop. Switching to default `easeQuadInOut` by throwing a warning.
108 |
109 | ## 0.3.2
110 | - Using simple string replace for `currentValueText` instead of template literal for better support for IE - https://caniuse.com/#feat=template-literals
111 |
112 | ## 0.3.1
113 | - updating `peerDependencies` with `React 16`
114 |
115 | ## 0.3.0
116 | - `REACT 16` updating react version to 16, along with enzyme to version 3.
117 |
118 | ## 0.2.3
119 | - `currentValueText` configuration to display custom current value text.
120 |
121 | ## 0.2.2
122 | - `valueFormat` option for formatting the values. Should be a valid input for - https://github.com/d3/d3-format#locale_format
123 |
124 | ## 0.2.1
125 | - Tweaked the positioning of current value element to be 23 points below so that it is legible in smaller speedometers.
126 |
127 | ## 0.2.0
128 | - `forceRender` config option, to rerender the whole component on props change. Previously, only the values are updated and animated.
129 |
130 | ## 0.1.11
131 | - new test case for custom `textColor` prop
132 |
133 | ## 0.1.10
134 | - new test case for validating default color
135 |
136 | ## 0.1.9
137 | - new test case for checking number of segments
138 |
139 | ## 0.1.8
140 | - configuring `textColor`
141 |
142 | ## 0.1.7
143 | - Basic test coverage using enzyme
144 |
145 | ## 0.1.5, 0.1.6
146 |
147 | - Moving to `MIT` license
148 |
149 | ## 0.1.4
150 |
151 | - Make `needleTransitionDuration` configurable
152 | - Make `needleTransition` configurable
153 | - make `ringWidth` configurable
154 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to React D3 Speedometer Component
2 |
3 | We welcome your help to make this component better. This document will help to streamline the contributing process and save everyone's precious time.
4 |
5 | ## Development Setup
6 |
7 | This component has been setup with [React CDK](https://github.com/kadirahq/react-cdk). Refer [React CDK documentation](https://github.com/kadirahq/react-cdk)) to get started with the development.
8 |
9 | ### Style
10 |
11 | This project uses `4 spaces` for indentation on top of the `airbnb` style.
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Arun Kumar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-d3-speedometer
2 |
3 | > React library for showing speedometer like gauge using d3.
4 |
5 |
6 | 
7 |
8 | [](https://codecov.io/gh/palerdot/react-d3-speedometer)
9 | [](https://www.npmjs.com/package/react-d3-speedometer)
10 | [](https://www.npmjs.com/package/react-d3-speedometer)
11 | [](https://github.com/prettier/prettier)
12 |
13 |
14 | **Note**: `v3.x` is compatible with `React v19` (and `Next 15`). `v2.x` is compatible with `React v18`. `v1.x` is compatible with `React 17`. Please use latest `v0.x` (`v0.14.1` at the time of writing) if you are using `React 16`.
15 |
16 | [](https://npmjs.org/package/react-d3-speedometer)
17 |
18 |
19 | # `react-d3-speedometer`
20 |
21 | - [Getting Started](#getting-started)
22 | - [Configuration Options](#configuration-options)
23 | - [Examples](#examples)
24 | - [FAQ](#faq)
25 | - [Ports](#ports)
26 |
27 | ## Getting Started:
28 |
29 | Install with `yarn` or `npm`.
30 |
31 | **Yarn:**
32 | ```
33 | yarn add react-d3-speedometer
34 | ```
35 |
36 | **npm:**
37 |
38 | ```
39 | npm install --save react-d3-speedometer
40 | ```
41 |
42 | And, use it like
43 |
44 | ```javascript
45 | // import the component
46 | import ReactSpeedometer from "react-d3-speedometer"
47 | // and just use it
48 |
49 | ```
50 |
51 | ### Slim Build:
52 |
53 | There is a `Slim` build available without bundling `d3`. This project uses `d3` *micro bundles*. If your project also uses `d3` *microbundles*, you can opt for **slim build**. Necessary `d3` dependencies required for slim build to work are - `d3-array`, `d3-color`, `d3-ease`, `d3-format`, `d3-interpolate`, `d3-scale`, `d3-selection`, `d3-shape`, `d3-transition`.
54 | ```javascript
55 | // sample slim build usage
56 | import ReactSpeedometer from "react-d3-speedometer/slim"
57 | // and use it
58 |
59 | ```
60 |
61 | [Live Examples](https://palerdot.in/react-d3-speedometer)
62 |
63 | ## Configuration Options:
64 |
65 | | prop | type | default | comments |
66 | | ------------|:--------------:| --------:| ---------|
67 | | value | Number | 0 | Make sure your value is between your `minValue` and `maxValue` |
68 | | minValue | Number | 0 | |
69 | | maxValue | Number | 1000 | |
70 | | segments | Number | 5 | Number of segments in the speedometer. Please note, `segments` is calculated with [d3-ticks]() which is an approximate count that is uniformly spaced between min and max. Please refer to [d3-ticks](https://github.com/d3/d3-scale/blob/master/README.md#continuous_ticks) and [d3-array ticks](https://github.com/d3/d3-array#ticks) for more detailed info. |
71 | | maxSegmentLabels | Number | value from 'segments' prop | Limit the number of segment labels to displayed. This is useful for acheiving a gradient effect by giving arbitrary large number of `segments` and limiting the labels with this prop. [See Live Example](https://palerdot.in/react-d3-speedometer/?path=/story/react-d3-speedometer--gradient-effect-with-large-number-of-segments-and-maxsegmentlabels-config). Please note, `maxSegmentLabels` is calculated with [d3-ticks]() which is an approximate count that is uniformly spaced between min and max. Please refer to [d3-ticks](https://github.com/d3/d3-scale/blob/master/README.md#continuous_ticks) and [d3-array ticks](https://github.com/d3/d3-array#ticks) for more detailed info. |
72 | | forceRender | Boolean | false | After initial rendering/mounting, when props change, only the `value` is changed and animated to maintain smooth visualization. But, if you want to force rerender the whole component like change in segments, colors, dimensions etc, you can use this option to force rerender of the whole component on props change. |
73 | | width | Number | 300 | **diameter** of the speedometer and the **width** of the svg element |
74 | | height | Number | 300 | height of the svg element. Height of the speedometer is always half the width since it is a **semi-circle**. For fluid width, please refere to `fluidWidth` config |
75 | | dimensionUnit | String | px | Default to `px` for `width/height`. Possible values - `"em" , "ex" , "px" , "in" , "cm" , "mm" , "pt" , ,"pc"` ... Please refer to [specification](https://developer.mozilla.org/en-US/docs/Web/SVG/Content_type#Length) for more details |
76 | | fluidWidth | Boolean | false | If `true` takes the width of the parent component. See [Live Example](https://palerdot.in/react-d3-speedometer/?selectedKind=React%20d3%20Speedometer&selectedStory=Fluid%20Width%20view&full=0&down=0&left=1&panelRight=0&downPanel=kadirahq%2Fstorybook-addon-actions%2Factions-panel) for more details |
77 | | needleColor | String | steelblue | Should be a valid color code - colorname, hexadecimal name or rgb value. Should be a valid input for [d3.interpolateHsl](https://github.com/d3/d3-interpolate#interpolateHsl) |
78 | | startColor | String | #FF471A | Should be a valid color code - colorname, hexadecimal name or rgb value. Should be a valid input for [d3.interpolateHsl](https://github.com/d3/d3-interpolate#interpolateHsl) |
79 | | endColor | String | #33CC33 | Should be a valid color code - colorname, hexadecimal name or rgb value. Should be a valid input for [d3.interpolateHsl](https://github.com/d3/d3-interpolate#interpolateHsl) |
80 | | segmentColors | Array (of colors) | [] | Custom segment colors can be given with this option. Should be an array of valid color codes. If this option is given **startColor** and **endColor** options will be ignored. |
81 | | needleTransition | String (JS) / Transition (TS) | easeQuadInOut | [d3-easing-identifiers](https://github.com/d3/d3-ease) - easeLinear, easeQuadIn, easeQuadOut, easeQuadInOut, easeCubicIn, easeCubicOut, easeCubicInOut, easePolyIn, easePolyOut, easePolyInOut, easeSinIn, easeSinOut, easeSinInOut, easeExpIn, easeExpOut, easeExpInOut, easeCircleIn, easeCircleOut, easeCircleInOut, easeBounceIn, easeBounceOut, easeBounceInOut, easeBackIn, easeBackOut, easeBackInOut, easeElasticIn, easeElasticOut, easeElasticInOut, easeElastic. There is a helper Object/Type 'Transtion', which you can import like `import { Transition } from 'react-d3-speedometer'` and use it like `Transition.easeElastic`. This works for both JS and Typescript. For `type(script)` definitions, please refer [here](./src/index.d.ts). |
82 | | needleTransitionDuration | Number | 500 | Time in milliseconds. |
83 | | needleHeightRatio | Float (between 0 and 1) | 0.9 | Control the height of the needle by giving a number/float between `0` and `1`. Default height ratio is `0.9`. |
84 | | ringWidth | Number | 60 | Width of the speedometer ring. |
85 | | textColor | String | #666 | Should be a valid color code - colorname, hexadecimal name or rgb value. Used for both showing the current value and the segment values |
86 | | valueFormat | String | | should be a valid format for [d3-format](https://github.com/d3/d3-format#locale_format). By default, no formatter is used. You can use a valid d3 format identifier (for eg: `d` to convert float to integers), to format the values. **Note:** This formatter affects all the values (current value, segment values) displayed in the speedometer |
87 | | segmentValueFormatter | Function | value => value | Custom segment values formatter function. This function is applied after 'valueFormat' prop if present. |
88 | | currentValueText | String | ${value} | Should be provided a string which should have **${value}** placeholder which will be replaced with current value. By default, current value is shown (formatted with `valueFormat`). For example, if current Value is 333 if you would like to show `Current Value: 333`, you should provide a string **`Current Value: ${value}`**. See [Live Example](https://palerdot.in/react-d3-speedometer/?selectedKind=react-d3-speedometer&selectedStory=Custom%20Current%20Value%20Text&full=0&down=1&left=1&panelRight=0) |
89 | | currentValuePlaceholderStyle | String | ${value} | Should be provided a placeholder string which will be replaced with current value in `currentValueTextProp`. For example: you can use ruby like interpolation by giving following props - ``. This is also helpful if you face `no-template-curly-in-string` eslint warnings and would like to use different placeholder for current value |
90 | | customSegmentStops | Array | [] | Array of values **starting** at `min` value, and **ending** at `max` value. This configuration is useful if you would like to split the segments at custom points or have unequal segments at preferred values. If the values does not begin and end with `min` and `max` value respectively, an error will be thrown. This configuration will override `segments` prop, since total number of segments will be `length - 1` of `customSegmentProps`. For example, `[0, 50, 75, 100]` value will have three segments - `0-50`, `50-75`, `75-100`. See [Live Example](https://palerdot.in/react-d3-speedometer/?path=/story/react-d3-speedometer--custom-segment-stops) |
91 | | customSegmentLabels | Array`` | [] | Takes an array of `CustomSegmentLabel` objects. Please note, the array length should match `segments` which is automatically calculated by `d3-ticks`. For more details, please check `segment` prop section. Each object has following keys for custom rendering of labels - `text`, `fontSize`, `color`, `position: OUTSIDE/INSIDE`. For `position`, there is a helper `CustomSegmentLabelPosition` Object/Type which you can import like `import { CustomSegmentLabelPosition } from 'react-d3-speedometer'`, and use it like `CustomSegmentLabelPosition.Inside / CustomSegmentLabelPosition.Outside`. This works for both JS and Typescript. For `type(script)` definitions, please refer [here](./src/index.d.ts). |
92 | | labelFontSize | String | 14px | Font size for segment labels/legends |
93 | | valueTextFontSize | String | 16px | Font size for current value text |
94 | | valueTextFontWeight | String | bold | Font weight for current value text. Any valid font weight identifier (500, bold etc) can be used. |
95 | | paddingHorizontal | Number | 0 | Provides right/left space for the label text. Takes a number (without explicit unit, unit will be taken from dimensionUnit config which defaults to px). Helpful when using a bigger font size for label texts. |
96 | | paddingVertical | Number | 0 | Provides top/bottom space for the current value label text below the needle. Takes a number (without explicit unit, unit will be taken from dimensionUnit config which defaults to px). Helpful when using a bigger font size for label texts. |
97 | | svgAriaLabel | String | React d3 speedometer | SVG aria-label property for Accessibility purposes |
98 |
99 | ## Examples
100 |
101 | You can view [Live Examples here](https://palerdot.in/react-d3-speedometer/?path=/story/react-d3-speedometer--default-with-no-config)
102 |
103 | #### Default with no config - [Live Example](https://palerdot.in/react-d3-speedometer/?path=/story/reactspeedometer--default-with-no-config)
104 |
105 | ```javascript
106 |
107 | ```
108 |
109 | #### With configurations - [Live Example](https://palerdot.in/react-d3-speedometer/?path=/story/reactspeedometer--configuring-values)
110 |
111 | ```javascript
112 |
120 | ```
121 |
122 | #### Custom Segment Colors - [Live Example](https://palerdot.in/react-d3-speedometer/?path=/story/reactspeedometer--custom-segment-colors)
123 |
124 | ```javascript
125 |
138 | ```
139 |
140 | #### Custom Segment Labels - [Live Example](https://palerdot.in/react-d3-speedometer/?path=/story/reactspeedometer--custom-segment-labels)
141 |
142 | ```javascript
143 | // 'customSegmentLabels' prop takes an array of 'CustomSegmentLabel' Object
144 | /*
145 | type CustomSegmentLabel = {
146 | text?: string
147 | position?: OUTSIDE/INSIDE
148 | fontSize?: string
149 | color?: string
150 | }
151 | */
152 |
153 |
185 | ```
186 |
187 | #### Custom Segment Stops - [Live Example](https://palerdot.in/react-d3-speedometer/?path=/story/reactspeedometer--custom-segment-stops)
188 |
189 | ```javascript
190 |
195 | // `segments` prop will be ignored since it will be calculated from `customSegmentStops`
196 | // In this case there will be `4` segments (0-500, 500-750, 750-900, 900-1000)
197 | />
198 | ```
199 |
200 | #### Fluid Width Example - [Live Example](https://palerdot.in/react-d3-speedometer/?path=/story/reactspeedometer--fluid-width-view)
201 |
202 | ```javascript
203 | // Speedometer will take the width of the parent div (500)
204 | // any width passed will be ignored
205 |
210 |
217 |
218 | ```
219 |
220 | #### Needle Transition Example - [Live Example](https://palerdot.in/react-d3-speedometer/?path=/story/reactspeedometer--needle-transition-duration)
221 |
222 | ```javascript
223 |
229 | ```
230 |
231 | #### Force Render component on props change - [Live Example](https://palerdot.in/react-d3-speedometer/?path=/story/reactspeedometer--force-render-the-component)
232 |
233 | ```javascript
234 | // By default, when props change, only the value prop is updated and animated.
235 | // This is to maintain smooth visualization and to ignore breaking appearance changes like segments, colors etc.
236 | // You can override this behaviour by giving forceRender: true
237 |
238 | // render a component initially
239 |
243 | // Now, if given forceRender: true, and change the appearance all together, the component will rerender completely on props change
244 |
250 | ```
251 |
252 | #### Needle Height Configuration Example - [Live Example](https://palerdot.in/react-d3-speedometer/?path=/story/reactspeedometer--configure-needle-length-and-font-sizes)
253 |
254 | ```javascript
255 |
259 | ```
260 |
261 | You can give a value between `0` and `1` to control the needle height.
262 |
263 |
264 | #### Gradient Like Effect - [Live Example](https://palerdot.in/react-d3-speedometer/?path=/story/reactspeedometer--gradient-effect-with-large-number-of-segments-and-max-segment-labels-config)
265 |
266 | ```javascript
267 |
272 | ```
273 |
274 | ### FAQ:
275 |
276 |
277 | 1) How to use with [nextjs](https://nextjs.org/)?
278 |
279 |
280 | `react-d3-speedometer` uses [lodash-es](https://www.npmjs.com/package/lodash-es) dependency for better tree shaking. For [nextjs](https://nextjs.org/), please use [next-transpile-modules](https://www.npmjs.com/package/next-transpile-modules), so that ES module exports from `lodash-es` package is properly transpiled. You can also `nextjs` dynamic imports - https://nextjs.org/docs/advanced-features/dynamic-import#with-no-ssr
281 |
282 |
283 | Please refer to this issue for more details on how to make this library work with `next.js` - https://github.com/palerdot/react-d3-speedometer/issues/89
284 |
285 | 2) How to use with `React 17`?
286 |
287 | Please use latest `v1.x` (`v1.0.0` at the time of writing). `v1.x` is compatible with `React 17`.
288 |
289 | 3) How to use with `React 16`?
290 |
291 | Please use latest `v0.x` (`v0.14.x` at the time of writing). `v0.x` is compatible with `React 16`.
292 |
293 | ---
294 |
295 | ## Ports:
296 | - Vue: [vue-speedometer](https://github.com/palerdot/vue-speedometer)
297 | - Svelte: [svelte-speedometer](https://github.com/palerdot/svelte-speedometer)
298 |
299 | ---
300 |
301 | ### Todos:
302 |
303 | - [x] Test coverage (with enzyme)
304 | - [x] Convert entire code base to ES6
305 | - [x] Split core from lifecycles
306 | - [x] Typescript support
307 |
308 | ---
309 |
310 | ### Tests:
311 |
312 | `react-d3-speedometer` comes with a test suite using [enzyme](https://github.com/airbnb/enzyme).
313 |
314 | ```javascript
315 | // navigate to root folder and run
316 | npm test
317 | // or 'yarn test' if you are using yarn
318 | ```
319 |
320 | ---
321 |
322 | #### Feature Updates:
323 | - [`v1.0.0`] `React v17` support. `d3 v6` support. [Live Example](https://codesandbox.io/s/kind-breeze-esuge?file=/src/App.js)
324 |
325 | - [`v0.14.0`] `valueTextFontWeight` config to control font weight of current value
326 | - [`v0.10.0`] Custom labels. [Live Example](https://codesandbox.io/s/vibrant-platform-cesh3)
327 | - [`v0.9.0`] `Typescript` support
328 | - [`v0.8.0`] `paddingHorizontal`, `paddingVertical` configuration to control spacing around text. [Live Example](https://codesandbox.io/s/blazing-sun-bsm0j)
329 | - [`v0.7.0`] Custom segment stops. [Live Example](https://palerdot.in/react-d3-speedometer/?path=/story/react-d3-speedometer--custom-segment-stops)
330 | - [`v0.6.0`] Custom segment colors. [Live Example](https://codesandbox.io/s/relaxed-silence-c3qkb)
331 |
332 | ---
333 |
334 | #### Changelog:
335 |
336 | [View Changelog](CHANGELOG.md)
337 |
338 | ---
339 |
340 | #### Credits:
341 | `react-d3-speedometer` was started as a react port of the following d3 snippet - [http://bl.ocks.org/msqr/3202712](http://bl.ocks.org/msqr/3202712). Component template was initially bootstrapped with [React CDK](https://github.com/storybooks/react-cdk). Also, many thanks to `react` and `d3` ecosystem contributors.
342 |
343 | ---
344 |
345 | #### Contributing:
346 | PRs are welcome. Please create a issue/bugfix/feature branch and create an issue with your branch details. Probably I will create a similar branch in the upstream repo so that PRs can be raised against that branch instead of `master`. [master-v0.x](https://github.com/palerdot/react-d3-speedometer/tree/master-v0.x) is the main branch for `React 16` compatible changes.
347 |
348 | #### Notes
349 | - `1.x` versions are compatible with React & React DOM Versions `v17.x`
350 | - `0.x` versions are compatible with React & React DOM Versions `v16.x`
351 | For every subsequent major react upgrade, `react-d3-speedometer` will be bumped to next major versions. For example `1.x` will be compatible with `React 17.x` so on and so forth ...
352 |
353 | For similar library for VueJS, please check out [vue-speedometer](https://github.com/palerdot/vue-speedometer).
354 |
355 | For similar library for Svelte, please check out [svelte-speedometer](https://github.com/palerdot/svelte-speedometer).
356 |
357 | #### License:
358 |
359 | [MIT](LICENSE)
360 |
--------------------------------------------------------------------------------
/cypress.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "cypress";
2 |
3 | export default defineConfig({
4 | video: false,
5 |
6 | component: {
7 | setupNodeEvents(on, config) {},
8 | specPattern: "src/__tests__/**/*.spec.{js,jsx}",
9 | devServer: {
10 | framework: "react",
11 | bundler: "vite",
12 | },
13 | },
14 |
15 | e2e: {
16 | setupNodeEvents(on, config) {
17 | // implement node event listeners here
18 | },
19 | },
20 | });
21 |
--------------------------------------------------------------------------------
/cypress/fixtures/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Using fixtures to represent data",
3 | "email": "hello@cypress.io",
4 | "body": "Fixtures are a great way to mock data for responses to routes"
5 | }
6 |
--------------------------------------------------------------------------------
/cypress/plugins/index.js:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************************
3 | // This example plugins/index.js can be used to load plugins
4 | //
5 | // You can change the location of this file or turn off loading
6 | // the plugins file with the 'pluginsFile' configuration option.
7 | //
8 | // You can read more here:
9 | // https://on.cypress.io/plugins-guide
10 | // ***********************************************************
11 |
12 | // This function is called when a project is opened or re-opened (e.g. due to
13 | // the project's config changing)
14 |
15 | // ref: https://docs.cypress.io/guides/component-testing/framework-configuration#Vite-Based-Projects-Vue-React
16 | const path = require('path')
17 | const { startDevServer } = require('@cypress/vite-dev-server')
18 |
19 | /**
20 | * @type {Cypress.PluginConfig}
21 | */
22 |
23 | // eslint-disable-next-line no-unused-vars
24 | module.exports = (on, config) => {
25 | on('dev-server:start', options => {
26 | return startDevServer({
27 | options,
28 | viteConfig: {
29 | configFile: path.resolve(__dirname, '..', '..', 'vite.config.js'),
30 | },
31 | })
32 | })
33 | }
34 |
--------------------------------------------------------------------------------
/cypress/support/commands.js:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add('login', (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26 |
--------------------------------------------------------------------------------
/cypress/support/commands.ts:
--------------------------------------------------------------------------------
1 | ///
2 | // ***********************************************
3 | // This example commands.ts shows you how to
4 | // create various custom commands and overwrite
5 | // existing commands.
6 | //
7 | // For more comprehensive examples of custom
8 | // commands please read more here:
9 | // https://on.cypress.io/custom-commands
10 | // ***********************************************
11 | //
12 | //
13 | // -- This is a parent command --
14 | // Cypress.Commands.add('login', (email, password) => { ... })
15 | //
16 | //
17 | // -- This is a child command --
18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
19 | //
20 | //
21 | // -- This is a dual command --
22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
23 | //
24 | //
25 | // -- This will overwrite an existing command --
26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
27 | //
28 | // declare global {
29 | // namespace Cypress {
30 | // interface Chainable {
31 | // login(email: string, password: string): Chainable
32 | // drag(subject: string, options?: Partial): Chainable
33 | // dismiss(subject: string, options?: Partial): Chainable
34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable
35 | // }
36 | // }
37 | // }
--------------------------------------------------------------------------------
/cypress/support/component-index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Components App
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/cypress/support/component.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/component.ts is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
22 | import { mount } from 'cypress/react'
23 |
24 | // Augment the Cypress namespace to include type definitions for
25 | // your custom command.
26 | // Alternatively, can be defined in cypress/support/component.d.ts
27 | // with a at the top of your spec.
28 | declare global {
29 | namespace Cypress {
30 | interface Chainable {
31 | mount: typeof mount
32 | }
33 | }
34 | }
35 |
36 | Cypress.Commands.add('mount', mount)
37 |
38 | // Example use:
39 | // cy.mount()
--------------------------------------------------------------------------------
/cypress/support/e2e.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/e2e.ts is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
--------------------------------------------------------------------------------
/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
22 | // ref: https://github.com/phongnd39/cypress-jest-adapter
23 | import 'cypress-jest-adapter'
24 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | transformIgnorePatterns: ['/node_modules/(?!lodash-es).+\\.js$'],
3 | verbose: true,
4 | // this option will show coverage everytime
5 | // collectCoverage: true,
6 | }
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-d3-speedometer",
3 | "version": "3.1.1",
4 | "description": "React library for showing speedometer like gauge using d3.",
5 | "author": {
6 | "name": "palerdot",
7 | "email": "palerdot@gmail.com"
8 | },
9 | "files": [
10 | "dist"
11 | ],
12 | "types": "./dist/index.d.ts",
13 | "main": "./dist/react-d3-speedometer.es.js",
14 | "module": "./dist/react-d3-speedometer.es.js",
15 | "exports": {
16 | ".": {
17 | "types": "./dist/index.d.ts",
18 | "import": "./dist/react-d3-speedometer.es.js",
19 | "require": "./dist/react-d3-speedometer.umd.js"
20 | },
21 | "./slim": {
22 | "import": "./dist/slim/index.js"
23 | }
24 | },
25 | "repository": {
26 | "type": "git",
27 | "url": "https://github.com/palerdot/react-d3-speedometer.git"
28 | },
29 | "bugs": {
30 | "url": "https://github.com/palerdot/react-d3-speedometer/issues"
31 | },
32 | "homepage": "https://github.com/palerdot/react-d3-speedometer",
33 | "keywords": [
34 | "react",
35 | "d3",
36 | "speedometer",
37 | "gauge",
38 | "component",
39 | "odometer"
40 | ],
41 | "license": "MIT",
42 | "scripts": {
43 | "lint": "prettier -l **/src/**/*.js",
44 | "lintfix": "prettier --write **/src/**/*.js",
45 | "test": "vitest",
46 | "full-test": "vitest --coverage && npm run cy:run",
47 | "coverage": "vitest run --coverage",
48 | "storybook": "storybook dev --port 6006",
49 | "publish-storybook": "bash .scripts/publish_storybook.sh",
50 | "cy:open": "cypress open",
51 | "cy:run": "cypress run --component",
52 | "dev": "vite",
53 | "build": "npm run build:main && npm run build:slim && npm run copy:types",
54 | "build:main": "NODE_ENV=production vite build",
55 | "build:slim": "NODE_ENV=production vite build -c slim.config.js",
56 | "copy:types": "cp ./src/index.d.ts ./dist/",
57 | "preview": "vite preview",
58 | "build-storybook": "storybook build -o .out",
59 | "prepublishOnly": "npm run build"
60 | },
61 | "devDependencies": {
62 | "@cypress/vite-dev-server": "^5.2.1",
63 | "@rollup/plugin-node-resolve": "^16.0.0",
64 | "@storybook/addon-actions": "^8.5.3",
65 | "@storybook/addon-docs": "^8.5.3",
66 | "@storybook/addon-essentials": "^8.5.3",
67 | "@storybook/addon-interactions": "^8.5.3",
68 | "@storybook/addon-knobs": "^8.0.1",
69 | "@storybook/addon-links": "^8.5.3",
70 | "@storybook/addon-storysource": "^8.5.3",
71 | "@storybook/react": "^8.5.3",
72 | "@storybook/react-vite": "^8.5.3",
73 | "@storybook/test": "^8.5.3",
74 | "@storybook/theming": "^8.5.3",
75 | "@testing-library/jest-dom": "^5.17.0",
76 | "@testing-library/react": "^16.2.0",
77 | "@vitejs/plugin-react": "^4.3.4",
78 | "@vitest/coverage-v8": "^3.0.5",
79 | "cypress": "^14.0.2",
80 | "cypress-jest-adapter": "^0.1.1",
81 | "eslint": "^5.16.0",
82 | "eslint-config-airbnb": "^17.1.1",
83 | "eslint-config-prettier": "^6.15.0",
84 | "eslint-plugin-import": "^2.31.0",
85 | "eslint-plugin-jsx-a11y": "^6.10.2",
86 | "eslint-plugin-prettier": "^3.4.1",
87 | "eslint-plugin-react": "^7.37.4",
88 | "git-url-parse": "^11.6.0",
89 | "happy-dom": "^8.9.0",
90 | "jest": "^26.6.3",
91 | "prettier": "^2.8.8",
92 | "react": "^19.0.0",
93 | "react-dom": "^19.0.0",
94 | "rollup-plugin-analyzer": "^4.0.0",
95 | "rollup-plugin-terser": "^7.0.2",
96 | "storybook": "^8.5.3",
97 | "typescript": "^5.7.3",
98 | "vite": "^6.1.0",
99 | "vitest": "^3.0.5"
100 | },
101 | "peerDependencies": {
102 | "react": "^19.0.0",
103 | "react-dom": "^19.0.0"
104 | },
105 | "dependencies": {
106 | "d3-array": "^3.2.4",
107 | "d3-color": "^3.1.0",
108 | "d3-ease": "^3.0.1",
109 | "d3-format": "^3.1.0",
110 | "d3-interpolate": "^3.0.1",
111 | "d3-scale": "^4.0.2",
112 | "d3-selection": "^3.0.0",
113 | "d3-shape": "^3.2.0",
114 | "d3-transition": "^3.0.1",
115 | "lodash-es": "^4.17.21",
116 | "memoize-one": "^6.0.0",
117 | "prop-types": "^15.8.1"
118 | },
119 | "engines": {
120 | "node": ">=14.0",
121 | "npm": ">=6.0.0"
122 | },
123 | "packageManager": "pnpm@10.2.0",
124 | "sideEffects": false
125 | }
126 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | semi: false,
3 | tabWidth: 2,
4 | arrowParens: 'avoid',
5 | printWidth: 80,
6 | trailingComma: 'es5',
7 | singleQuote: true,
8 | }
9 |
--------------------------------------------------------------------------------
/setupVitest.js:
--------------------------------------------------------------------------------
1 | // ref: https://github.com/testing-library/jest-dom
2 | import '@testing-library/jest-dom'
3 |
4 | // ref: https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html
5 | // In your test setup file
6 | globalThis.IS_REACT_ACT_ENVIRONMENT = true
7 |
--------------------------------------------------------------------------------
/slim.config.js:
--------------------------------------------------------------------------------
1 | // IMPORTANT: Slim config by externalizing 'd3'
2 | // There are bunch of 'd3-*' modules that are dependencies for 'react-d3-speedometer'
3 | // If the project already uses them as dependencies, they can use the slim build
4 |
5 | import path from 'path'
6 | import react from '@vitejs/plugin-react'
7 | import { defineConfig } from 'vite'
8 | import { nodeResolve } from '@rollup/plugin-node-resolve'
9 | import { terser } from 'rollup-plugin-terser'
10 | import analyze from 'rollup-plugin-analyzer'
11 |
12 | const devMode = process.env.NODE_ENV === 'development'
13 |
14 | // ref: https://blog.openreplay.com/the-ultimate-guide-to-getting-started-with-the-rollup-js-javascript-bundler
15 | function terserConfig() {
16 | return terser({
17 | ecma: 2020,
18 |
19 | mangle: { toplevel: true },
20 |
21 | compress: {
22 | module: true,
23 | toplevel: true,
24 | unsafe_arrows: true,
25 | drop_console: !devMode,
26 | drop_debugger: !devMode,
27 | },
28 |
29 | output: { quote_style: 1 },
30 | })
31 | }
32 |
33 | // ref: https://vitejs.dev/guide/build.html#library-mode
34 | module.exports = defineConfig({
35 | plugins: [react()],
36 | build: {
37 | outDir: path.resolve(__dirname, 'dist/slim'),
38 | lib: {
39 | entry: path.resolve(__dirname, 'src/index.jsx'),
40 | name: 'ReactSpeedometer',
41 | formats: ['es'],
42 | // fileName: format => `react-d3-speedometer.${format}.js`,
43 | fileName: () => 'index.js',
44 | },
45 | rollupOptions: {
46 | // make sure to externalize deps that shouldn't be bundled
47 | // into your library
48 | external: [
49 | 'react',
50 | 'react-dom',
51 | 'd3-array',
52 | 'd3-color',
53 | 'd3-ease',
54 | 'd3-format',
55 | 'd3-interpolate',
56 | 'd3-scale',
57 | 'd3-selection',
58 | 'd3-shape',
59 | 'd3-transition',
60 | ],
61 | output: {
62 | // Provide global variables to use in the UMD build
63 | // for externalized deps
64 | globals: {
65 | react: 'React',
66 | },
67 | extend: true,
68 | sourcemap: devMode ? 'inline' : false,
69 | plugins: [terserConfig()],
70 | },
71 |
72 | // ref: https://blog.logrocket.com/does-my-bundle-look-big-in-this/
73 | treeshake: {
74 | moduleSideEffects: false,
75 | },
76 | // IMPORTANT: This plugins is different from output plugins
77 | plugins: [
78 | nodeResolve(),
79 | // analyze({
80 | // summaryOnly: true,
81 | // filterSummary: true,
82 | // }),
83 | ],
84 | },
85 | },
86 | test: {
87 | globals: true,
88 | include: [
89 | '**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
90 | '**/__tests__/**',
91 | ],
92 | },
93 | })
94 |
--------------------------------------------------------------------------------
/speedo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/palerdot/react-d3-speedometer/93c30f4db7048803520120af0c79d3df8f075a50/speedo.gif
--------------------------------------------------------------------------------
/src/__tests__/ForceRender.spec.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from 'react'
2 | import { mount } from 'cypress/react'
3 | import ReactSpeedometer from '../index'
4 |
5 | function ForceRender() {
6 | const [value, setValue] = useState(333)
7 | const [segments, setSegments] = useState(5)
8 | const [forceRender, setForceRender] = useState(false)
9 |
10 | const normalUpdate = useCallback(() => {
11 | setValue(777)
12 | }, [])
13 |
14 | const forceUpdate = useCallback(() => {
15 | setValue(417)
16 | setForceRender(true)
17 | setSegments(10)
18 | }, [])
19 |
20 | return (
21 |
22 |
{'porumai ... force rendering'}
23 |
24 |
32 |
40 |
41 |
46 |
47 | )
48 | }
49 |
50 | describe('Force Render of React Speedometer is working fine', () => {
51 | it('updates component normally', () => {
52 | mount()
53 |
54 | // now we can use any Cypress command to interact with the component
55 | // https://on.cypress.io/api
56 | cy.contains('333')
57 | cy.get('.speedo-segment').should('have.length', 5)
58 |
59 | // click the button
60 | cy.get('button#normal-update').click()
61 |
62 | // now we should have the updated value
63 | cy.contains('777')
64 |
65 | // we did not force rendered; our segments should be the same(5)
66 | cy.get('.speedo-segment').should('have.length', 5)
67 | })
68 |
69 | it('force renders the component with correct value', () => {
70 | mount()
71 |
72 | // now we can use any Cypress command to interact with the component
73 | // https://on.cypress.io/api
74 | cy.contains('333')
75 | cy.get('.speedo-segment').should('have.length', 5)
76 |
77 | // click the button
78 | cy.get('button#force-render').click()
79 |
80 | // now we should have the updated value
81 | cy.contains('417')
82 |
83 | // we force rendered; our segments should be 10 (from 5)
84 | cy.get('.speedo-segment').should('have.length', 10)
85 | })
86 | })
87 |
--------------------------------------------------------------------------------
/src/__tests__/ReactSpeedometer.spec.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { mount } from 'cypress/react'
3 | import ReactSpeedometer from '../index'
4 |
5 | describe('React Speedometer', () => {
6 | it('Renders the component with correct default value', () => {
7 | mount()
8 |
9 | // now we can use any Cypress command to interact with the component
10 | // https://on.cypress.io/api
11 | cy.contains('0')
12 | })
13 |
14 | it('Renders the component with correct value prop', () => {
15 | mount()
16 |
17 | cy.contains('333')
18 | })
19 | })
20 |
--------------------------------------------------------------------------------
/src/__tests__/ReactSpeedometer.test.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @vitest-environment happy-dom
3 | */
4 |
5 | import React from 'react'
6 | import { render, act, screen } from '@testing-library/react'
7 | import ReactSpeedometer from '../index'
8 |
9 | afterAll(async () => {
10 | // cancel async tasks to ignore svg errors
11 | await happyDOM.cancelAsync()
12 | })
13 |
14 | describe('', () => {
15 | test('Default value is displayed correctly', () => {
16 | const { container } = render()
17 | expect(container.querySelector('text.current-value')).toHaveTextContent('0')
18 | })
19 |
20 | test('Default prop value is shown correctly', () => {
21 | const { container } = render()
22 | expect(container.querySelector('text.current-value')).toHaveTextContent(
23 | '333'
24 | )
25 | })
26 |
27 | test('svg.speedometer is present', () => {
28 | const { container } = render()
29 | expect(container.querySelectorAll('svg.speedometer')).toHaveLength(1)
30 | })
31 |
32 | // check if the default segments is 5 by counting 'speedo-segment'
33 | test('by default we should have 5 segments', () => {
34 | const { container } = render()
35 | expect(container.querySelectorAll('path.speedo-segment')).toHaveLength(5)
36 | })
37 |
38 | // check the text color of the current value is the default (#666)
39 | test('should have the default text color for current value', () => {
40 | const { container } = render()
41 | const cssValues = container.querySelector('text.current-value').style
42 | expect(cssValues['fill']).toBe('#666')
43 | })
44 |
45 | // should take the color given by us in 'textColor'
46 | test('should have the text color given by us => steelblue ', () => {
47 | const { container } = render()
48 | const elem = container.querySelector('text.current-value')
49 | const style = elem.style
50 | expect(style.fill).toBe('steelblue')
51 | })
52 |
53 | // default aria-label
54 | test('Default aria-label', () => {
55 | const { container } = render()
56 | const ariaLabel = container
57 | .querySelector('svg.speedometer')
58 | .getAttribute('aria-label')
59 | expect(ariaLabel).toBe('React d3 speedometer')
60 | })
61 |
62 | // aria-label when using svgAriaLabel
63 | test('Custom aria-label when svgAriaLabel is used', () => {
64 | const svgAriaLabel = 'My custom SVG aria label'
65 | const { container } = render(
66 |
67 | )
68 | const ariaLabel = container
69 | .querySelector('svg.speedometer')
70 | .getAttribute('aria-label')
71 | expect(ariaLabel).toBe(svgAriaLabel)
72 | })
73 |
74 | // should smoothly animate only the current value; not other breaking changes
75 | test('smooth update of values', () => {
76 | const value = 333
77 | const updatedValue = 470
78 | const { container, rerender } = render()
79 | expect(container.querySelector('text.current-value')).toHaveTextContent(
80 | value.toString()
81 | )
82 | const defaultStartColor = 'rgb(255, 71, 26)'
83 | const firstSegment = container.querySelectorAll('path.speedo-segment')[0]
84 | const firstFill = firstSegment.getAttribute('fill')
85 | expect(firstFill).toEqual(defaultStartColor)
86 |
87 | // rerender the element with updated props
88 | // ref: https://testing-library.com/docs/example-update-props/
89 | rerender()
90 |
91 | // confirm if our value is updated
92 | expect(container.querySelector('text.current-value')).toHaveTextContent(
93 | updatedValue.toString()
94 | )
95 | // confirm our start color is intact
96 | const updatedFill = container
97 | .querySelectorAll('path.speedo-segment')[0]
98 | .getAttribute('fill')
99 | expect(updatedFill).toEqual(defaultStartColor)
100 | })
101 |
102 | // if force render is present, it should re render the whole component
103 | test('should rerender the whole component when "forceRender: true" ', () => {
104 | const { container, rerender } = render()
105 | expect(container.querySelectorAll('path.speedo-segment')).toHaveLength(5)
106 | // change the props and give 'rerender' true
107 | // rerender the element with updated props
108 | // ref: https://testing-library.com/docs/example-update-props/
109 | rerender()
110 | expect(container.querySelectorAll('path.speedo-segment')).toHaveLength(10)
111 | // now change the forceRender option to false
112 | rerender()
113 | // the segments should remain in 10
114 | expect(container.querySelectorAll('path.speedo-segment')).toHaveLength(10)
115 | })
116 |
117 | // check the format of the values
118 | test('should display the format of the values correctly', () => {
119 | // checking the default value
120 | const { container, rerender } = render()
121 | expect(container.querySelector('text.current-value')).toHaveTextContent('0')
122 | // setting label format to "d" and verifying the resulting value
123 | let passed_value = 477.7,
124 | transformed_value = '478'
125 | // change the props
126 | rerender()
127 | expect(container.querySelector('text.current-value')).toHaveTextContent(
128 | transformed_value
129 | )
130 | })
131 |
132 | // custom value formatter
133 | test('should render with custom segmentValueFormatter correctly', () => {
134 | const segmentValueFormatter = value => `${value}%`
135 |
136 | const { container } = render(
137 |
141 | )
142 |
143 | const textNodes = container.querySelectorAll('text.segment-value')
144 | textNodes.forEach(node => {
145 | expect(node.textContent).toEqual(segmentValueFormatter(node.__data__))
146 | })
147 | })
148 |
149 | // check the custom value text
150 | test('should display custom current text value', () => {
151 | // checking the default value
152 | const { container, rerender } = render(
153 |
154 | )
155 | expect(container.querySelector('text.current-value')).toHaveTextContent(
156 | 'Porumai: 333'
157 | )
158 | // change props to another text
159 | rerender(
160 |
164 | )
165 | // test current value text reflects our new props
166 | expect(container.querySelector('text.current-value')).toHaveTextContent(
167 | 'Current Value: 555'
168 | )
169 | })
170 |
171 | // it should not break on invalid needle transition
172 | test('should not break on invalid needle transition', () => {
173 | const { container } = render(
174 |
175 | )
176 | expect(container.querySelectorAll('path.speedo-segment')).toHaveLength(5)
177 | })
178 |
179 | // [d3-scale][bug]: https://github.com/d3/d3-scale/issues/149
180 | // [fix] should render segments correctly when multiple speedometers are rendered
181 | test('should correctly show the ticks when multiple speedometers are rendered', () => {
182 | const { container } = render(
183 |
64 | Click the below button to force rerendering the whole component on
65 | props change. By default, on props change, only the speedometer
66 | value/needle value will be updated and animated for smooth
67 | visualization. Below button will toggle between two sets of totally
68 | different appearances, when forceRender option is given true.
69 |
42 | Click the below button to force rerendering the whole component on
43 | props change. By default, on props change, only the speedometer
44 | value/needle value will be updated and animated for smooth
45 | visualization. Below button will toggle between two sets of totally
46 | different appearances, when forceRender option is given true.
47 |