├── .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 | 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 | ![Image of Simple Grafana Gauge Component for a simple data visualization](https://antoniolago.github.io/react-gauge-component/images/simpleGauge.jpg "Simple Grafana Gauge Component") 55 |
56 | Show Simple Gauge code 57 | 58 | ### Simple Gauge 59 | 60 | ```jsx 61 | 88 | ``` 89 |
90 | 91 | ### Custom Bandwidth Gauge. 92 | ![Image of Gauge Component for bandwidth visualization](https://antoniolago.github.io/react-gauge-component/images/bandGauge.jpg "Gauge Component for bandwidth visualization") 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 | ![Image of React Gauge Component for temperature visualization](https://antoniolago.github.io/react-gauge-component/images/tempGauge.jpg "Gauge Component for temperature visualization") 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 | ![Image of Blob Gauge Component for a simple data visualization](https://antoniolago.github.io/react-gauge-component/images/blobGauge.jpg "Blob Gauge Component") 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 | ![Image of Gradient with Arrow Gauge Component for a simple data visualization](https://antoniolago.github.io/react-gauge-component/images/arrowGauge.jpg "Gradient with Arrow Gauge Component") 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 | ![Image of Radial Gauge Component for a simple data visualization](https://antoniolago.github.io/react-gauge-component/images/radialGauge.jpg "Radial Gauge Component") 314 |
315 | Show Custom Radial Gauge code 316 | 317 | ### Custom Radial Gauge 318 | 319 | ```jsx 320 | 346 | ``` 347 |
348 | 349 | # API 350 |

Props:

351 | 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 | 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 | } --------------------------------------------------------------------------------