├── .babelrc
├── .github
└── workflows
│ └── publish-npm.yml
├── .gitignore
├── .npmrc
├── .secrets.baseline
├── LICENSE
├── README.md
├── eslint.config.mjs
├── package.json
├── public
├── favicon.ico
├── images
│ ├── arrowGauge.jpg
│ ├── bandGauge.jpg
│ ├── blobGauge.jpg
│ ├── defaultGauge.jpg
│ ├── radialGauge.jpg
│ ├── simpleGauge.jpg
│ └── tempGauge.jpg
├── index.html
├── manifest.json
└── sitemap.xml
├── src
├── App.css
├── App.test.js
├── App.tsx
├── TestComponent
│ ├── Gauge.tsx
│ ├── GridLayout.tsx
│ ├── InputTest.tsx
│ └── MainPreviews.tsx
├── index.css
├── index.js
├── lib
│ ├── GaugeComponent
│ │ ├── constants.ts
│ │ ├── hooks
│ │ │ ├── arc.ts
│ │ │ ├── chart.ts
│ │ │ ├── labels.ts
│ │ │ ├── pointer.ts
│ │ │ └── utils.ts
│ │ ├── index.tsx
│ │ └── types
│ │ │ ├── Arc.ts
│ │ │ ├── Dimensions.ts
│ │ │ ├── Gauge.ts
│ │ │ ├── GaugeComponentProps.ts
│ │ │ ├── Labels.ts
│ │ │ ├── Pointer.ts
│ │ │ ├── Tick.ts
│ │ │ └── Tooltip.ts
│ └── index.ts
├── logo.svg
└── serviceWorker.js
├── tsconfig.json
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-react",
4 | ["@babel/preset-env", {"useBuiltIns": "entry"}]
5 | ],
6 | "plugins": ["@babel/plugin-proposal-class-properties"]
7 | }
8 |
--------------------------------------------------------------------------------
/.github/workflows/publish-npm.yml:
--------------------------------------------------------------------------------
1 | name: Update and Publish
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*.*.*'
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | #needs: pre
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v2
15 | - name: Set up Node.js
16 | uses: actions/setup-node@v4
17 | with:
18 | node-version: '20'
19 | registry-url: 'https://registry.npmjs.org'
20 | - uses: c-hive/gha-yarn-cache@v2
21 | - name: Install dependencies
22 | run: yarn install
23 | - name: build
24 | run: yarn run build
25 |
26 |
27 | test:
28 | runs-on: ubuntu-latest
29 | needs: build
30 | continue-on-error: true
31 | strategy:
32 | matrix:
33 | scan: [
34 | secret-scan,
35 | sast-scan,
36 | dependency-scan,
37 | dast-scan,
38 | lint-scan
39 | ]
40 | steps:
41 | - name: Checkout code
42 | uses: actions/checkout@v2
43 |
44 | - name: Set up Node.js
45 | uses: actions/setup-node@v4
46 | with:
47 | node-version: '20'
48 | registry-url: 'https://registry.npmjs.org'
49 |
50 | - uses: c-hive/gha-yarn-cache@v2
51 |
52 | - name: Install dependencies
53 | run: yarn install
54 |
55 | - name: Secret Scanner
56 | if: matrix.scan == 'secret-scan'
57 | uses: secret-scanner/action@0.0.2
58 |
59 | - name: nodejsscan scan
60 | if: matrix.scan == 'sast-scan'
61 | id: njsscan
62 | uses: ajinabraham/njsscan-action@master
63 | with:
64 | args: '.'
65 |
66 | - name: Depcheck
67 | if: matrix.scan == 'dependency-scan'
68 | uses: dependency-check/Dependency-Check_Action@main
69 | id: Depcheck
70 | with:
71 | project: 'test'
72 | path: '.'
73 | format: 'HTML'
74 | out: 'reports' # this is the default, no need to specify unless you wish to override it
75 | args: >
76 | --failOnCVSS 7
77 | --enableRetired
78 | - name: Upload Test results
79 | if: matrix.scan == 'dependency-scan'
80 | uses: actions/upload-artifact@master
81 | with:
82 | name: Depcheck report
83 | path: ${{github.workspace}}/reports
84 | - name: Run DAST
85 | if: matrix.scan == 'dast-scan'
86 | run: |
87 | yarn add global serve
88 | yarn run build-page
89 | yarn run serve -s build &
90 | sleep 5
91 | docker run --network host -v $(pwd):/zap/wrk/:rw -t zaproxy/zap-stable zap-baseline.py -t http://localhost:3000
92 | - name: Test lint
93 | if: matrix.scan == 'lint-scan'
94 | run: yarn run eslint
95 |
96 | publish:
97 | runs-on: ubuntu-latest
98 | needs: test
99 | steps:
100 | - name: Checkout code
101 | uses: actions/checkout@v2
102 |
103 | - name: Set up Node.js
104 | uses: actions/setup-node@v4
105 | with:
106 | node-version: '20'
107 | registry-url: 'https://registry.npmjs.org'
108 |
109 | - uses: c-hive/gha-yarn-cache@v2
110 |
111 | - name: Install dependencies
112 | run: yarn install
113 |
114 | - name: Extract tag version number
115 | id: get_version
116 | uses: battila7/get-version-action@v2
117 |
118 | - name: package.json info
119 | id: info
120 | uses: jaywcjlove/github-action-package@main
121 | with:
122 | version: ${{steps.get_version.outputs.version-without-v}}
123 | - name: build
124 | run: yarn run build-package
125 | - name: Publish to npm
126 | run: npm publish --access public
127 | env:
128 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
129 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | dist
4 | .vscode/
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | # @antoniolago:registry=https://npm.pkg.github.com
2 | @antoniolago:registry=https://registry.npmjs.org/
--------------------------------------------------------------------------------
/.secrets.baseline:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.2.0",
3 | "plugins_used": [
4 | {
5 | "name": "ArtifactoryDetector"
6 | },
7 | {
8 | "name": "AWSKeyDetector"
9 | },
10 | {
11 | "name": "AzureStorageKeyDetector"
12 | },
13 | {
14 | "name": "Base64HighEntropyString",
15 | "limit": 4.5
16 | },
17 | {
18 | "name": "BasicAuthDetector"
19 | },
20 | {
21 | "name": "CloudantDetector"
22 | },
23 | {
24 | "name": "HexHighEntropyString",
25 | "limit": 3
26 | },
27 | {
28 | "name": "IbmCloudIamDetector"
29 | },
30 | {
31 | "name": "IbmCosHmacDetector"
32 | },
33 | {
34 | "name": "JwtTokenDetector"
35 | },
36 | {
37 | "name": "KeywordDetector",
38 | "keyword_exclude": ""
39 | },
40 | {
41 | "name": "MailchimpDetector"
42 | },
43 | {
44 | "name": "NpmDetector"
45 | },
46 | {
47 | "name": "PrivateKeyDetector"
48 | },
49 | {
50 | "name": "SlackDetector"
51 | },
52 | {
53 | "name": "SoftlayerDetector"
54 | },
55 | {
56 | "name": "SquareOAuthDetector"
57 | },
58 | {
59 | "name": "StripeDetector"
60 | },
61 | {
62 | "name": "TwilioKeyDetector"
63 | }
64 | ],
65 | "filters_used": [
66 | {
67 | "path": "detect_secrets.filters.allowlist.is_line_allowlisted"
68 | },
69 | {
70 | "path": "detect_secrets.filters.common.is_baseline_file",
71 | "filename": ".secrets.baseline"
72 | },
73 | {
74 | "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
75 | "min_level": 2
76 | },
77 | {
78 | "path": "detect_secrets.filters.gibberish.should_exclude_secret",
79 | "limit": 3.7
80 | },
81 | {
82 | "path": "detect_secrets.filters.heuristic.is_indirect_reference"
83 | },
84 | {
85 | "path": "detect_secrets.filters.heuristic.is_likely_id_string"
86 | },
87 | {
88 | "path": "detect_secrets.filters.heuristic.is_lock_file"
89 | },
90 | {
91 | "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string"
92 | },
93 | {
94 | "path": "detect_secrets.filters.heuristic.is_potential_uuid"
95 | },
96 | {
97 | "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign"
98 | },
99 | {
100 | "path": "detect_secrets.filters.heuristic.is_sequential_string"
101 | },
102 | {
103 | "path": "detect_secrets.filters.heuristic.is_swagger_file"
104 | },
105 | {
106 | "path": "detect_secrets.filters.heuristic.is_templated_secret"
107 | },
108 | {
109 | "path": "detect_secrets.filters.regex.should_exclude_file",
110 | "pattern": [
111 | "test*"
112 | ]
113 | }
114 | ],
115 | "results": {
116 | "public/index.html": [
117 | {
118 | "type": "Base64 High Entropy String",
119 | "filename": "public/index.html",
120 | "hashed_secret": "cf6b0af1680c4a76b91195722ca326315f1e535e",
121 | "is_verified": false,
122 | "line_number": 19,
123 | "is_secret": false
124 | }
125 | ]
126 | },
127 | "generated_at": "2024-06-30T19:49:07Z"
128 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 antoniolago
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-gauge-component
2 | React Gauge Chart Component for data visualization.
3 |
4 | This is forked from [@Martin36/react-gauge-chart](https://github.com/Martin36/react-gauge-chart) [0b24a45](https://github.com/Martin36/react-gauge-chart/pull/131).
5 |
6 | 🔑Key differences
7 |
8 |
9 | Added min/max values
10 | Added grafana based gauge
11 | Added arcs limits in value instead of percent
12 | Added inner/outer ticks to the gauge for reference of the values
13 | Added blob pointer type
14 | Added arrow pointer type
15 | Added tooltips on hover for the arcs
16 | Added arc with linear gradient colors
17 | Full responsive
18 | All render flow fixed and optimized avoiding unecessary resource usage. Performance test, left is original: https://user-images.githubusercontent.com/45375617/239447916-217630e7-8e34-4a3e-a59f-7301471b9855.png
19 | Refactored project structure to separated files
20 | Refactored to Typescript
21 | Added complex objects for better modulation and organization of the project
22 | Fixed Rerenderings making arcs to be repeated in different sizes
23 | Fixed needing to set height bug
24 | Fixed needing to set id bug
25 |
26 |
27 |
28 | # Demo
29 | https://antoniolago.github.io/react-gauge-component/
30 |
31 | # Usage
32 | Install it by running `npm install react-gauge-component --save` or `yarn add react-gauge-component`. Then to use it:
33 |
34 | ```jsx
35 | import GaugeComponent from 'react-gauge-component';
36 | //or
37 | import { GaugeComponent } from 'react-gauge-component';
38 |
39 | //Component with default values
40 |
41 | ```
42 |
43 | For next.js you'll have to do dynamic import:
44 | ```jsx
45 | import dynamic from "next/dynamic";
46 | const GaugeComponent = dynamic(() => import('react-gauge-component'), { ssr: false });
47 |
48 | //Component with default values
49 |
50 | ```
51 |
52 | ## Examples
53 | ### Simple Gauge.
54 | 
55 |
56 | Show Simple Gauge code
57 |
58 | ### Simple Gauge
59 |
60 | ```jsx
61 |
88 | ```
89 |
90 |
91 | ### Custom Bandwidth Gauge.
92 | 
93 |
94 | Show Bandwidth Gauge code
95 |
96 | ### Bandwidth Gauge
97 |
98 | ```jsx
99 | const kbitsToMbits = (value) => {
100 | if (value >= 1000) {
101 | value = value / 1000;
102 | if (Number.isInteger(value)) {
103 | return value.toFixed(0) + ' mbit/s';
104 | } else {
105 | return value.toFixed(1) + ' mbit/s';
106 | }
107 | } else {
108 | return value.toFixed(0) + ' kbit/s';
109 | }
110 | }
111 |
149 | ```
150 |
151 |
152 | ### Custom Temperature Gauge
153 | 
154 |
155 | Show Temperature Gauge code
156 |
157 | ### Temperature Gauge
158 |
159 | ```jsx
160 | console.log("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
176 | onMouseMove: () => console.log("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"),
177 | onMouseLeave: () => console.log("CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"),
178 | },
179 | {
180 | limit: 17,
181 | color: '#F5CD19',
182 | showTick: true,
183 | tooltip: {
184 | text: 'Low temperature!'
185 | }
186 | },
187 | {
188 | limit: 28,
189 | color: '#5BE12C',
190 | showTick: true,
191 | tooltip: {
192 | text: 'OK temperature!'
193 | }
194 | },
195 | {
196 | limit: 30, color: '#F5CD19', showTick: true,
197 | tooltip: {
198 | text: 'High temperature!'
199 | }
200 | },
201 | {
202 | color: '#EA4228',
203 | tooltip: {
204 | text: 'Too high temperature!'
205 | }
206 | }
207 | ]
208 | }}
209 | pointer={{
210 | color: '#345243',
211 | length: 0.80,
212 | width: 15,
213 | // elastic: true,
214 | }}
215 | labels={{
216 | valueLabel: { formatTextValue: value => value + 'ºC' },
217 | tickLabels: {
218 | type: 'outer',
219 | defaultTickValueConfig: {
220 | formatTextValue: (value: any) => value + 'ºC' ,
221 | style: {fontSize: 10}
222 | },
223 | ticks: [
224 | { value: 13 },
225 | { value: 22.5 },
226 | { value: 32 }
227 | ],
228 | }
229 | }}
230 | value={22.5}
231 | minValue={10}
232 | maxValue={35}
233 | />
234 | ```
235 |
236 |
237 | ### Gauge with blob.
238 | 
239 |
240 | Show Gauge with blob code
241 |
242 | ### Custom gauge with blob
243 |
244 | ```jsx
245 |
264 | ```
265 |
266 |
267 |
268 | ### Gradient with arrow gauge.
269 | 
270 |
271 | Show Gradient with arrow code
272 |
273 | ### Custom gradient with arrow
274 |
275 | ```jsx
276 |
309 | ```
310 |
311 |
312 | ### Custom radial gauge.
313 | 
314 |
315 | Show Custom Radial Gauge code
316 |
317 | ### Custom Radial Gauge
318 |
319 | ```jsx
320 |
346 | ```
347 |
348 |
349 | # API
350 |
Props:
351 |
352 | type: string
: The type of the gauge, values can be "grafana"
, "semicircle
and "radial"
. Default: "grafana"
.
353 | id: string
: A unique identifier for the div surrounding the chart. Default: ""
.
354 | className: string
: Adds a className
to the div container. Default: "gauge-component-class"
.
355 | style: React.CSSProperties
: Adds a style object to the div container. Default: {width: 100}
.
356 | marginInPercent: number | {left: number, right: number, top: number, bottom: number}
: Sets the margin for the chart inside the containing SVG element. Default:
357 | "grafana": { top: 0.12, bottom: 0.00, left: 0.07, right: 0.07 }
.
358 | "semicircle": { top: 0.08, bottom: 0.00, left: 0.07, right: 0.07 }
359 | "radial": { top: 0.07, bottom: 0.00, left: 0.07, right: 0.07 }
360 | value: number
: The value of the gauge. Default: 33
.
361 | minValue: number
: The minimum value of the gauge. Default: 0
.
362 | maxValue: number
: The maximum value of the gauge. Default: 100
.
363 | arc: object
: The arc of the gauge.
364 |
365 | cornerRadius: number
: The corner radius of the arc. Default: 7
.
366 | padding: number
: The padding between subArcs, in rad. Default: 0.05
.
367 | width: number
: The width of the arc given in percent of the radius. Default:
368 | "grafana": 0.25
.
369 | "semicircle": 0.15
370 | "radial": 0.2
.
371 | nbSubArcs: number
: The number of subArcs. This overrides subArcs
. Default: undefined
372 | colorArray: Array<string>
: The colors of the arcs. This overrides subArcs
colors. Default: undefined
373 | emptyColor: string
: The default color of the grafana's "empty" subArc color. Default: "#5C5C5C"
374 | gradient: boolean
: This will draw a single arc with all colors provided in subArcs, using limits as references to draw the linear-gradient result. (limits may not be accurate in this mode) Default: false
.
375 | subArcs: Array<object>
: The subArcs of the gauge.
376 |
377 | limit: number
: The subArc length using value as reference. When no limits or lengths are defined will auto-calculate remaining arcs limits. Example of valid input: subArcs: [{limit: 50}, {limit: 100}]
this will render 2 arcs 50/50
378 | length: number
: The subArc length in percent of the arc (as the behavior of the original project). Example of
379 | a valid input: subArcs: [{length: 0.50}, {length: 0.50}]
, this will render 2 arcs 50/50
380 | color: string
: The subArc color. When not provided, it will use default subArc's colors and interpolate first and last colors when subArcs number is greater than colorArray
.
381 | showTick: boolean
: Whether or not to show the tick. Default: false
.
382 | tooltip: object
: Tooltip object.
383 |
384 | text: string
text that will be displayed in the tooltip when hovering the subArc.
385 | style: React.CSSProperties
: Overrides tooltip styles.
386 |
387 |
388 | onClick: (event: any) => void
: onClick callback. Default: undefined
.
389 | onMouseMove: (event: any) => void
: onMouseMove callback. Default: undefined
.
390 | onMouseLeave: (event: any) => void
: onMouseLeave callback. Default: undefined
.
391 |
392 | subArcs default value:
393 |
394 | [
395 | { limit: 33, color: "#5BE12C"},
396 | { limit: 66, color: "#F5CD19"},
397 | { color: "#EA4228"},
398 | ]
399 |
400 |
401 |
402 | pointer: object
: The value pointer of the gauge. Grafana gauge have it's own pointer logic, but animation properties will be applied.
403 |
404 | type: string
This can be "needle", "blob" or "arrow". Default: "needle"
405 | hide: boolean
Enabling this flag will hide the pointer. Default: false
406 | color: string
: The color of the pointer. Default: #464A4F
407 | baseColor: string
: The color of the base of the pointer. Default: #464A4F
408 | length: number
: The length of the pointer 0-1, 1 being the outer radius length. Default: 0.70
409 | animate: boolean
: Whether or not to animate the pointer. Default: true
410 | elastic: boolean
: Whether or not to use elastic pointer. Default: false
411 | animationDuration: number
: The duration of the pointer animation. Default: 3000
412 | animationDelay: number
: The delay of the pointer animation. Default: 100
413 | width: number
: The width of the pointer. Default: 20
414 | strokeWidth: number
: Only available for blob type. Set the width of the stroke. Default: 8
415 |
416 |
417 | labels: object
: The labels of the gauge.
418 |
419 | valueLabel: object
: The center value label of the gauge.
420 |
421 | matchColorWithArc: boolean
: when enabled valueLabel color will match current arc color
422 | formatTextValue: (value: any) => string
: The format of the value label. Default: undefined
.
423 | style: React.CSSProperties
: Overrides valueLabel styles. Default: {fontSize: "35px", fill: "#fff", textShadow: "black 1px 1px 0px, black 0px 0px 2.5em, black 0px 0px 0.2em"}
424 | maxDecimalDigits: number
: this is the number of decimal digits the value will round up to. Default: 2
425 | hide: boolean
: Whether or not to hide the value label. Default: false
.
426 |
427 | tickLabels: object
The tickLabels of the gauge.
428 |
429 | type: string
: This makes the ticks "inner"
or "outer"
the radius. Default:"outer"
430 | hideMinMax: boolean
: Whether or not to hide the min and max labels. Default: false
431 | ticks: Array<object>
: The ticks of the gauge. When not provided, it will use default gauge ticks with five values.
432 |
433 | value: number
: The value of the tick.
434 | valueConfig: object
: The config of the tick's value label. When not provided, it will use default config.
435 | lineConfig: object
: The config of the tick's line. When not provided, it will use default config.
436 |
437 |
438 | defaultTickValueConfig: object
: The default config of the tick's value label.
439 |
440 | formatTextValue: (value: any) => string
: The format of the tick's value label. Default: undefined
441 | style: React.CSSProperties
: Overrides tick's valueConfig styles. Default: {fontSize: "10px", fill: "#464A4F", textShadow: "black 1px 1px 0px, black 0px 0px 2.5em, black 0px 0px 0.2em"}
442 | maxDecimalDigits: number
: this is the number of decimal digits the value will round up to. Default: 2
443 | hide: boolean
: Whether or not to hide the tick's value label. Default: false
444 |
445 |
446 | defaultTickLineConfig: object
: The default config of the tick's line.
447 |
448 | width: number
: The width of the tick's line. Default: 1
449 | length: number
: The length of the tick's line. Default: 7
450 | color: string
: The color of the tick's line. Default: rgb(173 172 171)
451 | distanceFromArc: number
: The distance of the tick's line from the arc. Default: 3
452 | hide: boolean
: Whether or not to hide the tick's line. Default: false
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 | ##### Colors for the chart
462 |
463 | The 'colorArray' prop could either be specified as an array of hex color values, such as `["#FF0000", "#00FF00", "#0000FF"]` where
464 | 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**
465 | must match the **number of levels** in the arc.
466 | If the number of colors does not match the number of levels, then the **first** and the **last** color from the colors array will
467 | 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).
468 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from "globals";
2 | import pluginJs from "@eslint/js";
3 | import tseslint from "typescript-eslint";
4 | import pluginReactConfig from "eslint-plugin-react/configs/recommended.js";
5 | import { fixupConfigRules } from "@eslint/compat";
6 |
7 |
8 | export default [
9 | {files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"]},
10 | { languageOptions: { parserOptions: { ecmaFeatures: { jsx: true } } } },
11 | {languageOptions: { globals: globals.browser }},
12 | pluginJs.configs.recommended,
13 | ...tseslint.configs.recommended,
14 | ...fixupConfigRules(pluginReactConfig),
15 | ];
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-gauge-component",
3 | "version": "1.1.30",
4 | "main": "dist/lib/index.js",
5 | "module": "dist/lib/index.js",
6 | "types": "dist/lib/index.d.ts",
7 | "homepage": ".",
8 | "keywords": [
9 | "gauge",
10 | "chart",
11 | "speedometer",
12 | "grafana gauge",
13 | "react"
14 | ],
15 | "license": "MIT",
16 | "files": [
17 | "dist",
18 | "README.md"
19 | ],
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/antoniolago/react-gauge-component.git"
23 | },
24 | "dependencies": {
25 | "@types/d3": "^7.4.0",
26 | "d3": "^7.6.1",
27 | "lodash": "^4.17.21",
28 | "serve": "^14.2.3"
29 | },
30 | "scripts": {
31 | "start": "react-scripts start",
32 | "prebuild": "rimraf dist",
33 | "build": "set NODE_ENV=production babel src/lib --out-dir dist --copy-files",
34 | "build:types": "tsc",
35 | "build-package": "yarn run build && yarn run build:types",
36 | "build-page": "react-scripts build",
37 | "test": "react-scripts test",
38 | "eject": "react-scripts eject",
39 | "predeploy": "yarn run build-page",
40 | "deploy": "gh-pages -d build",
41 | "test-local": "yarn run build && npm pack --pack-destination ./"
42 | },
43 | "resolutions": {
44 | "@types/react": "~17.0.1"
45 | },
46 | "browserslist": [
47 | ">0.2%",
48 | "not dead",
49 | "not ie <= 11",
50 | "not op_mini all"
51 | ],
52 | "devDependencies": {
53 | "@babel/cli": "^7.12.8",
54 | "@babel/core": "^7.6.2",
55 | "@babel/plugin-proposal-class-properties": "^7.4.4",
56 | "@babel/preset-env": "^7.4.4",
57 | "@babel/preset-react": "^7.0.0",
58 | "@babel/runtime": "^7.6.2",
59 | "@eslint/compat": "^1.1.0",
60 | "@eslint/js": "^9.6.0",
61 | "@types/lodash": "^4.14.202",
62 | "@types/node": "^20.1.0",
63 | "@types/react": "^17.0.1",
64 | "@types/react-dom": "^17.0.1",
65 | "babel-preset-react-app": "^8.0.0",
66 | "cross-env": "^5.2.1",
67 | "eslint": "9.x",
68 | "eslint-config-airbnb": "^19.0.4",
69 | "eslint-config-prettier": "^9.1.0",
70 | "eslint-plugin-import": "^2.29.1",
71 | "eslint-plugin-jest": "^28.6.0",
72 | "eslint-plugin-jsx-a11y": "^6.9.0",
73 | "eslint-plugin-prettier": "^5.1.3",
74 | "eslint-plugin-react": "^7.34.3",
75 | "eslint-plugin-react-hooks": "^4.6.2",
76 | "eslint-plugin-testing-library": "^6.2.2",
77 | "gh-pages": "^2.1.1",
78 | "globals": "^15.7.0",
79 | "jest": "^29.7.0",
80 | "prettier": "^3.3.2",
81 | "react": "^17.0.1",
82 | "react-bootstrap": "^1.4.0",
83 | "react-dom": "^17.0.1",
84 | "react-grid-layout": "^1.4.4",
85 | "react-scripts": "^5.0.1",
86 | "rimraf": "^2.7.1",
87 | "typescript": "^5.0.4",
88 | "typescript-eslint": "^7.14.1"
89 | },
90 | "peerDependencies": {
91 | "react": "^16.8.2 || ^17.0 || ^18.x || ^19.x",
92 | "react-dom": "^16.8.2 || ^17.0 || ^18.x || ^19.x"
93 | },
94 | "publishConfig": {
95 | "registry": "https://registry.npmjs.org/"
96 | },
97 | "description": "Gauge component for React",
98 | "bugs": {
99 | "url": "https://github.com/antoniolago/react-gauge-component/issues"
100 | },
101 | "author": "Antônio Lago",
102 | "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
103 | }
104 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoniolago/react-gauge-component/b7905caff59b4373a10809b3bbea682ae7f6bb7d/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/arrowGauge.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoniolago/react-gauge-component/b7905caff59b4373a10809b3bbea682ae7f6bb7d/public/images/arrowGauge.jpg
--------------------------------------------------------------------------------
/public/images/bandGauge.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoniolago/react-gauge-component/b7905caff59b4373a10809b3bbea682ae7f6bb7d/public/images/bandGauge.jpg
--------------------------------------------------------------------------------
/public/images/blobGauge.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoniolago/react-gauge-component/b7905caff59b4373a10809b3bbea682ae7f6bb7d/public/images/blobGauge.jpg
--------------------------------------------------------------------------------
/public/images/defaultGauge.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoniolago/react-gauge-component/b7905caff59b4373a10809b3bbea682ae7f6bb7d/public/images/defaultGauge.jpg
--------------------------------------------------------------------------------
/public/images/radialGauge.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoniolago/react-gauge-component/b7905caff59b4373a10809b3bbea682ae7f6bb7d/public/images/radialGauge.jpg
--------------------------------------------------------------------------------
/public/images/simpleGauge.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoniolago/react-gauge-component/b7905caff59b4373a10809b3bbea682ae7f6bb7d/public/images/simpleGauge.jpg
--------------------------------------------------------------------------------
/public/images/tempGauge.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoniolago/react-gauge-component/b7905caff59b4373a10809b3bbea682ae7f6bb7d/public/images/tempGauge.jpg
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
37 |
38 | React Gauge Component Demo
39 |
40 |
41 | You need to enable JavaScript to run this app.
42 |
43 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React Gauge Component",
3 | "name": "React Gauge Component Demo",
4 | "description": "React Gauge Component for visualizing data metrics, built with D3.js.",
5 | "icons": [
6 | {
7 | "src": "favicon.ico",
8 | "sizes": "64x64 32x32 24x24 16x16",
9 | "type": "image/x-icon"
10 | }
11 | ],
12 | "start_url": ".",
13 | "display": "standalone",
14 | "theme_color": "#000000",
15 | "background_color": "#ffffff"
16 | }
17 |
--------------------------------------------------------------------------------
/public/sitemap.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | https://antoniolago.github.io/react-gauge-component/
5 | 2023-05-12
6 | daily
7 | 1.0
8 |
9 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antoniolago/react-gauge-component/b7905caff59b4373a10809b3bbea682ae7f6bb7d/src/App.css
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './App.css';
3 | import MainPreviews from './TestComponent/MainPreviews';
4 | import InputTest from './TestComponent/InputTest';
5 | import GridLayoutComponent from './TestComponent/GridLayout';
6 | import 'react-grid-layout/css/styles.css'
7 | import 'react-resizable/css/styles.css';
8 |
9 | const App = () => {
10 | return(
11 | <>
12 | {/* */}
13 |
14 |
15 | >
16 | )
17 | };
18 |
19 | export default App
20 |
--------------------------------------------------------------------------------
/src/TestComponent/Gauge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import GaugeComponent from '../lib/GaugeComponent';
3 | export type ReactGaugeComponentProps = {
4 | value: number;
5 | min: number;
6 | max: number;
7 | hideLabel: boolean;
8 | };
9 |
10 | export const ReactGaugeComponent: React.FC = ({
11 | value,
12 | min,
13 | max,
14 | hideLabel,
15 | }) => {
16 | // console.log("rendering", min, max, value);
17 | return (
18 |
19 |
ReactGauge
20 |
`${value} %` },
47 | // lineConfig: { hide: hideLabel },
48 | },
49 | {
50 | value,
51 | valueConfig: { formatTextValue: (value) => `${value} %` },
52 | // lineConfig: { hide: hideLabel },
53 | },
54 | {
55 | value: max,
56 | valueConfig: { formatTextValue: (value) => `${value} %` },
57 | // lineConfig: { hide: hideLabel },
58 | },
59 | ],
60 | },
61 | }}
62 | />
63 |
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/src/TestComponent/GridLayout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import GridLayout from 'react-grid-layout';
3 | import GaugeComponent from '../lib';
4 | import WidthProvider from 'react-grid-layout';
5 | import Responsive from 'react-grid-layout';
6 |
7 | // const ResponsiveReactGridLayout = WidthProvider(Responsive);
8 | const layout = [
9 | { i: 'a', x: 0, y: 0, w: 1, h: 4, static: false },
10 | { i: 'b', x: 1, y: 0, w: 3, h: 4, static: false },
11 | { i: 'c', x: 4, y: 0, w: 1, h: 4, static: false }
12 | ];
13 |
14 | const gaugeConfig = [
15 | {
16 | limit: 0,
17 | color: '#FFFFFF',
18 | showTick: true,
19 | tooltip: { text: 'Empty' }
20 | },
21 | {
22 | limit: 40,
23 | color: '#F58B19',
24 | showTick: true,
25 | tooltip: { text: 'Low' }
26 | },
27 | {
28 | limit: 60,
29 | color: '#F5CD19',
30 | showTick: true,
31 | tooltip: { text: 'Fine' }
32 | },
33 | {
34 | limit: 100,
35 | color: '#5BE12C',
36 | showTick: true,
37 | tooltip: { text: 'Full' }
38 | }
39 | ];
40 |
41 | const GridLayoutComponent = () => (
42 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | );
60 |
61 | export default GridLayoutComponent;
--------------------------------------------------------------------------------
/src/TestComponent/InputTest.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useState } from "react";
3 | import { ReactGaugeComponent } from "./Gauge";
4 | const InputTest = () => {
5 | const [value, setValue] = useState(50);
6 | const [min, setMin] = useState(0);
7 | const [max, setMax] = useState(100);
8 | const [hideLabel, setHideLabel] = useState(false);
9 |
10 | return (
11 |
12 |
16 | setValue((old) => {
17 | const newValue = parseFloat(event.target.value);
18 | if (isNaN(newValue)) {
19 | return old;
20 | }
21 | return newValue;
22 | })
23 | }
24 | />
25 |
29 | setMin((old) => {
30 | const newValue = parseFloat(event.target.value);
31 | if (isNaN(newValue)) {
32 | return old;
33 | }
34 | return newValue;
35 | })
36 | }
37 | />
38 |
42 | setMax((old) => {
43 | const newValue = parseFloat(event.target.value);
44 | if (isNaN(newValue)) {
45 | return old;
46 | }
47 | return newValue;
48 | })
49 | }
50 | />
51 | setHideLabel(event.target.checked)}
55 | />
56 |
62 |
63 | );
64 | }
65 | export default InputTest;
--------------------------------------------------------------------------------
/src/TestComponent/MainPreviews.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Container, Row, Col } from 'react-bootstrap';
3 | import GaugeComponent from '../lib';
4 | import CONSTANTS from '../lib/GaugeComponent/constants';
5 |
6 | const MainPreviews = () => {
7 | const [currentValue, setCurrentValue] = useState(50);
8 | const [arcs, setArcs] = useState([{ limit: 30 }, { limit: 50 }, { limit: 100 }])
9 |
10 | useEffect(() => {
11 | const timer = setTimeout(() => {
12 | setCurrentValue(Math.random() * 100);
13 |
14 | // setCurrentValue(0);
15 | // setArcs([{ limit: 30 }, { limit: 35 }, { limit: 100 }])
16 | }, 3000);
17 |
18 | return () => {
19 | clearTimeout(timer);
20 | };
21 | });
22 | const kbitsToMbits = (value: number) => {
23 | if (value >= 1000) {
24 | value = value / 1000;
25 | if (Number.isInteger(value)) {
26 | return value.toFixed(0) + ' mbit/s';
27 | } else {
28 | return value.toFixed(1) + ' mbit/s';
29 | }
30 | } else {
31 | return value.toFixed(0) + ' kbit/s';
32 | }
33 | }
34 | var ranges = [{ "fieldId": 5, "from": 0, "to": 5000, "state": { "base": "info", "appId": 1, "createdAt": "2023-11-19T11:11:32.039109", "name": "Info", "id": 1, "hexColor": "#3498db", } }, { "fieldId": 5, "from": 5000, "to": 30000, "state": { "base": "error", "appId": 1, "createdAt": "2023-11-19T11:11:32.039109", "name": "Error", "id": 4, "hexColor": "#c0392b", } }, { "fieldId": 5, "from": 30000, "to": 70000, "state": { "base": "critical", "appId": 1, "createdAt": "2023-11-19T11:11:32.039109", "name": "Critical Error", "id": 3, "hexColor": "#e74c3c", } }]
35 | function generateTickValues(min: number, max: number, count: number): { value: number }[] {
36 | const step = (max - min) / (count - 1);
37 | const values: { value: number }[] = [];
38 |
39 | for (let i = 0; i < count; i++) {
40 | const value = Math.round(min + step * i);
41 | values.push({ value });
42 | }
43 |
44 | return values;
45 | }
46 | const debugGauge = () => value + 'ºC' },
61 | tickLabels: {
62 | defaultTickValueConfig: { formatTextValue: value => value + 'ºC' },
63 | ticks: [
64 | { value: 22.5 }
65 | ]
66 | }
67 | }}
68 | value={100}
69 | minValue={10}
70 | maxValue={100}
71 | />
72 | return (
73 | CONSTANTS.debugSingleGauge ?
74 |
75 |
76 |
77 | Single GaugeComponent for debugging
78 | {debugGauge()}
79 |
80 |
81 | Single GaugeComponent for debugging
82 | {debugGauge()}
83 |
84 |
85 | Single GaugeComponent for debugging
86 | {debugGauge()}
87 |
88 |
89 |
90 | :
91 | <>
92 |
93 |
94 |
95 | React Gauge Component Demo
96 |
97 |
98 |
99 |
100 |
103 | Enhance your projects with this React Gauge chart built with D3 library.
104 | This component features custom min/max values, ticks, and tooltips,
105 | making it perfect for visualizing various metrics such as speed,
106 | temperature, charge, and humidity. This data visualization tool can be very useful for React developers looking to create engaging and informative dashboards.
107 | Documentation at react-gauge-component
108 |
109 |
110 |
111 |
112 |
113 | Default Gauge
114 |
115 |
116 |
117 | Simple Gauge (w/ tooltips)
118 |
149 |
150 |
151 | Simple interpolated Gauge
152 |
167 |
168 |
169 |
170 | Min/max values and formatted text
171 |
209 |
210 |
211 | Default semicircle Gauge
212 |
213 |
214 |
215 | Gradient arc with arrow
216 |
258 |
259 |
260 | Elastic blob pointer Live updates
261 |
281 |
282 |
283 | Temperature Gauge with tooltips on hover
284 | console.log("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
300 | onMouseMove: () => console.log("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"),
301 | onMouseLeave: () => console.log("CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"),
302 | },
303 | {
304 | limit: 17,
305 | color: '#F5CD19',
306 | showTick: true,
307 | tooltip: {
308 | text: 'Low temperature!'
309 | }
310 | },
311 | {
312 | limit: 28,
313 | color: '#5BE12C',
314 | showTick: true,
315 | tooltip: {
316 | text: 'OK temperature!'
317 | }
318 | },
319 | {
320 | limit: 30, color: '#F5CD19', showTick: true,
321 | tooltip: {
322 | text: 'High temperature!'
323 | }
324 | },
325 | {
326 | color: '#EA4228',
327 | tooltip: {
328 | text: 'Too high temperature!'
329 | }
330 | }
331 | ]
332 | }}
333 | pointer={{
334 | color: '#345243',
335 | length: 0.80,
336 | width: 15,
337 | // animate: true,
338 | // elastic: true,
339 | animationDelay: 200,
340 | }}
341 | labels={{
342 | valueLabel: { formatTextValue: value => value + 'ºC' },
343 | tickLabels: {
344 | type: 'outer',
345 | defaultTickValueConfig: { formatTextValue: value => value + 'ºC' },
346 | ticks: [
347 | { value: 13 },
348 | { value: 22.5 },
349 | { value: 32 }
350 | ],
351 | }
352 | }}
353 | value={22.5}
354 | minValue={10}
355 | maxValue={35}
356 | />
357 |
358 |
359 | Default Radial Gauge
360 |
361 |
362 |
363 | Radial custom width
364 |
386 |
387 |
388 | Radial inner ticks
389 |
412 |
413 |
414 | Radial elastic
415 |
442 |
443 |
444 |
445 | >
446 | )
447 | };
448 |
449 | export default MainPreviews
450 |
--------------------------------------------------------------------------------
/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/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/lib/GaugeComponent/constants.ts:
--------------------------------------------------------------------------------
1 | export const CONSTANTS: any = {
2 | arcTooltipClassname: "gauge-component-arc-tooltip",
3 | tickLineClassname: "tick-line",
4 | tickValueClassname: "tick-value",
5 | valueLabelClassname: "value-text",
6 | debugTicksRadius: false,
7 | debugSingleGauge: false,
8 | rangeBetweenCenteredTickValueLabel: [0.35, 0.65]
9 | }
10 | export default CONSTANTS;
--------------------------------------------------------------------------------
/src/lib/GaugeComponent/hooks/arc.ts:
--------------------------------------------------------------------------------
1 | import * as utils from './utils';
2 | import {
3 | select,
4 | scaleLinear,
5 | interpolateHsl,
6 | arc,
7 | } from "d3";
8 | import { Gauge } from '../types/Gauge';
9 | import * as arcHooks from './arc';
10 | import CONSTANTS from '../constants';
11 | import { Tooltip, defaultTooltipStyle } from '../types/Tooltip';
12 | import { GaugeType } from '../types/GaugeComponentProps';
13 | import { throttle } from 'lodash';
14 | import { Arc, SubArc } from '../types/Arc';
15 |
16 | const onArcMouseMove = (event: any, d: any, gauge: Gauge) => {
17 | //event.target.style.stroke = "#ffffff5e";
18 | if (d.data.tooltip != undefined) {
19 | let shouldChangeText = d.data.tooltip.text != gauge.tooltip.current.text();
20 | if (shouldChangeText) {
21 | gauge.tooltip.current.html(d.data.tooltip.text)
22 | .style("position", "absolute")
23 | .style("display", "block")
24 | .style("opacity", 1);
25 | applyTooltipStyles(d.data.tooltip, d.data.color, gauge);
26 | }
27 | gauge.tooltip.current.style("left", (event.pageX + 15) + "px")
28 | .style("top", (event.pageY - 10) + "px");
29 | }
30 | if (d.data.onMouseMove != undefined) d.data.onMouseMove(event);
31 | }
32 | const applyTooltipStyles = (tooltip: Tooltip, arcColor: string, gauge: Gauge) => {
33 | //Apply default styles
34 | Object.entries(defaultTooltipStyle).forEach(([key, value]) => gauge.tooltip.current.style(utils.camelCaseToKebabCase(key), value))
35 | gauge.tooltip.current.style("background-color", arcColor);
36 | //Apply custom styles
37 | if (tooltip.style != undefined) Object.entries(tooltip.style).forEach(([key, value]) => gauge.tooltip.current.style(utils.camelCaseToKebabCase(key), value))
38 | }
39 | const onArcMouseLeave = (event: any, d: any, gauge: Gauge, mousemoveCbThrottled: any) => {
40 | mousemoveCbThrottled.cancel();
41 | hideTooltip(gauge);
42 | if (d.data.onMouseLeave != undefined) d.data.onMouseLeave(event);
43 | }
44 | export const hideTooltip = (gauge: Gauge) => {
45 | gauge.tooltip.current.html(" ").style("display", "none");
46 | }
47 | const onArcMouseOut = (event: any, d: any, gauge: Gauge) => {
48 | event.target.style.stroke = "none";
49 | }
50 | const onArcMouseClick = (event: any, d: any) => {
51 | if (d.data.onMouseClick != undefined) d.data.onMouseClick(event);
52 | }
53 |
54 | export const setArcData = (gauge: Gauge) => {
55 | let arc = gauge.props.arc as Arc;
56 | let minValue = gauge.props.minValue as number;
57 | let maxValue = gauge.props.maxValue as number;
58 | // Determine number of arcs to display
59 | let nbArcsToDisplay: number = arc?.nbSubArcs || (arc?.subArcs?.length || 1);
60 |
61 | let colorArray = getColors(nbArcsToDisplay, gauge);
62 | if (arc?.subArcs && !arc?.nbSubArcs) {
63 | let lastSubArcLimit = 0;
64 | let lastSubArcLimitPercentageAcc = 0;
65 | let subArcsLength: Array = [];
66 | let subArcsLimits: Array = [];
67 | let subArcsTooltip: Array = [];
68 | arc?.subArcs?.forEach((subArc, i) => {
69 | let subArcLength = 0;
70 | //map limit for non defined subArcs limits
71 | let subArcRange = 0;
72 | let limit = subArc.limit as number;
73 | if (subArc.length != undefined) {
74 | subArcLength = subArc.length;
75 | limit = utils.getCurrentGaugeValueByPercentage(subArcLength + lastSubArcLimitPercentageAcc, gauge);
76 | } else if (subArc.limit == undefined) {
77 | subArcRange = lastSubArcLimit;
78 | let remainingPercentageEquallyDivided: number | undefined = undefined;
79 | let remainingSubArcs = arc?.subArcs?.slice(i);
80 | let remainingPercentage = (1 - utils.calculatePercentage(minValue, maxValue, lastSubArcLimit)) * 100;
81 | if (!remainingPercentageEquallyDivided) {
82 | remainingPercentageEquallyDivided = (remainingPercentage / Math.max(remainingSubArcs?.length || 1, 1)) / 100;
83 | }
84 | limit = lastSubArcLimit + (remainingPercentageEquallyDivided * 100);
85 | subArcLength = remainingPercentageEquallyDivided;
86 | } else {
87 | subArcRange = limit - lastSubArcLimit;
88 | // Calculate arc length based on previous arc percentage
89 | if (i !== 0) {
90 | subArcLength = utils.calculatePercentage(minValue, maxValue, limit) - lastSubArcLimitPercentageAcc;
91 | } else {
92 | subArcLength = utils.calculatePercentage(minValue, maxValue, subArcRange);
93 | }
94 | }
95 | subArcsLength.push(subArcLength);
96 | subArcsLimits.push(limit);
97 | lastSubArcLimitPercentageAcc = subArcsLength.reduce((count, curr) => count + curr, 0);
98 | lastSubArcLimit = limit;
99 | if (subArc.tooltip != undefined) subArcsTooltip.push(subArc.tooltip);
100 | });
101 | let subArcs = arc.subArcs as SubArc[];
102 | gauge.arcData.current = subArcsLength.map((length, i) => ({
103 | value: length,
104 | limit: subArcsLimits[i],
105 | color: colorArray[i],
106 | showTick: subArcs[i].showTick || false,
107 | tooltip: subArcs[i].tooltip || undefined,
108 | onMouseMove: subArcs[i].onMouseMove,
109 | onMouseLeave: subArcs[i].onMouseLeave,
110 | onMouseClick: subArcs[i].onClick
111 | }));
112 | } else {
113 | const arcValue = maxValue / nbArcsToDisplay;
114 |
115 | gauge.arcData.current = Array.from({ length: nbArcsToDisplay }, (_, i) => ({
116 | value: arcValue,
117 | limit: (i + 1) * arcValue,
118 | color: colorArray[i],
119 | tooltip: undefined,
120 | }));
121 | }
122 | };
123 |
124 | const getGrafanaMainArcData = (gauge: Gauge, percent: number | undefined = undefined) => {
125 | let currentPercentage = percent != undefined ? percent : utils.calculatePercentage(gauge.props.minValue as number,
126 | gauge.props.maxValue as number,
127 | gauge.props.value as number);
128 | let curArcData = getArcDataByPercentage(currentPercentage, gauge);
129 | let firstSubArc = {
130 | value: currentPercentage,
131 | //White indicate that no arc was found and work as an alert for debug
132 | color: curArcData?.color || "white",
133 | //disabled for now because onMouseOut is not working properly with the
134 | //high amount of renderings of this arc
135 | //tooltip: curArcData?.tooltip
136 | }
137 | //This is the grey arc that will be displayed when the gauge is not full
138 | let secondSubArc = {
139 | value: 1 - currentPercentage,
140 | color: gauge.props.arc?.emptyColor,
141 | }
142 | return [firstSubArc, secondSubArc];
143 | }
144 | const drawGrafanaOuterArc = (gauge: Gauge, resize: boolean = false) => {
145 | const { outerRadius } = gauge.dimensions.current;
146 | //Grafana's outer arc will be populates as the standard arc data would
147 | if (gauge.props.type == GaugeType.Grafana && resize) {
148 | gauge.doughnut.current.selectAll(".outerSubArc").remove();
149 | let outerArc = arc()
150 | .outerRadius(outerRadius + 7)
151 | .innerRadius(outerRadius + 2)
152 | .cornerRadius(0)
153 | .padAngle(0);
154 | var arcPaths = gauge.doughnut.current
155 | .selectAll("anyString")
156 | .data(gauge.pieChart.current(gauge.arcData.current))
157 | .enter()
158 | .append("g")
159 | .attr("class", "outerSubArc");
160 | let outerArcSubarcs = arcPaths
161 | .append("path")
162 | .attr("d", outerArc);
163 | applyColors(outerArcSubarcs, gauge);
164 | const mousemoveCbThrottled = throttle((event: any, d: any) => onArcMouseMove(event, d, gauge), 20);
165 | arcPaths
166 | .on("mouseleave", (event: any, d: any) => onArcMouseLeave(event, d, gauge, mousemoveCbThrottled))
167 | .on("mouseout", (event: any, d: any) => onArcMouseOut(event, d, gauge))
168 | .on("mousemove", mousemoveCbThrottled)
169 | .on("click", (event: any, d: any) => onArcMouseClick(event, d))
170 | }
171 | }
172 | export const drawArc = (gauge: Gauge, percent: number | undefined = undefined) => {
173 | const { padding, cornerRadius } = gauge.props.arc as Arc;
174 | const { innerRadius, outerRadius } = gauge.dimensions.current;
175 | // chartHooks.clearChart(gauge);
176 | let data = {}
177 | //When gradient enabled, it'll have only 1 arc
178 | if (gauge.props?.arc?.gradient) {
179 | data = [{ value: 1 }];
180 | } else {
181 | data = gauge.arcData.current
182 | }
183 | if (gauge.props.type == GaugeType.Grafana) {
184 | data = getGrafanaMainArcData(gauge, percent);
185 | }
186 | let arcPadding = gauge.props.type == GaugeType.Grafana ? 0 : padding;
187 | let arcCornerRadius = gauge.props.type == GaugeType.Grafana ? 0 : cornerRadius;
188 | let arcObj = arc()
189 | .outerRadius(outerRadius)
190 | .innerRadius(innerRadius)
191 | .cornerRadius(arcCornerRadius as number)
192 | .padAngle(arcPadding);
193 | var arcPaths = gauge.doughnut.current
194 | .selectAll("anyString")
195 | .data(gauge.pieChart.current(data))
196 | .enter()
197 | .append("g")
198 | .attr("class", "subArc");
199 | let subArcs = arcPaths
200 | .append("path")
201 | .attr("d", arcObj);
202 | applyColors(subArcs, gauge);
203 | const mousemoveCbThrottled = throttle((event: any, d: any) => onArcMouseMove(event, d, gauge), 20);
204 | arcPaths
205 | .on("mouseleave", (event: any, d: any) => onArcMouseLeave(event, d, gauge, mousemoveCbThrottled))
206 | .on("mouseout", (event: any, d: any) => onArcMouseOut(event, d, gauge))
207 | .on("mousemove", mousemoveCbThrottled)
208 | .on("click", (event: any, d: any) => onArcMouseClick(event, d))
209 |
210 | }
211 | export const setupArcs = (gauge: Gauge, resize: boolean = false) => {
212 | //Setup the arc
213 | setupTooltip(gauge);
214 | drawGrafanaOuterArc(gauge, resize);
215 | drawArc(gauge);
216 | };
217 |
218 | export const setupTooltip = (gauge: Gauge) => {
219 | //Add tooltip
220 | let isTooltipInTheDom = document.getElementsByClassName(CONSTANTS.arcTooltipClassname).length != 0;
221 | if (!isTooltipInTheDom) select("body").append("div").attr("class", CONSTANTS.arcTooltipClassname);
222 | gauge.tooltip.current = select(`.${CONSTANTS.arcTooltipClassname}`);
223 | gauge.tooltip.current
224 | .on("mouseleave", () => arcHooks.hideTooltip(gauge))
225 | .on("mouseout", () => arcHooks.hideTooltip(gauge))
226 | }
227 |
228 | export const applyColors = (subArcsPath: any, gauge: Gauge) => {
229 | if (gauge.props?.arc?.gradient) {
230 | let uniqueId = `subArc-linear-gradient-${Math.random()}`
231 | let gradEl = createGradientElement(gauge.doughnut.current, uniqueId);
232 | applyGradientColors(gradEl, gauge)
233 | subArcsPath.style("fill", (d: any) => `url(#${uniqueId})`);
234 | } else {
235 | subArcsPath.style("fill", (d: any) => d.data.color);
236 | }
237 | }
238 |
239 | export const getArcDataByValue = (value: number, gauge: Gauge): SubArc =>
240 | gauge.arcData.current.find(subArcData => value <= (subArcData.limit as number)) as SubArc;
241 |
242 | export const getArcDataByPercentage = (percentage: number, gauge: Gauge): SubArc =>
243 | getArcDataByValue(utils.getCurrentGaugeValueByPercentage(percentage, gauge), gauge) as SubArc;
244 |
245 | export const applyGradientColors = (gradEl: any, gauge: Gauge) => {
246 |
247 | gauge.arcData.current.forEach((subArcData: SubArc) => {
248 | const normalizedOffset = utils.normalize(subArcData?.limit!, gauge?.props?.minValue ?? 0, gauge?.props?.maxValue ?? 100);
249 | gradEl.append("stop")
250 | .attr("offset", `${normalizedOffset}%`)
251 | .style("stop-color", subArcData.color)//end in red
252 | .style("stop-opacity", 1)
253 | }
254 | )
255 | }
256 |
257 | //Depending on the number of levels in the chart
258 | //This function returns the same number of colors
259 | export const getColors = (nbArcsToDisplay: number, gauge: Gauge) => {
260 | let arc = gauge.props.arc as Arc;
261 | let colorsValue: string[] = [];
262 | if (!arc.colorArray) {
263 | let subArcColors = arc.subArcs?.map((subArc) => subArc.color);
264 | colorsValue = subArcColors?.some((color) => color != undefined) ? subArcColors : CONSTANTS.defaultColors;
265 | } else {
266 | colorsValue = arc.colorArray;
267 | }
268 | //defaults colorsValue to white in order to avoid compilation error
269 | if (!colorsValue) colorsValue = ["#fff"];
270 | //Check if the number of colors equals the number of levels
271 | //Otherwise make an interpolation
272 | let arcsEqualsColorsLength = nbArcsToDisplay === colorsValue?.length;
273 | if (arcsEqualsColorsLength) return colorsValue;
274 | var colorScale = scaleLinear()
275 | .domain([1, nbArcsToDisplay])
276 | //@ts-ignore
277 | .range([colorsValue[0], colorsValue[colorsValue.length - 1]]) //Use the first and the last color as range
278 | //@ts-ignore
279 | .interpolate(interpolateHsl);
280 | var colorArray = [];
281 | for (var i = 1; i <= nbArcsToDisplay; i++) {
282 | colorArray.push(colorScale(i));
283 | }
284 | return colorArray;
285 | };
286 | export const createGradientElement = (div: any, uniqueId: string) => {
287 | //make defs and add the linear gradient
288 | var lg = div.append("defs").append("linearGradient")
289 | .attr("id", uniqueId)//id of the gradient
290 | .attr("x1", "0%")
291 | .attr("x2", "100%")
292 | .attr("y1", "0%")
293 | .attr("y2", "0%")
294 | ;
295 | return lg
296 | }
297 |
298 | export const getCoordByValue = (value: number, gauge: Gauge, position = "inner", centerToArcLengthSubtract = 0, radiusFactor = 1) => {
299 | let positionCenterToArcLength: { [key: string]: () => number } = {
300 | "outer": () => gauge.dimensions.current.outerRadius - centerToArcLengthSubtract + 2,
301 | "inner": () => gauge.dimensions.current.innerRadius * radiusFactor - centerToArcLengthSubtract + 9,
302 | "between": () => {
303 | let lengthBetweenOuterAndInner = (gauge.dimensions.current.outerRadius - gauge.dimensions.current.innerRadius);
304 | let middlePosition = gauge.dimensions.current.innerRadius + lengthBetweenOuterAndInner - 5;
305 | return middlePosition;
306 | }
307 | };
308 | let centerToArcLength = positionCenterToArcLength[position]();
309 | // This normalizes the labels when distanceFromArc = 0 to be just touching the arcs
310 | if (gauge.props.type === GaugeType.Grafana) {
311 | centerToArcLength += 5;
312 | } else if (gauge.props.type === GaugeType.Semicircle) {
313 | centerToArcLength += -2;
314 | }
315 | let percent = utils.calculatePercentage(gauge.props.minValue as number, gauge.props.maxValue as number, value);
316 | let gaugeTypesAngles: Record = {
317 | [GaugeType.Grafana]: {
318 | startAngle: utils.degToRad(-23),
319 | endAngle: utils.degToRad(203)
320 | },
321 | [GaugeType.Semicircle]: {
322 | startAngle: utils.degToRad(0.9),
323 | endAngle: utils.degToRad(179.1)
324 | },
325 | [GaugeType.Radial]: {
326 | startAngle: utils.degToRad(-39),
327 | endAngle: utils.degToRad(219)
328 | },
329 | };
330 |
331 | let { startAngle, endAngle } = gaugeTypesAngles[gauge.props.type as GaugeType];
332 | const angle = startAngle + (percent) * (endAngle - startAngle);
333 |
334 | let coordsRadius = 1 * (gauge.dimensions.current.width / 500);
335 | let coord = [0, -coordsRadius / 2];
336 | let coordMinusCenter = [
337 | coord[0] - centerToArcLength * Math.cos(angle),
338 | coord[1] - centerToArcLength * Math.sin(angle),
339 | ];
340 | let centerCoords = [gauge.dimensions.current.outerRadius, gauge.dimensions.current.outerRadius];
341 | let x = (centerCoords[0] + coordMinusCenter[0]);
342 | let y = (centerCoords[1] + coordMinusCenter[1]);
343 | return { x, y }
344 | }
345 | export const redrawArcs = (gauge: Gauge) => {
346 | clearArcs(gauge);
347 | setArcData(gauge);
348 | setupArcs(gauge);
349 | }
350 | export const clearArcs = (gauge: Gauge) => {
351 | gauge.doughnut.current.selectAll(".subArc").remove();
352 | }
353 | export const clearOuterArcs = (gauge: Gauge) => {
354 | gauge.doughnut.current.selectAll(".outerSubArc").remove();
355 | }
356 |
357 | export const validateArcs = (gauge: Gauge) => {
358 | verifySubArcsLimits(gauge);
359 | }
360 | /**
361 | * Reorders the subArcs within the gauge's arc property based on the limit property.
362 | * SubArcs with undefined limits are sorted last.
363 | */
364 | const reOrderSubArcs = (gauge: Gauge): void => {
365 | let subArcs = gauge.props.arc?.subArcs as SubArc[];
366 | subArcs.sort((a, b) => {
367 | if (typeof a.limit === 'undefined' && typeof b.limit === 'undefined') {
368 | return 0;
369 | }
370 | if (typeof a.limit === 'undefined') {
371 | return 1;
372 | }
373 | if (typeof b.limit === 'undefined') {
374 | return -1;
375 | }
376 | return a.limit - b.limit;
377 | });
378 | }
379 | const verifySubArcsLimits = (gauge: Gauge) => {
380 | // disabled when length implemented.
381 | // reOrderSubArcs(gauge);
382 | let minValue = gauge.props.minValue as number;
383 | let maxValue = gauge.props.maxValue as number;
384 | let arc = gauge.props.arc as Arc;
385 | let subArcs = arc.subArcs as SubArc[];
386 | let prevLimit: number | undefined = undefined;
387 | for (const subArc of gauge.props.arc?.subArcs || []) {
388 | const limit = subArc.limit;
389 | if (typeof limit !== 'undefined') {
390 | // Check if the limit is within the valid range
391 | if (limit < minValue || limit > maxValue)
392 | throw new Error(`The limit of a subArc must be between the minValue and maxValue. The limit of the subArc is ${limit}`);
393 | // Check if the limit is greater than the previous limit
394 | if (typeof prevLimit !== 'undefined') {
395 | if (limit <= prevLimit)
396 | throw new Error(`The limit of a subArc must be greater than the limit of the previous subArc. The limit of the subArc is ${limit}. If you're trying to specify length in percent of the arc, use property "length". refer to: https://github.com/antoniolago/react-gauge-component`);
397 | }
398 | prevLimit = limit;
399 | }
400 | }
401 | // If the user has defined subArcs, make sure the last subArc has a limit equal to the maxValue
402 | if (subArcs.length > 0) {
403 | let lastSubArc = subArcs[subArcs.length - 1];
404 | if (lastSubArc.limit as number < maxValue) lastSubArc.limit = maxValue;
405 | }
406 | }
--------------------------------------------------------------------------------
/src/lib/GaugeComponent/hooks/chart.ts:
--------------------------------------------------------------------------------
1 | import CONSTANTS from "../constants";
2 | import { Arc } from "../types/Arc";
3 | import { Gauge } from "../types/Gauge";
4 | import { GaugeType, GaugeInnerMarginInPercent } from "../types/GaugeComponentProps";
5 | import { Labels } from "../types/Labels";
6 | import * as arcHooks from "./arc";
7 | import * as labelsHooks from "./labels";
8 | import * as pointerHooks from "./pointer";
9 | import * as utilHooks from "./utils";
10 | export const initChart = (gauge: Gauge, isFirstRender: boolean) => {
11 | const { angles } = gauge.dimensions.current;
12 | if (gauge.resizeObserver?.current?.disconnect) {
13 | gauge.resizeObserver?.current?.disconnect();
14 | }
15 | let updatedValue = (JSON.stringify(gauge.prevProps.current.value) !== JSON.stringify(gauge.props.value));
16 | if (updatedValue && !isFirstRender) {
17 | renderChart(gauge, false);
18 | return;
19 | }
20 | gauge.container.current.select("svg").remove();
21 | gauge.svg.current = gauge.container.current.append("svg");
22 | gauge.g.current = gauge.svg.current.append("g"); //Used for margins
23 | gauge.doughnut.current = gauge.g.current.append("g").attr("class", "doughnut");
24 | //gauge.outerDougnut.current = gauge.g.current.append("g").attr("class", "doughnut");
25 | calculateAngles(gauge);
26 | gauge.pieChart.current
27 | .value((d: any) => d.value)
28 | //.padAngle(15)
29 | .startAngle(angles.startAngle)
30 | .endAngle(angles.endAngle)
31 | .sort(null);
32 | //Set up pointer
33 | pointerHooks.addPointerElement(gauge);
34 | renderChart(gauge, true);
35 | }
36 | export const calculateAngles = (gauge: Gauge) => {
37 | const { angles } = gauge.dimensions.current;
38 | if (gauge.props.type == GaugeType.Semicircle) {
39 | angles.startAngle = -Math.PI / 2 + 0.02;
40 | angles.endAngle = Math.PI / 2 - 0.02;
41 | } else if (gauge.props.type == GaugeType.Radial) {
42 | angles.startAngle = -Math.PI / 1.37;
43 | angles.endAngle = Math.PI / 1.37;
44 | } else if (gauge.props.type == GaugeType.Grafana) {
45 | angles.startAngle = -Math.PI / 1.6;
46 | angles.endAngle = Math.PI / 1.6;
47 | }
48 | }
49 | //Renders the chart, should be called every time the window is resized
50 | export const renderChart = (gauge: Gauge, resize: boolean = false) => {
51 | const { dimensions } = gauge;
52 | let arc = gauge.props.arc as Arc;
53 | let labels = gauge.props.labels as Labels;
54 | //if resize recalculate dimensions, clear chart and redraw
55 | //if not resize, treat each prop separately
56 | if (resize) {
57 | updateDimensions(gauge);
58 | //Set dimensions of svg element and translations
59 | gauge.g.current.attr(
60 | "transform",
61 | "translate(" + dimensions.current.margin.left + ", " + 35 + ")"
62 | );
63 | //Set the radius to lesser of width or height and remove the margins
64 | //Calculate the new radius
65 | calculateRadius(gauge);
66 | gauge.doughnut.current.attr(
67 | "transform",
68 | "translate(" + dimensions.current.outerRadius + ", " + dimensions.current.outerRadius + ")"
69 | );
70 | //Hide tooltip failsafe (sometimes subarcs events are not fired)
71 | gauge.doughnut.current
72 | .on("mouseleave", () => arcHooks.hideTooltip(gauge))
73 | .on("mouseout", () => arcHooks.hideTooltip(gauge))
74 | let arcWidth = arc.width as number;
75 | dimensions.current.innerRadius = dimensions.current.outerRadius * (1 - arcWidth);
76 | clearChart(gauge);
77 | arcHooks.setArcData(gauge);
78 | arcHooks.setupArcs(gauge, resize);
79 | labelsHooks.setupLabels(gauge);
80 | if (!gauge.props?.pointer?.hide)
81 | pointerHooks.drawPointer(gauge, resize);
82 | let gaugeTypeHeightCorrection: Record = {
83 | [GaugeType.Semicircle]: 50,
84 | [GaugeType.Radial]: 55,
85 | [GaugeType.Grafana]: 55
86 | }
87 | let boundHeight = gauge.doughnut.current.node().getBoundingClientRect().height;
88 | let boundWidth = gauge.container.current.node().getBoundingClientRect().width;
89 | let gaugeType = gauge.props.type as string;
90 | gauge.svg.current
91 | .attr("width", boundWidth)
92 | .attr("height", boundHeight + gaugeTypeHeightCorrection[gaugeType]);
93 | } else {
94 | let arcsPropsChanged = (JSON.stringify(gauge.prevProps.current.arc) !== JSON.stringify(gauge.props.arc));
95 | let pointerPropsChanged = (JSON.stringify(gauge.prevProps.current.pointer) !== JSON.stringify(gauge.props.pointer));
96 | let valueChanged = (JSON.stringify(gauge.prevProps.current.value) !== JSON.stringify(gauge.props.value));
97 | let ticksChanged = (JSON.stringify(gauge.prevProps.current.labels?.tickLabels) !== JSON.stringify(labels.tickLabels));
98 | let shouldRedrawArcs = arcsPropsChanged
99 | if (shouldRedrawArcs) {
100 | arcHooks.clearArcs(gauge);
101 | arcHooks.setArcData(gauge);
102 | arcHooks.setupArcs(gauge, resize);
103 | }
104 | //If pointer is hidden there's no need to redraw it when only value changes
105 | var shouldRedrawPointer = pointerPropsChanged || (valueChanged && !gauge.props?.pointer?.hide);
106 | if ((shouldRedrawPointer)) {
107 | pointerHooks.drawPointer(gauge);
108 | }
109 | if (arcsPropsChanged || ticksChanged) {
110 | labelsHooks.clearTicks(gauge);
111 | labelsHooks.setupTicks(gauge);
112 | }
113 | if (valueChanged) {
114 | labelsHooks.clearValueLabel(gauge);
115 | labelsHooks.setupValueLabel(gauge);
116 | }
117 | }
118 | };
119 | export const updateDimensions = (gauge: Gauge) => {
120 | const { marginInPercent } = gauge.props;
121 | const { dimensions } = gauge;
122 | var divDimensions = gauge.container.current.node().getBoundingClientRect(),
123 | divWidth = divDimensions.width,
124 | divHeight = divDimensions.height;
125 | if (dimensions.current.fixedHeight == 0) dimensions.current.fixedHeight = divHeight + 200;
126 | //Set the new width and horizontal margins
127 | let isMarginBox = typeof marginInPercent == 'number';
128 | let marginLeft: number = isMarginBox ? marginInPercent as number : (marginInPercent as GaugeInnerMarginInPercent).left;
129 | let marginRight: number = isMarginBox ? marginInPercent as number : (marginInPercent as GaugeInnerMarginInPercent).right;
130 | let marginTop: number = isMarginBox ? marginInPercent as number : (marginInPercent as GaugeInnerMarginInPercent).top;
131 | let marginBottom: number = isMarginBox ? marginInPercent as number : (marginInPercent as GaugeInnerMarginInPercent).bottom;
132 | dimensions.current.margin.left = divWidth * marginLeft;
133 | dimensions.current.margin.right = divWidth * marginRight;
134 | dimensions.current.width = divWidth - dimensions.current.margin.left - dimensions.current.margin.right;
135 |
136 | dimensions.current.margin.top = dimensions.current.fixedHeight * marginTop;
137 | dimensions.current.margin.bottom = dimensions.current.fixedHeight * marginBottom;
138 | dimensions.current.height = dimensions.current.width / 2 - dimensions.current.margin.top - dimensions.current.margin.bottom;
139 | //gauge.height.current = divHeight - dimensions.current.margin.top - dimensions.current.margin.bottom;
140 | };
141 | export const calculateRadius = (gauge: Gauge) => {
142 | const { dimensions } = gauge;
143 | //The radius needs to be constrained by the containing div
144 | //Since it is a half circle we are dealing with the height of the div
145 | //Only needs to be half of the width, because the width needs to be 2 * radius
146 | //For the whole arc to fit
147 |
148 | //First check if it is the width or the height that is the "limiting" dimension
149 | if (dimensions.current.width < 2 * dimensions.current.height) {
150 | //Then the width limits the size of the chart
151 | //Set the radius to the width - the horizontal margins
152 | dimensions.current.outerRadius = (dimensions.current.width - dimensions.current.margin.left - dimensions.current.margin.right) / 2;
153 | } else {
154 | dimensions.current.outerRadius =
155 | dimensions.current.height - dimensions.current.margin.top - dimensions.current.margin.bottom + 35;
156 | }
157 | centerGraph(gauge);
158 | };
159 |
160 | //Calculates new margins to make the graph centered
161 | export const centerGraph = (gauge: Gauge) => {
162 | const { dimensions } = gauge;
163 | dimensions.current.margin.left =
164 | dimensions.current.width / 2 - dimensions.current.outerRadius + dimensions.current.margin.right;
165 | gauge.g.current.attr(
166 | "transform",
167 | "translate(" + dimensions.current.margin.left + ", " + (dimensions.current.margin.top) + ")"
168 | );
169 | };
170 |
171 |
172 | export const clearChart = (gauge: Gauge) => {
173 | //Remove the old stuff
174 | labelsHooks.clearTicks(gauge);
175 | labelsHooks.clearValueLabel(gauge);
176 | pointerHooks.clearPointerElement(gauge);
177 | arcHooks.clearArcs(gauge);
178 | };
--------------------------------------------------------------------------------
/src/lib/GaugeComponent/hooks/labels.ts:
--------------------------------------------------------------------------------
1 | import * as utils from './utils';
2 | import CONSTANTS from '../constants';
3 | import { Gauge } from '../types/Gauge';
4 | import { Tick, defaultTickLabels } from '../types/Tick';
5 | import * as d3 from 'd3';
6 | import React from 'react';
7 | import { GaugeType } from '../types/GaugeComponentProps';
8 | import { getArcDataByValue, getCoordByValue } from './arc';
9 | import { Labels, ValueLabel } from '../types/Labels';
10 | import { Arc, SubArc } from '../types/Arc';
11 | export const setupLabels = (gauge: Gauge) => {
12 | setupValueLabel(gauge);
13 | setupTicks(gauge);
14 | }
15 | export const setupValueLabel = (gauge: Gauge) => {
16 | const { labels } = gauge.props;
17 | if (!labels?.valueLabel?.hide) addValueText(gauge)
18 | }
19 | export const setupTicks = (gauge: Gauge) => {
20 | let labels = gauge.props.labels as Labels;
21 | let minValue = gauge.props.minValue as number;
22 | let maxValue = gauge.props.maxValue as number;
23 | if (CONSTANTS.debugTicksRadius) {
24 | for (let index = 0; index < maxValue; index++) {
25 | let indexTick = mapTick(index, gauge);
26 | addTick(indexTick, gauge);
27 | }
28 | } else if (!labels.tickLabels?.hideMinMax) {
29 | let alreadyHaveMinValueTick = labels.tickLabels?.ticks?.some((tick: Tick) => tick.value == minValue);
30 | if (!alreadyHaveMinValueTick) {
31 | //Add min value tick
32 | let minValueTick = mapTick(minValue, gauge);
33 | addTick(minValueTick, gauge);
34 | }
35 | let alreadyHaveMaxValueTick = labels.tickLabels?.ticks?.some((tick: Tick) => tick.value == maxValue);
36 | if (!alreadyHaveMaxValueTick) {
37 | // //Add max value tick
38 | let maxValueTick = mapTick(maxValue, gauge);
39 | addTick(maxValueTick, gauge);
40 | }
41 | }
42 | if (labels.tickLabels?.ticks?.length as number > 0) {
43 | labels.tickLabels?.ticks?.forEach((tick: Tick) => {
44 | addTick(tick, gauge);
45 | });
46 | }
47 | addArcTicks(gauge);
48 | }
49 |
50 | export const addArcTicks = (gauge: Gauge) => {
51 | gauge.arcData.current?.map((subArc: SubArc) => {
52 | if (subArc.showTick) return subArc.limit;
53 | }).forEach((tickValue: any) => {
54 | if (tickValue) addTick(mapTick(tickValue, gauge), gauge);
55 | });
56 | }
57 | export const mapTick = (value: number, gauge: Gauge): Tick => {
58 | const { tickLabels } = gauge.props.labels as Labels;
59 | return {
60 | value: value,
61 | valueConfig: tickLabels?.defaultTickValueConfig,
62 | lineConfig: tickLabels?.defaultTickLineConfig
63 | } as Tick;
64 | }
65 | export const addTickLine = (tick: Tick, gauge: Gauge) => {
66 | const { labels } = gauge.props;
67 | const { tickAnchor, angle } = calculateAnchorAndAngleByValue(tick?.value as number, gauge);
68 | var tickDistanceFromArc = tick.lineConfig?.distanceFromArc || labels?.tickLabels?.defaultTickLineConfig?.distanceFromArc || 0;
69 | if (gauge.props.labels?.tickLabels?.type == "outer") tickDistanceFromArc = -tickDistanceFromArc;
70 | // else tickDistanceFromArc = tickDistanceFromArc - 10;
71 | let coords = getLabelCoordsByValue(tick?.value as number, gauge, tickDistanceFromArc);
72 |
73 | var tickColor = tick.lineConfig?.color || labels?.tickLabels?.defaultTickLineConfig?.color || defaultTickLabels.defaultTickLineConfig?.color;
74 | var tickWidth = tick.lineConfig?.width || labels?.tickLabels?.defaultTickLineConfig?.width || defaultTickLabels.defaultTickLineConfig?.width;
75 | var tickLength = tick.lineConfig?.length || labels?.tickLabels?.defaultTickLineConfig?.length || defaultTickLabels.defaultTickLineConfig?.length as number;
76 | // Calculate the end coordinates based on the adjusted position
77 | var endX;
78 | var endY;
79 | // When inner should draw from outside to inside
80 | // When outer should draw from inside to outside
81 | if (labels?.tickLabels?.type == "inner") {
82 | endX = coords.x + tickLength * Math.cos((angle * Math.PI) / 180);
83 | endY = coords.y + tickLength * Math.sin((angle * Math.PI) / 180);
84 | } else {
85 | endX = coords.x - tickLength * Math.cos((angle * Math.PI) / 180);
86 | endY = coords.y - tickLength * Math.sin((angle * Math.PI) / 180);
87 | }
88 |
89 | // (gauge.dimensions.current.outerRadius - gauge.dimensions.current.innerRadius)
90 | // Create a D3 line generator
91 | var lineGenerator = d3.line();
92 |
93 | var lineCoordinates;
94 | // Define the line coordinates
95 | lineCoordinates = [[coords.x, coords.y], [endX, endY]];
96 | // Append a path element for the line
97 | gauge.g.current
98 | .append("path")
99 | .datum(lineCoordinates)
100 | .attr("class", CONSTANTS.tickLineClassname)
101 | .attr("d", lineGenerator)
102 | // .attr("transform", `translate(${0}, ${0})`)
103 | .attr("stroke", tickColor)
104 | .attr("stroke-width", tickWidth)
105 | .attr("fill", "none")
106 | // .attr("stroke-linecap", "round")
107 | // .attr("stroke-linejoin", "round")
108 | // .attr("transform", `rotate(${angle})`);
109 | };
110 | export const addTickValue = (tick: Tick, gauge: Gauge) => {
111 | const { labels } = gauge.props;
112 | let arc = gauge.props.arc as Arc;
113 | let arcWidth = arc.width as number;
114 | let tickValue = tick?.value as number;
115 | let { tickAnchor } = calculateAnchorAndAngleByValue(tickValue, gauge);
116 | let centerToArcLengthSubtract = 27 - arcWidth * 10;
117 | let isInner = labels?.tickLabels?.type == "inner";
118 | if (!isInner) centerToArcLengthSubtract = arcWidth * 10 - 10
119 | else centerToArcLengthSubtract -= 10
120 | var tickDistanceFromArc = tick.lineConfig?.distanceFromArc || labels?.tickLabels?.defaultTickLineConfig?.distanceFromArc || 0;
121 | var tickLength = tick.lineConfig?.length || labels?.tickLabels?.defaultTickLineConfig?.length || 0;
122 | var _shouldHideTickLine = shouldHideTickLine(tick, gauge);
123 | if (!_shouldHideTickLine) {
124 | if (isInner) {
125 | centerToArcLengthSubtract += tickDistanceFromArc;
126 | centerToArcLengthSubtract += tickLength;
127 | } else {
128 | centerToArcLengthSubtract -= tickDistanceFromArc;
129 | centerToArcLengthSubtract -= tickLength;
130 | }
131 | }
132 | let coords = getLabelCoordsByValue(tickValue, gauge, centerToArcLengthSubtract);
133 | let tickValueStyle = tick.valueConfig?.style || (labels?.tickLabels?.defaultTickValueConfig?.style || {});
134 | tickValueStyle = { ...tickValueStyle };
135 | let text = '';
136 | let maxDecimalDigits = tick.valueConfig?.maxDecimalDigits || labels?.tickLabels?.defaultTickValueConfig?.maxDecimalDigits;
137 | if (tick.valueConfig?.formatTextValue) {
138 | text = tick.valueConfig.formatTextValue(utils.floatingNumber(tickValue, maxDecimalDigits));
139 | } else if (labels?.tickLabels?.defaultTickValueConfig?.formatTextValue) {
140 | text = labels.tickLabels.defaultTickValueConfig.formatTextValue(utils.floatingNumber(tickValue, maxDecimalDigits));
141 | } else if (gauge.props.minValue === 0 && gauge.props.maxValue === 100) {
142 | text = utils.floatingNumber(tickValue, maxDecimalDigits).toString();
143 | text += "%";
144 | } else {
145 | text = utils.floatingNumber(tickValue, maxDecimalDigits).toString();
146 | }
147 | if (labels?.tickLabels?.type == "inner") {
148 | if (tickAnchor === "end") coords.x += 10;
149 | if (tickAnchor === "start") coords.x -= 10;
150 | // if (tickAnchor === "middle") coords.y -= 0;
151 | } else {
152 | // if(tickAnchor === "end") coords.x -= 10;
153 | // if(tickAnchor === "start") coords.x += 10;
154 | if (tickAnchor === "middle") coords.y += 2;
155 | }
156 | if (tickAnchor === "middle") {
157 | coords.y += 0;
158 | } else {
159 | coords.y += 3;
160 | }
161 | tickValueStyle.textAnchor = tickAnchor as any;
162 | addText(text, coords.x, coords.y, gauge, tickValueStyle, CONSTANTS.tickValueClassname);
163 | }
164 | export const addTick = (tick: Tick, gauge: Gauge) => {
165 | const { labels } = gauge.props;
166 | //Make validation for sequence of values respecting DEFAULT -> DEFAULT FROM USER -> SPECIFIC TICK VALUE
167 | var _shouldHideTickLine = shouldHideTickLine(tick, gauge);
168 | var _shouldHideTickValue = shouldHideTickValue(tick, gauge);
169 | if (!_shouldHideTickLine)
170 | addTickLine(tick, gauge);
171 | if (!CONSTANTS.debugTicksRadius && !_shouldHideTickValue) {
172 | addTickValue(tick, gauge);
173 | }
174 | }
175 | export const getLabelCoordsByValue = (value: number, gauge: Gauge, centerToArcLengthSubtract = 0) => {
176 | let labels = gauge.props.labels as Labels;
177 | let minValue = gauge.props.minValue as number;
178 | let maxValue = gauge.props.maxValue as number;
179 | let type = labels.tickLabels?.type;
180 | let { x, y } = getCoordByValue(value, gauge, type, centerToArcLengthSubtract, 0.93);
181 | let percent = utils.calculatePercentage(minValue, maxValue, value);
182 | //This corrects labels in the cener being too close from the arc
183 | // let isValueBetweenCenter = percent > CONSTANTS.rangeBetweenCenteredTickValueLabel[0] &&
184 | // percent < CONSTANTS.rangeBetweenCenteredTickValueLabel[1];
185 | // if (isValueBetweenCenter){
186 | // let isInner = type == "inner";
187 | // y+= isInner ? 8 : -1;
188 | // }
189 | if (gauge.props.type == GaugeType.Radial) {
190 | y += 3;
191 | }
192 | return { x, y }
193 | }
194 | export const addText = (html: any, x: number, y: number, gauge: Gauge, style: React.CSSProperties, className: string, rotate = 0) => {
195 | let div = gauge.g.current
196 | .append("g")
197 | .attr("class", className)
198 | .attr("transform", `translate(${x}, ${y})`)
199 | .append("text")
200 | .text(html) // use html() instead of text()
201 | applyTextStyles(div, style)
202 | div.attr("transform", `rotate(${rotate})`);
203 | }
204 |
205 | const applyTextStyles = (div: any, style: React.CSSProperties) => {
206 | //Apply default styles
207 | Object.entries(style).forEach(([key, value]: any) => div.style(utils.camelCaseToKebabCase(key), value))
208 | //Apply custom styles
209 | if (style != undefined) Object.entries(style).forEach(([key, value]: any) => div.style(utils.camelCaseToKebabCase(key), value))
210 | }
211 |
212 | //Adds text undeneath the graft to display which percentage is the current one
213 | export const addValueText = (gauge: Gauge) => {
214 | const { labels } = gauge.props;
215 | let value = gauge.props.value as number;
216 | let valueLabel = labels?.valueLabel as ValueLabel;
217 | var textPadding = 20;
218 | let text = '';
219 | let maxDecimalDigits = labels?.valueLabel?.maxDecimalDigits;
220 | let floatValue = utils.floatingNumber(value, maxDecimalDigits);
221 | if (valueLabel.formatTextValue) {
222 | text = valueLabel.formatTextValue(floatValue);
223 | } else if (gauge.props.minValue === 0 && gauge.props.maxValue === 100) {
224 | text = floatValue.toString();
225 | text += "%";
226 | } else {
227 | text = floatValue.toString();
228 | }
229 | const maxLengthBeforeComputation = 4;
230 | const textLength = text?.length || 0;
231 | let fontRatio = textLength > maxLengthBeforeComputation ? maxLengthBeforeComputation / textLength * 1.5 : 1; // Compute the font size ratio
232 | let valueFontSize = valueLabel?.style?.fontSize as string;
233 | let valueTextStyle = { ...valueLabel.style };
234 | let x = gauge.dimensions.current.outerRadius;
235 | let y = 0;
236 | valueTextStyle.textAnchor = "middle";
237 | if (gauge.props.type == GaugeType.Semicircle) {
238 | y = gauge.dimensions.current.outerRadius / 1.5 + textPadding;
239 | } else if (gauge.props.type == GaugeType.Radial) {
240 | y = gauge.dimensions.current.outerRadius * 1.45 + textPadding;
241 | } else if (gauge.props.type == GaugeType.Grafana) {
242 | y = gauge.dimensions.current.outerRadius * 1.0 + textPadding;
243 | }
244 | //if(gauge.props.pointer.type == PointerType.Arrow){
245 | // y = gauge.dimensions.current.outerRadius * 0.79 + textPadding;
246 | //}
247 | let widthFactor = gauge.props.type == GaugeType.Radial ? 0.003 : 0.003;
248 | fontRatio = gauge.dimensions.current.width * widthFactor * fontRatio;
249 | let fontSizeNumber = parseInt(valueFontSize, 10) * fontRatio;
250 | valueTextStyle.fontSize = fontSizeNumber + "px";
251 | if (valueLabel.matchColorWithArc) valueTextStyle.fill = getArcDataByValue(value, gauge)?.color as string || "white";
252 | addText(text, x, y, gauge, valueTextStyle, CONSTANTS.valueLabelClassname);
253 | };
254 |
255 | export const clearValueLabel = (gauge: Gauge) => gauge.g.current.selectAll(`.${CONSTANTS.valueLabelClassname}`).remove();
256 | export const clearTicks = (gauge: Gauge) => {
257 | gauge.g.current.selectAll(`.${CONSTANTS.tickLineClassname}`).remove();
258 | gauge.g.current.selectAll(`.${CONSTANTS.tickValueClassname}`).remove();
259 | }
260 |
261 | export const calculateAnchorAndAngleByValue = (value: number, gauge: Gauge) => {
262 | const { labels } = gauge.props;
263 | let minValue = gauge.props.minValue as number;
264 | let maxValue = gauge.props.maxValue as number;
265 | let valuePercentage = utils.calculatePercentage(minValue, maxValue, value)
266 | let gaugeTypesAngles: Record = {
267 | [GaugeType.Grafana]: {
268 | startAngle: -20,
269 | endAngle: 220
270 | },
271 | [GaugeType.Semicircle]: {
272 | startAngle: 0,
273 | endAngle: 180
274 | },
275 | [GaugeType.Radial]: {
276 | startAngle: -42,
277 | endAngle: 266
278 | },
279 | };
280 | let { startAngle, endAngle } = gaugeTypesAngles[gauge.props.type as string];
281 |
282 | let angle = startAngle + (valuePercentage * 100) * endAngle / 100;
283 | let isValueLessThanHalf = valuePercentage < 0.5;
284 | //Values between 40% and 60% are aligned in the middle
285 | let isValueBetweenTolerance = valuePercentage > CONSTANTS.rangeBetweenCenteredTickValueLabel[0] &&
286 | valuePercentage < CONSTANTS.rangeBetweenCenteredTickValueLabel[1];
287 | let tickAnchor = '';
288 | let isInner = labels?.tickLabels?.type == "inner";
289 | if (isValueBetweenTolerance) {
290 | tickAnchor = "middle";
291 | } else if (isValueLessThanHalf) {
292 | tickAnchor = isInner ? "start" : "end";
293 | } else {
294 | tickAnchor = isInner ? "end" : "start";
295 | }
296 | // if(valuePercentage > 0.50) angle = angle - 180;
297 | return { tickAnchor, angle };
298 | }
299 | const shouldHideTickLine = (tick: Tick, gauge: Gauge): boolean => {
300 | const { labels } = gauge.props;
301 | var defaultHideValue = defaultTickLabels.defaultTickLineConfig?.hide;
302 | var shouldHide = defaultHideValue;
303 | var defaultHideLineFromUser = labels?.tickLabels?.defaultTickLineConfig?.hide;
304 | if (defaultHideLineFromUser != undefined) {
305 | shouldHide = defaultHideLineFromUser;
306 | }
307 | var specificHideValueFromUser = tick.lineConfig?.hide;
308 | if (specificHideValueFromUser != undefined) {
309 | shouldHide = specificHideValueFromUser;
310 | }
311 | return shouldHide as boolean;
312 | }
313 | const shouldHideTickValue = (tick: Tick, gauge: Gauge): boolean => {
314 | const { labels } = gauge.props;
315 | var defaultHideValue = defaultTickLabels.defaultTickValueConfig?.hide;
316 | var shouldHide = defaultHideValue;
317 | var defaultHideValueFromUser = labels?.tickLabels?.defaultTickValueConfig?.hide;
318 | if (defaultHideValueFromUser != undefined) {
319 | shouldHide = defaultHideValueFromUser;
320 | }
321 | var specificHideValueFromUser = tick.valueConfig?.hide;
322 | if (specificHideValueFromUser != undefined) {
323 | shouldHide = specificHideValueFromUser;
324 | }
325 | return shouldHide as boolean;
326 | }
--------------------------------------------------------------------------------
/src/lib/GaugeComponent/hooks/pointer.ts:
--------------------------------------------------------------------------------
1 | import {
2 | easeElastic,
3 | easeExpOut,
4 | interpolateNumber,
5 | } from "d3";
6 | import { PointerContext, PointerProps, PointerType } from "../types/Pointer";
7 | import { getCoordByValue } from "./arc";
8 | import { Gauge } from "../types/Gauge";
9 | import * as utils from "./utils";
10 | import * as arcHooks from "./arc";
11 | import { GaugeType } from "../types/GaugeComponentProps";
12 |
13 | export const drawPointer = (gauge: Gauge, resize: boolean = false) => {
14 | gauge.pointer.current.context = setupContext(gauge);
15 | const { prevPercent, currentPercent, prevProgress } = gauge.pointer.current.context;
16 | let pointer = gauge.props.pointer as PointerProps;
17 | let isFirstTime = gauge.prevProps?.current.value == undefined;
18 | if ((isFirstTime || resize) && gauge.props.type != GaugeType.Grafana)
19 | initPointer(gauge);
20 | let shouldAnimate = (!resize || isFirstTime) && pointer.animate;
21 | if (shouldAnimate) {
22 | gauge.doughnut.current
23 | .transition()
24 | .delay(pointer.animationDelay)
25 | .ease(pointer.elastic ? easeElastic : easeExpOut)
26 | .duration(pointer.animationDuration)
27 | .tween("progress", () => {
28 | const currentInterpolatedPercent = interpolateNumber(prevPercent, currentPercent);
29 | return function (percentOfPercent: number) {
30 | const progress = currentInterpolatedPercent(percentOfPercent);
31 | if (isProgressValid(progress, prevProgress, gauge)) {
32 | if(gauge.props.type == GaugeType.Grafana){
33 | arcHooks.clearArcs(gauge);
34 | arcHooks.drawArc(gauge, progress);
35 | //arcHooks.setupArcs(gauge);
36 | } else {
37 | updatePointer(progress, gauge);
38 | }
39 | }
40 | gauge.pointer.current.context.prevProgress = progress;
41 | };
42 | });
43 | } else {
44 | updatePointer(currentPercent, gauge);
45 | }
46 | };
47 | const setupContext = (gauge: Gauge): PointerContext => {
48 | const { value } = gauge.props;
49 | let pointer = gauge.props.pointer as PointerProps;
50 | let pointerLength = pointer.length as number;
51 | let minValue = gauge.props.minValue as number;
52 | let maxValue = gauge.props.maxValue as number;
53 | const { pointerPath } = gauge.pointer.current.context;
54 | var pointerRadius = getPointerRadius(gauge)
55 | let length = pointer.type == PointerType.Needle ? pointerLength : 0.2;
56 | let typesWithPath = [PointerType.Needle, PointerType.Arrow];
57 | let pointerContext: PointerContext = {
58 | centerPoint: [0, -pointerRadius / 2],
59 | pointerRadius: getPointerRadius(gauge),
60 | pathLength: gauge.dimensions.current.outerRadius * length,
61 | currentPercent: utils.calculatePercentage(minValue, maxValue, value as number),
62 | prevPercent: utils.calculatePercentage(minValue, maxValue, gauge.prevProps?.current.value || minValue),
63 | prevProgress: 0,
64 | pathStr: "",
65 | shouldDrawPath: typesWithPath.includes(pointer.type as PointerType),
66 | prevColor: ""
67 | }
68 | return pointerContext;
69 | }
70 | const initPointer = (gauge: Gauge) => {
71 | let value = gauge.props.value as number;
72 | let pointer = gauge.props.pointer as PointerProps;
73 | const { shouldDrawPath, centerPoint, pointerRadius, pathStr, currentPercent, prevPercent } = gauge.pointer.current.context;
74 | if(shouldDrawPath){
75 | gauge.pointer.current.context.pathStr = calculatePointerPath(gauge, prevPercent || currentPercent);
76 | gauge.pointer.current.path = gauge.pointer.current.element.append("path").attr("d", gauge.pointer.current.context.pathStr).attr("fill", pointer.color);
77 | }
78 | //Add a circle at the bottom of pointer
79 | if (pointer.type == PointerType.Needle) {
80 | gauge.pointer.current.element
81 | .append("circle")
82 | .attr("cx", centerPoint[0])
83 | .attr("cy", centerPoint[1])
84 | .attr("r", pointerRadius)
85 | .attr("fill", pointer.color);
86 | } else if (pointer.type == PointerType.Blob) {
87 | gauge.pointer.current.element
88 | .append("circle")
89 | .attr("cx", centerPoint[0])
90 | .attr("cy", centerPoint[1])
91 | .attr("r", pointerRadius)
92 | .attr("fill", pointer.baseColor)
93 | .attr("stroke", pointer.color)
94 | .attr("stroke-width", pointer.strokeWidth! * pointerRadius / 10);
95 | }
96 | //Translate the pointer starting point of the arc
97 | setPointerPosition(pointerRadius, value, gauge);
98 | }
99 | const updatePointer = (percentage: number, gauge: Gauge) => {
100 | let pointer = gauge.props.pointer as PointerProps;
101 | const { pointerRadius, shouldDrawPath, prevColor } = gauge.pointer.current.context;
102 | setPointerPosition(pointerRadius, percentage, gauge);
103 | if(shouldDrawPath && gauge.props.type != GaugeType.Grafana)
104 | gauge.pointer.current.path.attr("d", calculatePointerPath(gauge, percentage));
105 | if(pointer.type == PointerType.Blob) {
106 | let currentColor = arcHooks.getArcDataByPercentage(percentage, gauge)?.color as string;
107 | let shouldChangeColor = currentColor != prevColor;
108 | if(shouldChangeColor) gauge.pointer.current.element.select("circle").attr("stroke", currentColor)
109 | var strokeWidth = pointer.strokeWidth! * pointerRadius / 10;
110 | gauge.pointer.current.element.select("circle").attr("stroke-width", strokeWidth);
111 | gauge.pointer.current.context.prevColor = currentColor;
112 | }
113 | }
114 | const setPointerPosition = (pointerRadius: number, progress: number, gauge: Gauge) => {
115 | let pointer = gauge.props.pointer as PointerProps;
116 | let pointerType = pointer.type as string;
117 | const { dimensions } = gauge;
118 | let value = utils.getCurrentGaugeValueByPercentage(progress, gauge);
119 | let pointers: { [key: string]: () => void } = {
120 | [PointerType.Needle]: () => {
121 | // Set needle position to center
122 | translatePointer(dimensions.current.outerRadius,dimensions.current.outerRadius, gauge);
123 | },
124 | [PointerType.Arrow]: () => {
125 | let { x, y } = getCoordByValue(value, gauge, "inner", 0, 0.70);
126 | x -= 1;
127 | y += pointerRadius-3;
128 | translatePointer(x, y, gauge);
129 | },
130 | [PointerType.Blob]: () => {
131 | let { x, y } = getCoordByValue(value, gauge, "between", 0, 0.75);
132 | x -= 1;
133 | y += pointerRadius;
134 | translatePointer(x, y, gauge);
135 | },
136 | };
137 | return pointers[pointerType]();
138 | }
139 |
140 | const isProgressValid = (currentPercent: number, prevPercent: number, gauge: Gauge) => {
141 | //Avoid unnecessary re-rendering (when progress is too small) but allow the pointer to reach the final value
142 | let overFlow = currentPercent > 1 || currentPercent < 0;
143 | let tooSmallValue = Math.abs(currentPercent - prevPercent) < 0.0001;
144 | let sameValueAsBefore = currentPercent == prevPercent;
145 | return !tooSmallValue && !sameValueAsBefore && !overFlow;
146 | }
147 |
148 | const calculatePointerPath = (gauge: Gauge, percent: number) => {
149 | const { centerPoint, pointerRadius, pathLength } = gauge.pointer.current.context;
150 | let startAngle = utils.degToRad(gauge.props.type == GaugeType.Semicircle ? 0 : -42);
151 | let endAngle = utils.degToRad(gauge.props.type == GaugeType.Semicircle ? 180 : 223);
152 | const angle = startAngle + (percent) * (endAngle - startAngle);
153 | var topPoint = [
154 | centerPoint[0] - pathLength * Math.cos(angle),
155 | centerPoint[1] - pathLength * Math.sin(angle),
156 | ];
157 | var thetaMinusHalfPi = angle - Math.PI / 2;
158 | var leftPoint = [
159 | centerPoint[0] - pointerRadius * Math.cos(thetaMinusHalfPi),
160 | centerPoint[1] - pointerRadius * Math.sin(thetaMinusHalfPi),
161 | ];
162 | var thetaPlusHalfPi = angle + Math.PI / 2;
163 | var rightPoint = [
164 | centerPoint[0] - pointerRadius * Math.cos(thetaPlusHalfPi),
165 | centerPoint[1] - pointerRadius * Math.sin(thetaPlusHalfPi),
166 | ];
167 |
168 | var pathStr = `M ${leftPoint[0]} ${leftPoint[1]} L ${topPoint[0]} ${topPoint[1]} L ${rightPoint[0]} ${rightPoint[1]}`;
169 | return pathStr;
170 | };
171 |
172 | const getPointerRadius = (gauge: Gauge) => {
173 | let pointer = gauge.props.pointer as PointerProps;
174 | let pointerWidth = pointer.width as number;
175 | return pointerWidth * (gauge.dimensions.current.width / 500);
176 | }
177 |
178 | export const translatePointer = (x: number, y: number, gauge: Gauge) => gauge.pointer.current.element.attr("transform", "translate(" + x + ", " + y + ")");
179 | export const addPointerElement = (gauge: Gauge) => gauge.pointer.current.element = gauge.g.current.append("g").attr("class", "pointer");
180 | export const clearPointerElement = (gauge: Gauge) => gauge.pointer.current.element.selectAll("*").remove();
181 |
--------------------------------------------------------------------------------
/src/lib/GaugeComponent/hooks/utils.ts:
--------------------------------------------------------------------------------
1 | import { Gauge } from '../types/Gauge';
2 | import { GaugeComponentProps } from '../types/GaugeComponentProps';
3 | export const calculatePercentage = (minValue: number, maxValue: number, value: number) => {
4 | if (value < minValue) {
5 | return 0;
6 | } else if (value > maxValue) {
7 | return 1;
8 | } else {
9 | let percentage = (value - minValue) / (maxValue - minValue)
10 | return (percentage);
11 | }
12 | }
13 | export const isEmptyObject = (obj: any) => {
14 | return Object.keys(obj).length === 0 && obj.constructor === Object;
15 | }
16 | export const mergeObjects = (obj1: any, obj2: Partial): any => {
17 | const mergedObj = { ...obj1 } as any;
18 |
19 | Object.keys(obj2).forEach(key => {
20 | const val1 = obj1[key];
21 | const val2 = obj2[key];
22 |
23 | if (Array.isArray(val1) && Array.isArray(val2)) {
24 | mergedObj[key] = val2;
25 | } else if (typeof val1 === 'object' && typeof val2 === 'object') {
26 | mergedObj[key] = mergeObjects(val1, val2);
27 | } else if (val2 !== undefined) {
28 | mergedObj[key] = val2;
29 | }
30 | });
31 |
32 | return mergedObj;
33 | }
34 | //Returns the angle (in rad) for the given 'percent' value where percent = 1 means 100% and is 180 degree angle
35 | export const percentToRad = (percent: number, angle: number) => {
36 | return percent * (Math.PI / angle);
37 | };
38 |
39 | export const floatingNumber = (value: number, maxDigits = 2) => {
40 | return Math.round(value * 10 ** maxDigits) / 10 ** maxDigits;
41 | };
42 | // Function to normalize a value between a new min and max
43 | export function normalize(value: number, min: number, max: number) {
44 | return ((value - min) / (max - min)) * 100;
45 | }
46 | export const degToRad = (degrees: number) => {
47 | return degrees * (Math.PI / 180);
48 | }
49 | export const getCurrentGaugePercentageByValue = (value: number, gauge: GaugeComponentProps) => calculatePercentage(gauge.minValue as number, gauge.maxValue as number, value);
50 | export const getCurrentGaugeValueByPercentage = (percentage: number, gauge: Gauge) => {
51 | let minValue = gauge.props.minValue as number;
52 | let maxValue = gauge.props.maxValue as number;
53 | let value = minValue + (percentage) * (maxValue - minValue);
54 | return value;
55 | }
56 | export const camelCaseToKebabCase = (str: string): string => str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
--------------------------------------------------------------------------------
/src/lib/GaugeComponent/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useLayoutEffect, Suspense } from "react";
2 | import { pie, select } from "d3";
3 | import { defaultGaugeProps, GaugeComponentProps, GaugeType, getGaugeMarginByType } from "./types/GaugeComponentProps";
4 | import { Gauge } from "./types/Gauge";
5 | import * as chartHooks from "./hooks/chart";
6 | import * as arcHooks from "./hooks/arc";
7 | import { isEmptyObject, mergeObjects } from "./hooks/utils";
8 | import { Dimensions, defaultDimensions } from "./types/Dimensions";
9 | import { PointerRef, defaultPointerRef } from "./types/Pointer";
10 | import { Arc, getArcWidthByType } from "./types/Arc";
11 | import { debounce } from "lodash";
12 | /*
13 | GaugeComponent creates a gauge chart using D3
14 | The chart is responsive and will have the same width as the "container"
15 | The radius of the gauge depends on the width and height of the container
16 | It will use whichever is smallest of width or height
17 | The svg element surrounding the gauge will always be square
18 | "container" is the div where the chart should be placed
19 | */
20 | const GaugeComponent = (props: Partial) => {
21 | const svg = useRef({});
22 | const tooltip = useRef({});
23 | const g = useRef({});
24 | const doughnut = useRef({});
25 | const isFirstRun = useRef(true);
26 | const currentProgress = useRef(0);
27 | const pointer = useRef({ ...defaultPointerRef });
28 | const container = useRef({});
29 | const arcData = useRef([]);
30 | const pieChart = useRef(pie());
31 | const dimensions = useRef({ ...defaultDimensions });
32 | const mergedProps = useRef(props as GaugeComponentProps);
33 | const prevProps = useRef({});
34 | const resizeObserver = useRef({});
35 | let selectedRef = useRef(null);
36 |
37 | var gauge: Gauge = {
38 | props: mergedProps.current,
39 | resizeObserver,
40 | prevProps,
41 | svg,
42 | g,
43 | dimensions,
44 | doughnut,
45 | isFirstRun,
46 | currentProgress,
47 | pointer,
48 | container,
49 | arcData,
50 | pieChart,
51 | tooltip
52 | };
53 | //Merged properties will get the default props and overwrite by the user's defined props
54 | //To keep the original default props in the object
55 | const updateMergedProps = () => {
56 | let defaultValues = { ...defaultGaugeProps };
57 | gauge.props = mergedProps.current = mergeObjects(defaultValues, props);
58 | if (gauge.props.arc?.width == defaultGaugeProps.arc?.width) {
59 | let mergedArc = mergedProps.current.arc as Arc;
60 | mergedArc.width = getArcWidthByType(gauge.props.type as GaugeType);
61 | }
62 | if (gauge.props.marginInPercent == defaultGaugeProps.marginInPercent) mergedProps.current.marginInPercent = getGaugeMarginByType(gauge.props.type as GaugeType);
63 | arcHooks.validateArcs(gauge);
64 | }
65 |
66 | const shouldInitChart = () => {
67 | let arcsPropsChanged = (JSON.stringify(prevProps.current.arc) !== JSON.stringify(mergedProps.current.arc));
68 | let pointerPropsChanged = (JSON.stringify(prevProps.current.pointer) !== JSON.stringify(mergedProps.current.pointer));
69 | let valueChanged = (JSON.stringify(prevProps.current.value) !== JSON.stringify(mergedProps.current.value));
70 | let minValueChanged = (JSON.stringify(prevProps.current.minValue) !== JSON.stringify(mergedProps.current.minValue));
71 | let maxValueChanged = (JSON.stringify(prevProps.current.maxValue) !== JSON.stringify(mergedProps.current.maxValue));
72 | return arcsPropsChanged || pointerPropsChanged || valueChanged || minValueChanged || maxValueChanged;
73 | }
74 | useLayoutEffect(() => {
75 | updateMergedProps();
76 | isFirstRun.current = isEmptyObject(container.current)
77 | if (isFirstRun.current) container.current = select(selectedRef.current);
78 | if (shouldInitChart()) chartHooks.initChart(gauge, isFirstRun.current);
79 | gauge.prevProps.current = mergedProps.current;
80 | }, [props]);
81 |
82 | // useEffect(() => {
83 | // const observer = new MutationObserver(function () {
84 | // setTimeout(() => window.dispatchEvent(new Event('resize')), 10);
85 | // if (!selectedRef.current?.offsetParent) return;
86 |
87 | // chartHooks.renderChart(gauge, true);
88 | // observer.disconnect()
89 | // });
90 | // observer.observe(selectedRef.current?.parentNode, {attributes: true, subtree: false});
91 | // return () => observer.disconnect();
92 | // }, [selectedRef.current?.parentNode?.offsetWidth, selectedRef.current?.parentNode?.offsetHeight]);
93 |
94 | useEffect(() => {
95 | const handleResize = () => chartHooks.renderChart(gauge, true);
96 | //Set up resize event listener to re-render the chart everytime the window is resized
97 | window.addEventListener("resize", handleResize);
98 | return () => window.removeEventListener("resize", handleResize);
99 | }, [props]);
100 |
101 | // useEffect(() => {
102 | // console.log(selectedRef.current?.offsetWidth)
103 | // // workaround to trigger recomputing of gauge size on first load (e.g. F5)
104 | // setTimeout(() => window.dispatchEvent(new Event('resize')), 10);
105 | // }, [selectedRef.current?.parentNode]);
106 | useEffect(() => {
107 | const element = selectedRef.current;
108 | if (!element) return;
109 |
110 | // Create observer instance
111 | const observer = new ResizeObserver(() => {
112 | chartHooks.renderChart(gauge, true);
113 | });
114 |
115 | // Store observer reference
116 | gauge.resizeObserver.current = observer;
117 |
118 | // Observe parent node
119 | if (element.parentNode) {
120 | observer.observe(element.parentNode);
121 | }
122 |
123 | // Cleanup
124 | return () => {
125 | if (gauge.resizeObserver) {
126 | gauge.resizeObserver.current?.disconnect();
127 | delete gauge.resizeObserver.current;
128 | }
129 | };
130 | }, []);
131 |
132 | const { id, style, className, type } = props;
133 | return (
134 | (selectedRef.current = svg)}
139 | />
140 | );
141 | };
142 | export { GaugeComponent };
143 | export default GaugeComponent;
--------------------------------------------------------------------------------
/src/lib/GaugeComponent/types/Arc.ts:
--------------------------------------------------------------------------------
1 | import { GaugeType, defaultGaugeProps } from './GaugeComponentProps';
2 | import { Tooltip } from './Tooltip';
3 | export interface Arc {
4 | /** The corner radius of the arc. */
5 | cornerRadius?: number,
6 | /** The padding between subArcs, in rad. */
7 | padding?: number,
8 | /** The width of the arc given in percent of the radius. */
9 | width?: number,
10 | /** The number of subArcs, this overrides "subArcs" limits. */
11 | nbSubArcs?: number,
12 | /** Boolean flag that enables or disables gradient mode, which
13 | * draws a single arc with provided colors. */
14 | gradient?: boolean,
15 | /** The colors of the arcs, this overrides "subArcs" colors. */
16 | colorArray?: Array
,
17 | /** Color of the grafana's empty subArc */
18 | emptyColor?: string,
19 | /** list of sub arcs segments of the whole arc. */
20 | subArcs?: Array
21 | }
22 | export interface SubArc {
23 | /** The limit of the subArc, in accord to the gauge value. */
24 | limit?: number,
25 | /** The color of the subArc */
26 | color?: string | number,
27 | /** The length of the subArc, in percent */
28 | length?: number,
29 | // needleColorWhenWithinLimit?: string, //The color of the needle when it is within the subArc
30 | /** Whether or not to show the tick */
31 | showTick?: boolean,
32 | /** Tooltip that appears onHover of the subArc */
33 | tooltip?: Tooltip,
34 | /** This will trigger onClick of the subArc */
35 | onClick?: () => void,
36 | /** This will trigger onMouseMove of the subArc */
37 | onMouseMove?: () => void,
38 | /** This will trigger onMouseMove of the subArc */
39 | onMouseLeave?: () => void
40 | }
41 | export const defaultSubArcs: SubArc[] = [
42 | { limit: 33, color: "#5BE12C" }, // needleColorWhenWithinLimit: "#AA4128"},
43 | { limit: 66, color: "#F5CD19" },
44 | { color: "#EA4228" },
45 | ];
46 |
47 | export const getArcWidthByType = (type: string): number => {
48 | let gaugeTypesWidth: Record = {
49 | [GaugeType.Grafana]: 0.25,
50 | [GaugeType.Semicircle]: 0.15,
51 | [GaugeType.Radial]: 0.2,
52 | };
53 | if(!type) type = defaultGaugeProps.type as string;
54 | return gaugeTypesWidth[type as string];
55 | }
56 | export const defaultArc: Arc = {
57 | padding: 0.05,
58 | width: 0.25,
59 | cornerRadius: 7,
60 | nbSubArcs: undefined,
61 | emptyColor: "#5C5C5C",
62 | colorArray: undefined,
63 | subArcs: defaultSubArcs,
64 | gradient: false
65 | };
--------------------------------------------------------------------------------
/src/lib/GaugeComponent/types/Dimensions.ts:
--------------------------------------------------------------------------------
1 | export interface Margin {
2 | top: number;
3 | right: number;
4 | bottom: number;
5 | left: number;
6 | }
7 | export interface Angles {
8 | startAngle: number;
9 | endAngle: number;
10 | startAngleDeg: number;
11 | endAngleDeg: number;
12 | }
13 | export interface Dimensions {
14 | width: number;
15 | height: number;
16 | margin: Margin;
17 | angles: Angles;
18 | outerRadius: number;
19 | innerRadius: number;
20 | fixedHeight: number;
21 | }
22 | export const defaultMargins: Margin = {
23 | top: 0,
24 | right: 0,
25 | bottom: 0,
26 | left: 0
27 | }
28 | export const defaultAngles: Angles = {
29 | startAngle: 0,
30 | endAngle: 0,
31 | startAngleDeg: 0,
32 | endAngleDeg: 0
33 | }
34 | export const defaultDimensions: Dimensions = {
35 | width: 0,
36 | height: 0,
37 | margin: defaultMargins,
38 | outerRadius: 0,
39 | innerRadius: 0,
40 | angles: defaultAngles,
41 | fixedHeight: 0
42 | }
--------------------------------------------------------------------------------
/src/lib/GaugeComponent/types/Gauge.ts:
--------------------------------------------------------------------------------
1 | import { GaugeComponentProps } from './GaugeComponentProps';
2 | import { SubArc } from './Arc';
3 | import { Dimensions } from './Dimensions';
4 | export interface Gauge {
5 | props: GaugeComponentProps;
6 | prevProps: React.MutableRefObject;
7 | svg: React.MutableRefObject;
8 | g: React.MutableRefObject;
9 | doughnut: React.MutableRefObject;
10 | resizeObserver : React.MutableRefObject;
11 | pointer: React.MutableRefObject;
12 | container: React.MutableRefObject;
13 | isFirstRun: React.MutableRefObject;
14 | currentProgress: React.MutableRefObject;
15 | dimensions: React.MutableRefObject;
16 | //This holds the computed data for the arcs, computed only once and then reused without changing original props to avoid render problems
17 | arcData: React.MutableRefObject;
18 | pieChart: React.MutableRefObject;
19 | //This holds the only tooltip element rendered for any given gauge chart to use
20 | tooltip: React.MutableRefObject;
21 | }
22 |
--------------------------------------------------------------------------------
/src/lib/GaugeComponent/types/GaugeComponentProps.ts:
--------------------------------------------------------------------------------
1 | import { Arc, defaultArc } from "./Arc";
2 | import { Labels, defaultLabels } from './Labels';
3 | import { PointerProps, defaultPointer } from "./Pointer";
4 | export enum GaugeType {
5 | Semicircle = "semicircle",
6 | Radial = "radial",
7 | Grafana = "grafana"
8 | }
9 | export interface GaugeInnerMarginInPercent {
10 | top: number,
11 | bottom: number,
12 | left: number,
13 | right: number
14 | }
15 | export interface GaugeComponentProps {
16 | /** Gauge element will inherit this. */
17 | id?: string,
18 | /** Gauge element will inherit this. */
19 | className?: string,
20 | /** Gauge element will inherit this. */
21 | style?: React.CSSProperties,
22 | /** This configures the canvas margin in relationship with the gauge.
23 | * Default values:
24 | * [GaugeType.Grafana]: { top: 0.12, bottom: 0.00, left: 0.07, right: 0.07 },
25 | [GaugeType.Semicircle]: { top: 0.08, bottom: 0.00, left: 0.07, right: 0.07 },
26 | [GaugeType.Radial]: { top: 0.07, bottom: 0.00, left: 0.07, right: 0.07 },
27 | */
28 | marginInPercent?: GaugeInnerMarginInPercent | number,
29 | /** Current pointer value. */
30 | value?: number,
31 | /** Minimum value possible for the Gauge. */
32 | minValue?: number,
33 | /** Maximum value possible for the Gauge. */
34 | maxValue?: number,
35 | /** This configures the arc of the Gauge. */
36 | arc?: Arc,
37 | /** This configures the labels of the Gauge. */
38 | labels?: Labels,
39 | /** This configures the pointer of the Gauge. */
40 | pointer?: PointerProps,
41 | /** This configures the type of the Gauge. */
42 | type?: "semicircle" | "radial" | "grafana"
43 | }
44 |
45 | export const defaultGaugeProps: GaugeComponentProps = {
46 | id: "",
47 | className: "gauge-component-class",
48 | style: { width: "100%"},
49 | marginInPercent: 0.07,
50 | value: 33,
51 | minValue: 0,
52 | maxValue: 100,
53 | arc: defaultArc,
54 | labels: defaultLabels,
55 | pointer: defaultPointer,
56 | type: GaugeType.Grafana
57 | }
58 | export const getGaugeMarginByType = (type: string): GaugeInnerMarginInPercent | number => {
59 | let gaugeTypesMargin: Record = {
60 | [GaugeType.Grafana]: { top: 0.12, bottom: 0.00, left: 0.07, right: 0.07 },
61 | [GaugeType.Semicircle]: { top: 0.08, bottom: 0.00, left: 0.08, right: 0.08 },
62 | [GaugeType.Radial]: { top: 0.07, bottom: 0.00, left: 0.07, right: 0.07 },
63 | };
64 | return gaugeTypesMargin[type as string];
65 | }
--------------------------------------------------------------------------------
/src/lib/GaugeComponent/types/Labels.ts:
--------------------------------------------------------------------------------
1 | import { defaultTickLabels, TickLabels } from './Tick';
2 | export interface Labels {
3 | /** This configures the central value label. */
4 | valueLabel?: ValueLabel,
5 | /** This configures the ticks and it's values labels. */
6 | tickLabels?: TickLabels
7 | }
8 |
9 | export interface ValueLabel {
10 | /** This function enables to format the central value text as you wish. */
11 | formatTextValue?: (value: any) => string;
12 | /** This will sync the value label color with the current value of the Gauge. */
13 | matchColorWithArc?: boolean;
14 | /** This enables configuration for the number of decimal digits of the
15 | * central value label */
16 | maxDecimalDigits?: number;
17 | /** Central label value will inherit this */
18 | style?: React.CSSProperties;
19 | /** This hides the central value label if true */
20 | hide?: boolean;
21 | }
22 |
23 | export const defaultValueLabel: ValueLabel = {
24 | formatTextValue: undefined,
25 | matchColorWithArc: false,
26 | maxDecimalDigits: 2,
27 | style: {
28 | fontSize: "35px",
29 | fill: '#fff',
30 | textShadow: "black 1px 0.5px 0px, black 0px 0px 0.03em, black 0px 0px 0.01em"
31 | },
32 | hide: false
33 | }
34 | export const defaultLabels: Labels = {
35 | valueLabel: defaultValueLabel,
36 | tickLabels: defaultTickLabels
37 | }
--------------------------------------------------------------------------------
/src/lib/GaugeComponent/types/Pointer.ts:
--------------------------------------------------------------------------------
1 | export interface PointerProps {
2 | /** Pointer type */
3 | type?: "needle" | "blob" | "arrow",
4 | /** Pointer color */
5 | color?: string,
6 | /** Enabling this flag will hide the pointer */
7 | hide?: boolean,
8 | /** Pointer color of the central circle */
9 | baseColor?: string,
10 | /** Pointer length */
11 | length?: number,
12 | /** This is a factor to multiply by the width of the gauge */
13 | width?: number,
14 | /** This enables pointer animation for transiction between values when enabled */
15 | animate?: boolean,
16 | /** This gives animation an elastic transiction between values */
17 | elastic?: boolean,
18 | /** Animation duration in ms */
19 | animationDuration?: number,
20 | /** Animation delay in ms */
21 | animationDelay?: number,
22 | /** Stroke width of the pointer */
23 | strokeWidth?: number
24 | }
25 | export interface PointerRef {
26 | element: any,
27 | path: any,
28 | context: PointerContext
29 | }
30 | export interface PointerContext {
31 | centerPoint: number[],
32 | pointerRadius: number,
33 | pathLength: number,
34 | currentPercent: number,
35 | prevPercent: number,
36 | prevProgress: number,
37 | pathStr: string,
38 | shouldDrawPath: boolean,
39 | prevColor: string
40 | }
41 | export enum PointerType {
42 | Needle = "needle",
43 | Blob = "blob",
44 | Arrow = "arrow"
45 | }
46 | export const defaultPointerContext: PointerContext = {
47 | centerPoint: [0, 0],
48 | pointerRadius: 0,
49 | pathLength: 0,
50 | currentPercent: 0,
51 | prevPercent: 0,
52 | prevProgress: 0,
53 | pathStr: "",
54 | shouldDrawPath: false,
55 | prevColor: ""
56 | }
57 | export const defaultPointerRef: PointerRef = {
58 | element: undefined,
59 | path: undefined,
60 | context: defaultPointerContext
61 | }
62 | export const defaultPointer: PointerProps = {
63 | type: PointerType.Needle,
64 | color: "#5A5A5A",
65 | baseColor: "white",
66 | length: 0.70,
67 | width: 20, // this is a factor to multiply by the width of the gauge
68 | animate: true,
69 | elastic: false,
70 | hide: false,
71 | animationDuration: 3000,
72 | animationDelay: 100,
73 | strokeWidth: 8
74 | }
75 |
--------------------------------------------------------------------------------
/src/lib/GaugeComponent/types/Tick.ts:
--------------------------------------------------------------------------------
1 | export interface TickLabels {
2 | /** Hide first and last ticks and it's values */
3 | hideMinMax?: boolean;
4 | /** Wheter the ticks are inside or outside the arcs */
5 | type?: "inner" | "outer";
6 | /** List of desired ticks */
7 | ticks?: Array;
8 | /** Default tick value label configs, this will apply to all
9 | * ticks but the individually configured */
10 | defaultTickValueConfig?: TickValueConfig;
11 | /** Default tick line label configs, this will apply to all
12 | * ticks but the individually configured */
13 | defaultTickLineConfig?: TickLineConfig;
14 | }
15 | export interface Tick {
16 | /** The value the tick will correspond to */
17 | value?: number;
18 | /** This will override defaultTickValueConfig */
19 | valueConfig?: TickValueConfig;
20 | /** This will override defaultTickLineConfig */
21 | lineConfig?: TickLineConfig;
22 | }
23 | export interface TickValueConfig {
24 | /** This function allows to customize the rendered tickValue label */
25 | formatTextValue?: (value: any) => string;
26 | /** This enables configuration for the number of decimal digits of the
27 | * central value label */
28 | maxDecimalDigits?: number;
29 | /** The tick value label will inherit this */
30 | style?: React.CSSProperties;
31 | /** If true will hide the tick value label */
32 | hide?: boolean;
33 | }
34 | export interface TickLineConfig {
35 | /** The width of the tick's line */
36 | width?: number;
37 | /** The length of the tick's line */
38 | length?: number;
39 | /** The distance of the tick's line from the arc */
40 | distanceFromArc?: number;
41 | /** The color of the tick's line */
42 | color?: string;
43 | /** If true will hide the tick line */
44 | hide?: boolean;
45 | }
46 |
47 | const defaultTickLineConfig: TickLineConfig = {
48 | color: "rgb(173 172 171)",
49 | length: 7,
50 | width: 1,
51 | distanceFromArc: 3,
52 | hide: false
53 | };
54 |
55 | const defaultTickValueConfig: TickValueConfig = {
56 | formatTextValue: undefined,
57 | maxDecimalDigits: 2,
58 | style:{
59 | fontSize: "10px",
60 | fill: "rgb(173 172 171)",
61 | },
62 | hide: false,
63 | };
64 | const defaultTickList: Tick[] = [];
65 | export const defaultTickLabels: TickLabels = {
66 | type: 'outer',
67 | hideMinMax: false,
68 | ticks: defaultTickList,
69 | defaultTickValueConfig: defaultTickValueConfig,
70 | defaultTickLineConfig: defaultTickLineConfig
71 | };
--------------------------------------------------------------------------------
/src/lib/GaugeComponent/types/Tooltip.ts:
--------------------------------------------------------------------------------
1 | export interface Tooltip {
2 | style?: React.CSSProperties,
3 | text?: string
4 | }
5 |
6 | export const defaultTooltipStyle = {
7 | borderColor: '#5A5A5A',
8 | borderStyle: 'solid',
9 | borderWidth: '1px',
10 | borderRadius: '5px',
11 | color: 'white',
12 | padding: '5px',
13 | fontSize: '15px',
14 | textShadow: '1px 1px 2px black, 0 0 1em black, 0 0 0.2em black'
15 |
16 | // fontSize: '15px'
17 | }
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | import GaugeComponent from './GaugeComponent';
2 | export type { GaugeComponentProps, GaugeType } from './GaugeComponent/types/GaugeComponentProps';
3 | export type { Arc, SubArc } from './GaugeComponent/types/Arc';
4 | export type { Tooltip } from './GaugeComponent/types/Tooltip';
5 | export type { Labels, ValueLabel } from './GaugeComponent/types/Labels';
6 | export type { PointerContext, PointerProps, PointerRef, PointerType } from './GaugeComponent/types/Pointer';
7 | export type { Tick, TickLabels, TickLineConfig, TickValueConfig } from './GaugeComponent/types/Tick';
8 | export type { Angles, Dimensions, Margin } from './GaugeComponent/types/Dimensions';
9 | export { GaugeComponent };
10 | export default GaugeComponent;
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "jsx": "react",
6 | "declaration": true,
7 | "strict": true,
8 | "outDir": "dist",
9 | "esModuleInterop": true,
10 | "rootDir": "src",
11 | "sourceMap": false,
12 | "noEmit": false
13 | },
14 | "include": ["src/lib/**/*"]
15 | }
--------------------------------------------------------------------------------