├── projects ├── demo-app │ ├── src │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── favicon.ico │ │ ├── styles.css │ │ ├── main.ts │ │ ├── index.html │ │ └── app │ │ │ ├── mock-data-src.service.ts │ │ │ └── app.component.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── .eslintrc.json └── ng2-gauge │ ├── ng-package.json │ ├── src │ ├── public_api.ts │ ├── lib │ │ ├── gauge.module.ts │ │ ├── shared │ │ │ ├── interfaces.ts │ │ │ ├── config.ts │ │ │ └── validators.ts │ │ ├── gauge.component.css │ │ ├── gauge.component.html │ │ ├── gauge.component.spec.ts │ │ └── gauge.component.ts │ └── test.ts │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ ├── tsconfig.lib.json │ ├── package.json │ └── .eslintrc.json ├── assets ├── demo.gif └── gauge.png ├── .prettierrc ├── .prettierignore ├── .editorconfig ├── scripts └── copy-readme.ts ├── .gitignore ├── .github └── workflows │ └── ng2-gauge.yaml ├── LICENSE ├── tsconfig.json ├── CHANGELOG.md ├── package.json ├── README.md └── angular.json /projects/demo-app/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hawkgs/ng2-gauge/HEAD/assets/demo.gif -------------------------------------------------------------------------------- /assets/gauge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hawkgs/ng2-gauge/HEAD/assets/gauge.png -------------------------------------------------------------------------------- /projects/demo-app/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hawkgs/ng2-gauge/HEAD/projects/demo-app/src/favicon.ico -------------------------------------------------------------------------------- /projects/demo-app/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | trailingComma: all 2 | tabWidth: 2 3 | printWidth: 80 4 | semi: true 5 | singleQuote: true 6 | bracketSpacing: true 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | 4 | /*.json 5 | tsconfig.json 6 | tsconfig.*.json 7 | package.json 8 | ng-package.json 9 | karma.conf.json 10 | .eslintrc.json 11 | -------------------------------------------------------------------------------- /projects/ng2-gauge/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/ng2-gauge", 4 | "lib": { 5 | "entryFile": "src/public_api.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /projects/demo-app/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { AppComponent } from './app/app.component'; 3 | 4 | bootstrapApplication(AppComponent).catch((err) => console.error(err)); 5 | -------------------------------------------------------------------------------- /projects/ng2-gauge/src/public_api.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of ng2-gauge 3 | */ 4 | 5 | export { GaugeComponent } from './lib/gauge.component'; 6 | export { GaugeModule } from './lib/gauge.module'; 7 | export { Sector } from './lib/shared/interfaces'; 8 | export { GaugeConfig } from './lib/shared/config'; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /projects/ng2-gauge/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "compilationMode": "partial" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/ng2-gauge/src/lib/gauge.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { GaugeComponent } from './gauge.component'; 4 | 5 | @NgModule({ 6 | declarations: [GaugeComponent], 7 | imports: [CommonModule], 8 | exports: [GaugeComponent], 9 | }) 10 | export class GaugeModule {} 11 | -------------------------------------------------------------------------------- /projects/demo-app/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/ng2-gauge/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "**/*.spec.ts", 12 | "**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/demo-app/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/ng2-gauge/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../../out-tsc/lib", 6 | "declaration": true, 7 | "declarationMap": true, 8 | "inlineSources": true, 9 | "types": [] 10 | }, 11 | "exclude": [ 12 | "**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /projects/demo-app/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ng2-gauge demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /scripts/copy-readme.ts: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const copyFileToBuildRoot = (src: string) => { 4 | const fileName = src.split('/').pop(); 5 | fs.copyFile(src, `./dist/ng2-gauge/${fileName}`, (err: unknown) => { 6 | if (err) { 7 | throw new Error(`Unable to copy ${fileName}`); 8 | } 9 | }); 10 | }; 11 | 12 | console.log('Copying additional files to the dist folder ...'); 13 | 14 | copyFileToBuildRoot('./README.md'); 15 | copyFileToBuildRoot('./CHANGELOG.md'); 16 | 17 | console.log('Done!'); 18 | -------------------------------------------------------------------------------- /projects/demo-app/src/app/mock-data-src.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { BehaviorSubject } from 'rxjs'; 3 | 4 | /** 5 | * A mock OBD data source used for demonstrating how ng2-gauge is fed with data. 6 | */ 7 | @Injectable() 8 | export class MockEngineObdService { 9 | rpm$ = new BehaviorSubject(0); 10 | 11 | connect() { 12 | const target = 5600; 13 | 14 | const simulate = () => { 15 | for (let i = 0, t = 0; i < target; i += 15, t++) { 16 | setTimeout(() => this.rpm$.next(i), t * 2); 17 | } 18 | }; 19 | 20 | simulate(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /projects/ng2-gauge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng2-gauge", 3 | "version": "1.3.2", 4 | "peerDependencies": { 5 | "@angular/common": ">=12.0.0", 6 | "@angular/core": ">=12.0.0" 7 | }, 8 | "license": "MIT", 9 | "author": "hawkgs (Georgi Serev)", 10 | "keywords": [ 11 | "angular", 12 | "gauge", 13 | "ng gauge", 14 | "angular gauge", 15 | "analog gauge" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/hawkgs/ng2-gauge.git" 20 | }, 21 | "homepage": "https://github.com/hawkgs/ng2-gauge", 22 | "bugs": { 23 | "url": "https://github.com/hawkgs/ng2-gauge/issues" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /projects/ng2-gauge/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'core-js/es7/reflect'; 4 | import 'zone.js/dist/zone'; 5 | import 'zone.js/dist/zone-testing'; 6 | import { getTestBed } from '@angular/core/testing'; 7 | import { 8 | BrowserDynamicTestingModule, 9 | platformBrowserDynamicTesting, 10 | } from '@angular/platform-browser-dynamic/testing'; 11 | 12 | declare const require: any; 13 | 14 | // First, initialize the Angular testing environment. 15 | getTestBed().initTestEnvironment( 16 | BrowserDynamicTestingModule, 17 | platformBrowserDynamicTesting(), 18 | ); 19 | // Then we find all the tests. 20 | const context = require.context('./', true, /\.spec\.ts$/); 21 | // And load the modules. 22 | context.keys().map(context); 23 | -------------------------------------------------------------------------------- /projects/ng2-gauge/src/lib/shared/interfaces.ts: -------------------------------------------------------------------------------- 1 | import { GaugeConfig } from './config'; 2 | 3 | export interface CartesianCoor { 4 | x: number; 5 | y: number; 6 | } 7 | 8 | export interface Line { 9 | from: CartesianCoor; 10 | to: CartesianCoor; 11 | color: string; 12 | } 13 | 14 | export interface Value { 15 | coor: CartesianCoor; 16 | text: string; 17 | } 18 | 19 | export interface Sector { 20 | from: number; 21 | to: number; 22 | color: string; 23 | } 24 | 25 | export interface RenderSector { 26 | path: string; 27 | color: string; 28 | } 29 | 30 | export interface GaugeProps { 31 | arcStart: number; 32 | arcEnd: number; 33 | max: number; 34 | sectors: Sector[]; 35 | unit: string; 36 | digitalDisplay: boolean; 37 | activateRedLightAfter: number; 38 | darkTheme: boolean; 39 | config: GaugeConfig; 40 | } 41 | 42 | export enum Separator { 43 | NA, 44 | Big, 45 | Small, 46 | } 47 | -------------------------------------------------------------------------------- /projects/demo-app/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { GaugeModule } from 'ng2-gauge'; 4 | import { MockEngineObdService } from './mock-data-src.service'; 5 | 6 | @Component({ 7 | selector: 'app-root', 8 | standalone: true, 9 | imports: [CommonModule, GaugeModule], 10 | providers: [MockEngineObdService], 11 | template: ``, 26 | }) 27 | export class AppComponent implements OnInit { 28 | constructor(public obd: MockEngineObdService) {} 29 | 30 | ngOnInit() { 31 | this.obd.connect(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/ng2-gauge.yaml: -------------------------------------------------------------------------------- 1 | name: 'ng2-gauge' 2 | 3 | on: push 4 | 5 | jobs: 6 | lint-build-test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Use Node.js 20.x 11 | uses: actions/setup-node@v1 12 | with: 13 | node-version: 20.x 14 | - name: Cache Node modules 15 | uses: actions/cache@v2 16 | env: 17 | cache-name: cache-node-modules 18 | with: 19 | # npm cache files are stored in `~/.npm` on Linux/macOS 20 | path: ~/.npm 21 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 22 | restore-keys: | 23 | ${{ runner.os }}-build-${{ env.cache-name }}- 24 | ${{ runner.os }}-build- 25 | ${{ runner.os }}- 26 | - name: Install Node modules 27 | run: npm install 28 | - name: Lint ng2-gauge 29 | run: npm run lint 30 | - name: Build ng2-gauge 31 | run: npm run prod-build 32 | - name: Test ng2-gauge 33 | run: npm run test-ci 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Georgi Serev 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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "paths": { 14 | "ng2-gauge": [ 15 | "./dist/ng2-gauge" 16 | ] 17 | }, 18 | "esModuleInterop": true, 19 | "sourceMap": true, 20 | "declaration": false, 21 | "experimentalDecorators": true, 22 | "moduleResolution": "node", 23 | "importHelpers": true, 24 | "target": "ES2022", 25 | "module": "ES2022", 26 | "useDefineForClassFields": false, 27 | "lib": [ 28 | "ES2022", 29 | "dom" 30 | ] 31 | }, 32 | "angularCompilerOptions": { 33 | "enableI18nLegacyMessageIdFormat": false, 34 | "strictInjectionParameters": true, 35 | "strictInputAccessModifiers": true, 36 | "strictTemplates": true 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /projects/ng2-gauge/src/lib/gauge.component.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Orbitron'; 3 | font-style: normal; 4 | font-weight: 700; 5 | src: local('Orbitron Bold'), local('Orbitron-Bold'), 6 | url(https://fonts.gstatic.com/s/orbitron/v8/Y82YH_MJJWnsH2yUA5AuYY4P5ICox8Kq3LLUNMylGO4.woff2) 7 | format('woff2'); 8 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, 9 | U+2000-206F, U+2074, U+20AC, U+2212, U+2215; 10 | } 11 | 12 | .ng2-gauge { 13 | position: relative; 14 | width: 400px; /* Default size, use @Input size for manipulation */ 15 | } 16 | 17 | .ng2-gauge .info { 18 | position: absolute; 19 | top: 0; 20 | left: 0; 21 | } 22 | 23 | .ng2-gauge .arrow { 24 | transform-origin: 50% 50%; 25 | fill: orange; 26 | } 27 | 28 | .ng2-gauge text { 29 | font-family: 'Orbitron', sans-serif; 30 | font-weight: bold; 31 | text-anchor: middle; 32 | fill: #333; 33 | } 34 | 35 | .ng2-gauge.light text { 36 | fill: #fff; 37 | } 38 | 39 | .ng2-gauge .text-val { 40 | font-size: 12px; 41 | } 42 | 43 | .ng2-gauge .arrow-pin { 44 | fill: #333; 45 | } 46 | 47 | .ng2-gauge .main-arc { 48 | stroke: #333; 49 | } 50 | 51 | .ng2-gauge.light .main-arc { 52 | stroke: #fff; 53 | } 54 | 55 | .ng2-gauge .factor { 56 | font-size: 7px; 57 | } 58 | 59 | .ng2-gauge .digital { 60 | font-size: 16px; 61 | } 62 | 63 | .ng2-gauge .unit { 64 | font-size: 10px; 65 | } 66 | 67 | .ng2-gauge .red-light { 68 | fill: #ff4f4f; 69 | opacity: 0.1; 70 | } 71 | 72 | .ng2-gauge .red-light.on { 73 | opacity: 1; 74 | } 75 | -------------------------------------------------------------------------------- /projects/ng2-gauge/src/lib/shared/config.ts: -------------------------------------------------------------------------------- 1 | export interface GaugeConfig { 2 | WIDTH: number; // Width of the SVG (Use size input, if you want to change gauge size) 3 | ARC_STROKE: number; // Stroke/width of the arc 4 | ARROW_Y: number; // Distance from the arc to the tip of the arrow (Y position) 5 | ARROW_WIDTH: number; // Arrow width/stroke 6 | ARROW_PIN_RAD: number; // Radius of the arrow pin 7 | SL_NORM: number; // Length of a scale line 8 | SL_MID_SEP: number; // Length of a middle separator (a.k.a. small) 9 | SL_SEP: number; // Length of a separator (a.k.a. big) 10 | SL_WIDTH: number; // Scale line width/stroke 11 | TXT_MARGIN: number; // Y margin for a scale value 12 | LIGHT_Y: number; // Light Y position 13 | LIGHT_RADIUS: number; // Radius of the light 14 | S_FAC_Y: number; // Scale factor text Y position 15 | DIGITAL_Y: number; // Digital gauge Y position 16 | UNIT_Y: number; // Unit label Y position 17 | MAX_PURE_SCALE_VAL: number; // Max pure scale value (After that the scale shows only the multiplier) 18 | INIT_LINE_FREQ: number; // Initial scale line frequency 19 | DEF_START: number; // Default start angle (Use the input property in order to change) 20 | DEF_END: number; // Default end angle (Use the input property in order to change) 21 | } 22 | 23 | export const DefaultConfig: GaugeConfig = { 24 | WIDTH: 200, 25 | ARC_STROKE: 5, 26 | ARROW_Y: 22.5, 27 | ARROW_WIDTH: 4, 28 | ARROW_PIN_RAD: 8, 29 | SL_NORM: 3, 30 | SL_MID_SEP: 7, 31 | SL_SEP: 10, 32 | SL_WIDTH: 2, 33 | TXT_MARGIN: 10, 34 | LIGHT_Y: 55, 35 | LIGHT_RADIUS: 10, 36 | S_FAC_Y: 80, 37 | DIGITAL_Y: 145, 38 | UNIT_Y: 155, 39 | MAX_PURE_SCALE_VAL: 1000, 40 | INIT_LINE_FREQ: 2, 41 | DEF_START: 225, 42 | DEF_END: 135, 43 | }; 44 | -------------------------------------------------------------------------------- /projects/ng2-gauge/src/lib/shared/validators.ts: -------------------------------------------------------------------------------- 1 | import { GaugeProps, Sector } from './interfaces'; 2 | 3 | const error = (text: string, throwErr?: boolean) => { 4 | const msg = `GaugeComponent: ${text}`; 5 | 6 | if (throwErr) { 7 | throw new Error(msg); 8 | } 9 | console.error(msg); 10 | }; 11 | 12 | export const validate = (props: GaugeProps) => { 13 | if (!props.max) { 14 | error('Missing "max" input property (or zero)', true); 15 | } 16 | 17 | if (props.max < 0) { 18 | error('"max" input property cannot be negative.', true); 19 | } 20 | 21 | if ( 22 | !(0 <= props.arcStart && props.arcStart <= 359) || 23 | !(0 <= props.arcEnd && props.arcEnd <= 359) 24 | ) { 25 | error( 26 | 'The scale arc end and start must be between 0 and 359 degrees.', 27 | true, 28 | ); 29 | } 30 | 31 | if (props.activateRedLightAfter && props.activateRedLightAfter > props.max) { 32 | error( 33 | 'The red light trigger value cannot be greater than the max value of the gauge.', 34 | ); 35 | } 36 | 37 | // if (props.scaleFactor && props.scaleFactor >= props.max) { 38 | // showError('The factor cannot be greater than or equal to the max value.'); 39 | // } 40 | 41 | if (props.sectors) { 42 | props.sectors.forEach((s: Sector) => { 43 | if (s.from < 0 || s.to < 0) { 44 | error('The sector bounds cannot be negative.', true); 45 | } 46 | 47 | if (s.from > props.max || s.to > props.max) { 48 | error('The sector bounds cannot be greater than the max value.', true); 49 | } 50 | 51 | if (s.from >= s.to) { 52 | error( 53 | 'The lower bound of the sector cannot be greater than or equal to the upper one.', 54 | true, 55 | ); 56 | } 57 | }); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.3.2 (Feb 2023) 2 | 3 | ### Fixes and/or improvements 4 | 5 | - Added support for a static `value` 6 | 7 | ## v1.3.1 (Feb 2023) 8 | 9 | ### Fixes and/or improvements 10 | 11 | - Fixed peer dependencies issues 12 | 13 | ## v1.3.0 (Feb 2023) 14 | 15 | ### Fixes and/or improvements 16 | 17 | - Gauge input is now limited by the max value 18 | - Improved validation 19 | - Changed component input names to better and more descriptive ones (see **Breaking changes**) 20 | - Fixed issue #5 21 | - Other smaller improvements 22 | - Project upgraded to Angular 17 23 | - Integrated with GitHub Actions 24 | 25 | ### Breaking changes 26 | 27 | - Component name is now reverted back to `ng2-gauge` 28 | - The main module name is now reverted back to `GaugeModule` 29 | - Some component input names were updated as follow: 30 | - `input` to `value` 31 | - `start` to `arcStart` 32 | - `end` to `arcEnd` 33 | - `showDigital` to `digitalDisplay` 34 | - `lightTheme` to `darkTheme` (a mistake in the initial release) 35 | - `light` to `activateRedLightAfter` 36 | - `factor` input is no longer supported 37 | 38 | ## v1.2.0 (Dec 2018) 39 | 40 | ### Fixes and/or improvements 41 | 42 | - Upgrade to Angular 7 43 | - Use `angular-cli` projects feature for maintaining the library. Optimized & smaller build 44 | 45 | ### Breaking changes 46 | 47 | - Module name changed from `GaugeModule` to `Ng2GaugeModule` 48 | - Component name changed from `ng2-gauge` to `nga-ng2-gauge` due to project prefixing in `angular-cli` 49 | 50 | ## v1.1.7 (Mar 2018) 51 | 52 | ### Fixes and/or improvements 53 | 54 | - Fix support of the `config` input - Credits: [@mehrjouei](https://github.com/mehrjouei) 55 | - Fix arrow position - Credits: [@mehrjouei](https://github.com/mehrjouei) 56 | - Introduce dynamic `max` - Credits: [@leticiafatimaa](https://github.com/leticiafatimaa) 57 | - Introduce `size` property for changing the width/size 58 | 59 | ### Breaking changes 60 | 61 | No breaking changes 62 | -------------------------------------------------------------------------------- /projects/demo-app/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "overrides": [ 4 | { 5 | "files": [ 6 | "*.ts" 7 | ], 8 | "parserOptions": { 9 | "project": [ 10 | "tsconfig.json" 11 | ], 12 | "createDefaultProgram": true 13 | }, 14 | "extends": [ 15 | "plugin:@angular-eslint/recommended", 16 | "plugin:@typescript-eslint/recommended", 17 | "plugin:@angular-eslint/template/process-inline-templates" 18 | ], 19 | "rules": { 20 | "@angular-eslint/component-class-suffix": [ 21 | "error", 22 | { 23 | "suffixes": [ 24 | "Component" 25 | ] 26 | } 27 | ], 28 | "@angular-eslint/component-selector": [ 29 | "error", 30 | { 31 | "type": "element", 32 | "prefix": "app", 33 | "style": "kebab-case" 34 | } 35 | ], 36 | "@angular-eslint/directive-selector": [ 37 | "error", 38 | { 39 | "type": "attribute", 40 | "prefix": "app", 41 | "style": "camelCase" 42 | } 43 | ], 44 | "@typescript-eslint/naming-convention": [ 45 | "error", 46 | { 47 | "selector": "memberLike", 48 | "modifiers": ["private"], 49 | "format": ["camelCase"], 50 | "leadingUnderscore": "require" 51 | } 52 | ], 53 | "@angular-eslint/no-empty-lifecycle-method": "off", 54 | "@typescript-eslint/no-empty-function": "off", 55 | "@typescript-eslint/no-explicit-any": "off", 56 | "@typescript-eslint/no-unused-vars": "off", 57 | "@typescript-eslint/no-inferrable-types": "off", 58 | "no-underscore-dangle": "off", 59 | "comma-dangle": ["warn", "always-multiline"] 60 | } 61 | }, 62 | { 63 | "files": [ 64 | "*.html" 65 | ], 66 | "extends": [ 67 | "plugin:@angular-eslint/template/recommended" 68 | ], 69 | "rules": {} 70 | } 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /projects/ng2-gauge/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "overrides": [ 4 | { 5 | "files": [ 6 | "*.ts" 7 | ], 8 | "parserOptions": { 9 | "project": [ 10 | "tsconfig.json" 11 | ], 12 | "createDefaultProgram": true 13 | }, 14 | "extends": [ 15 | "plugin:@angular-eslint/recommended", 16 | "plugin:@typescript-eslint/recommended", 17 | "plugin:@angular-eslint/template/process-inline-templates" 18 | ], 19 | "rules": { 20 | "@angular-eslint/component-class-suffix": [ 21 | "error", 22 | { 23 | "suffixes": [ 24 | "Component" 25 | ] 26 | } 27 | ], 28 | "@angular-eslint/component-selector": [ 29 | "error", 30 | { 31 | "type": "element", 32 | "prefix": "ng2", 33 | "style": "kebab-case" 34 | } 35 | ], 36 | "@angular-eslint/directive-selector": [ 37 | "error", 38 | { 39 | "type": "attribute", 40 | "prefix": "ng2", 41 | "style": "camelCase" 42 | } 43 | ], 44 | "@typescript-eslint/naming-convention": [ 45 | "error", 46 | { 47 | "selector": "memberLike", 48 | "modifiers": ["private"], 49 | "format": ["camelCase"], 50 | "leadingUnderscore": "require" 51 | } 52 | ], 53 | "@angular-eslint/no-empty-lifecycle-method": "off", 54 | "@typescript-eslint/no-empty-function": "off", 55 | "@typescript-eslint/no-explicit-any": "off", 56 | "@typescript-eslint/no-unused-vars": "off", 57 | "@typescript-eslint/no-inferrable-types": "off", 58 | "no-underscore-dangle": "off", 59 | "comma-dangle": ["warn", "always-multiline"] 60 | } 61 | }, 62 | { 63 | "files": [ 64 | "*.html" 65 | ], 66 | "extends": [ 67 | "plugin:@angular-eslint/template/recommended" 68 | ], 69 | "rules": {} 70 | } 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng2-gauge", 3 | "version": "1.3.2", 4 | "license": "MIT", 5 | "author": "hawkgs (Georgi Serev)", 6 | "keywords": [ 7 | "angular", 8 | "gauge", 9 | "ng gauge", 10 | "angular gauge", 11 | "analog gauge" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/hawkgs/ng2-gauge.git" 16 | }, 17 | "homepage": "https://github.com/hawkgs/ng2-gauge", 18 | "bugs": { 19 | "url": "https://github.com/hawkgs/ng2-gauge/issues" 20 | }, 21 | "scripts": { 22 | "ng": "ng", 23 | "start": "ng serve demo-app", 24 | "watch:ng2-gauge": "ng build ng2-gauge --watch", 25 | "build": "ng build ng2-gauge && ts-node ./scripts/copy-readme.ts", 26 | "prod-build": "ng build ng2-gauge --configuration=production && ts-node ./scripts/copy-readme.ts", 27 | "test": "ng test ng2-gauge", 28 | "test-ci": "ng test ng2-gauge --no-watch --browsers=ChromeHeadless", 29 | "lint": "ng lint ng2-gauge" 30 | }, 31 | "private": true, 32 | "dependencies": { 33 | "@angular/common": "^17.0.0", 34 | "@angular/compiler": "^17.0.0", 35 | "@angular/core": "^17.0.0", 36 | "@angular/platform-browser": "^17.0.0", 37 | "@angular/platform-browser-dynamic": "^17.0.0", 38 | "rxjs": "~7.8.0", 39 | "tslib": "^2.3.0", 40 | "zone.js": "~0.14.2" 41 | }, 42 | "devDependencies": { 43 | "@angular-devkit/build-angular": "^17.1.2", 44 | "@angular-eslint/builder": "17.2.1", 45 | "@angular-eslint/eslint-plugin": "17.2.1", 46 | "@angular-eslint/eslint-plugin-template": "17.2.1", 47 | "@angular-eslint/schematics": "17.2.1", 48 | "@angular-eslint/template-parser": "17.2.1", 49 | "@angular/cli": "^17.0.7", 50 | "@angular/compiler-cli": "^17.0.0", 51 | "@types/jasmine": "~5.1.0", 52 | "@typescript-eslint/eslint-plugin": "6.19.0", 53 | "@typescript-eslint/parser": "6.19.0", 54 | "eslint": "^8.56.0", 55 | "jasmine-core": "~5.1.0", 56 | "karma": "~6.4.0", 57 | "karma-chrome-launcher": "~3.2.0", 58 | "karma-coverage": "~2.2.0", 59 | "karma-jasmine": "~5.1.0", 60 | "karma-jasmine-html-reporter": "~2.1.0", 61 | "ng-packagr": "^17.1.2", 62 | "ts-node": "^10.9.2", 63 | "typescript": "~5.2.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /projects/ng2-gauge/src/lib/gauge.component.html: -------------------------------------------------------------------------------- 1 |
6 | 7 | 15 | 21 | x{{ scaleFactor }} {{ unit }} 22 | 23 | 29 | {{ value }} 30 | 31 | 32 | {{ unit }} 33 | 34 | 35 | 36 | 42 | 49 | 58 | 74 | {{ val.text }} 75 | 76 | 86 | 92 | 93 |
94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ng2-gauge 2 | 3 | SVG gauge component for Angular 4 | 5 | 6 | 7 | 10 | 14 | 15 |
8 | ng2-gauge 9 | 11 | 12 |

Suitable for building virtual dashboards (initially designed for that).

13 |
16 | 17 | **v1.3.2** | [CHANGELOG](./CHANGELOG.md) 18 | 19 | ## Installation 20 | 21 | ``` 22 | npm install ng2-gauge --save 23 | ``` 24 | 25 | ## How to? 26 | 27 | You should import the `GaugeModule` to your desired module: 28 | 29 | ```typescript 30 | import { NgModule } from '@angular/core'; 31 | import { GaugeModule } from 'ng2-gauge'; 32 | 33 | @NgModule({ 34 | imports: [CommonModule, GaugeModule], 35 | }) 36 | export class SharedModule {} 37 | ``` 38 | 39 | Then you can simply use the component in your template: 40 | 41 | ```typescript 42 | @Component({ 43 | selector: 'app-my-component', 44 | template: ` 45 | `, 49 | }) 50 | export class MyComponent { 51 | value$: Observable; 52 | } 53 | ``` 54 | 55 | ## Options 56 | 57 | The component provides a list of the following options: 58 | 59 | - **`max: number`** _(required)_ – The maximal value of the gauge. It is suggested to use a number that is divisible by 10^n (e.g. 100, 1000, etc.) 60 | - **`value: number`** – The current value of the gauge 61 | - **`unit: string`** – The unit of the gauge (i.e. mph, psi, etc.) 62 | - **`size: number`** – Size/width of the gauge _in pixels_ 63 | - **`arcStart: number`** – The start/beginning of the scale arc _in degrees_. Default `225` 64 | - **`arcEnd: number`** – The end of the scale arc _in degrees_. Default: `135` 65 | - **`digitalDisplay: boolean`** – Displays the current value as digital number inside the gauge 66 | - **`darkTheme: boolean`** – Enables the dark theme 67 | - **`activateRedLightAfter: number`** - Shows a red light when the specified limit is reached 68 | - **`sectors: Sectors[]`** – Defines the coloring of specified sectors 69 | - **`config: GaugeConfig`** _(Not recommended)_ – Alters the default configuration; This may lead to unexpected behavior; [GaugeConfig](./src/app/gauge/shared/config.ts) 70 | 71 | ### Sectors 72 | 73 | Sectors are used for marking parts of the arc with a different color. 74 | 75 | **Example:** 76 | 77 | ```typescript 78 | const max = 9000; 79 | const sectors = [ 80 | { 81 | from: 6500, 82 | to: 8000, 83 | color: 'orange', 84 | }, 85 | { 86 | from: 8000, 87 | to: 9000, 88 | color: 'red', 89 | }, 90 | ]; 91 | ``` 92 | 93 | ## Styling 94 | 95 | The component provides two themes - light (default) and dark. Yet, you can easily alter the CSS through the parent component in order to fit your needs. The font used for the gauge is Orbitron (Google Fonts). 96 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ng2-gauge": { 7 | "root": "projects/ng2-gauge", 8 | "sourceRoot": "projects/ng2-gauge/src", 9 | "projectType": "library", 10 | "prefix": "ng2", 11 | "architect": { 12 | "build": { 13 | "builder": "@angular-devkit/build-angular:ng-packagr", 14 | "options": { 15 | "project": "projects/ng2-gauge/ng-package.json" 16 | }, 17 | "configurations": { 18 | "production": { 19 | "tsConfig": "projects/ng2-gauge/tsconfig.lib.prod.json" 20 | }, 21 | "development": { 22 | "tsConfig": "projects/ng2-gauge/tsconfig.lib.json" 23 | } 24 | } 25 | }, 26 | "test": { 27 | "builder": "@angular-devkit/build-angular:karma", 28 | "options": { 29 | "tsConfig": "projects/ng2-gauge/tsconfig.spec.json", 30 | "polyfills": [ 31 | "zone.js", 32 | "zone.js/testing" 33 | ] 34 | } 35 | }, 36 | "lint": { 37 | "builder": "@angular-eslint/builder:lint", 38 | "options": { 39 | "lintFilePatterns": [ 40 | "projects/ng2-gauge/src/lib/**/*.ts", 41 | "projects/ng2-gauge/src/lib/**/*.html" 42 | ] 43 | } 44 | } 45 | } 46 | }, 47 | "demo-app": { 48 | "projectType": "application", 49 | "schematics": {}, 50 | "root": "projects/demo-app", 51 | "sourceRoot": "projects/demo-app/src", 52 | "prefix": "app", 53 | "architect": { 54 | "build": { 55 | "builder": "@angular-devkit/build-angular:application", 56 | "options": { 57 | "outputPath": "dist/demo-app", 58 | "index": "projects/demo-app/src/index.html", 59 | "browser": "projects/demo-app/src/main.ts", 60 | "polyfills": [ 61 | "zone.js" 62 | ], 63 | "tsConfig": "projects/demo-app/tsconfig.app.json", 64 | "assets": [ 65 | "projects/demo-app/src/favicon.ico", 66 | "projects/demo-app/src/assets" 67 | ], 68 | "styles": [ 69 | "projects/demo-app/src/styles.css" 70 | ], 71 | "scripts": [] 72 | }, 73 | "configurations": { 74 | "production": { 75 | "budgets": [ 76 | { 77 | "type": "initial", 78 | "maximumWarning": "500kb", 79 | "maximumError": "1mb" 80 | }, 81 | { 82 | "type": "anyComponentStyle", 83 | "maximumWarning": "2kb", 84 | "maximumError": "4kb" 85 | } 86 | ], 87 | "outputHashing": "all" 88 | }, 89 | "development": { 90 | "optimization": false, 91 | "extractLicenses": false, 92 | "sourceMap": true 93 | } 94 | }, 95 | "defaultConfiguration": "production" 96 | }, 97 | "serve": { 98 | "builder": "@angular-devkit/build-angular:dev-server", 99 | "configurations": { 100 | "production": { 101 | "buildTarget": "demo-app:build:production" 102 | }, 103 | "development": { 104 | "buildTarget": "demo-app:build:development" 105 | } 106 | }, 107 | "defaultConfiguration": "development" 108 | }, 109 | "extract-i18n": { 110 | "builder": "@angular-devkit/build-angular:extract-i18n", 111 | "options": { 112 | "buildTarget": "demo-app:build" 113 | } 114 | }, 115 | "test": { 116 | "builder": "@angular-devkit/build-angular:karma", 117 | "options": { 118 | "polyfills": [ 119 | "zone.js", 120 | "zone.js/testing" 121 | ], 122 | "tsConfig": "projects/demo-app/tsconfig.spec.json", 123 | "assets": [ 124 | "projects/demo-app/src/favicon.ico", 125 | "projects/demo-app/src/assets" 126 | ], 127 | "styles": [ 128 | "projects/demo-app/src/styles.css" 129 | ], 130 | "scripts": [] 131 | } 132 | } 133 | } 134 | } 135 | }, 136 | "cli": { 137 | "analytics": "862d5b41-2e89-444c-8fd6-cd0883605a22", 138 | "schematicCollections": ["@angular-eslint/schematics"] 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /projects/ng2-gauge/src/lib/gauge.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { GaugeComponent } from './gauge.component'; 4 | import { CommonModule } from '@angular/common'; 5 | 6 | describe('GaugeComponent', () => { 7 | function createComponent() { 8 | const fixture = TestBed.createComponent(GaugeComponent); 9 | const component = fixture.componentInstance; 10 | 11 | return { fixture, component }; 12 | } 13 | 14 | beforeEach(async () => { 15 | await TestBed.configureTestingModule({ 16 | imports: [CommonModule], 17 | declarations: [GaugeComponent], 18 | }).compileComponents(); 19 | }); 20 | 21 | it('should create', async () => { 22 | const { component, fixture } = createComponent(); 23 | component.max = 5000; 24 | fixture.detectChanges(); 25 | 26 | expect(component).toBeTruthy(); 27 | }); 28 | 29 | describe('validators', () => { 30 | it('should throw an error, if "max" is not provided', async () => { 31 | expect(() => { 32 | const { fixture } = createComponent(); 33 | fixture.detectChanges(); 34 | }).toThrowError('GaugeComponent: Missing "max" input property (or zero)'); 35 | }); 36 | 37 | it('should throw an error, if "max" is negative', async () => { 38 | expect(() => { 39 | const { component, fixture } = createComponent(); 40 | component.max = -1; 41 | fixture.detectChanges(); 42 | }).toThrowError( 43 | 'GaugeComponent: "max" input property cannot be negative.', 44 | ); 45 | }); 46 | 47 | it('should throw an error, if scale arc start is negative', () => { 48 | expect(() => { 49 | const { component, fixture } = createComponent(); 50 | component.max = 5000; 51 | component.arcStart = -1; 52 | fixture.detectChanges(); 53 | }).toThrowError( 54 | 'GaugeComponent: The scale arc end and start must be between 0 and 359 degrees.', 55 | ); 56 | }); 57 | 58 | it('should throw an error, if scale arc start above 359', () => { 59 | expect(() => { 60 | const { component, fixture } = createComponent(); 61 | component.max = 5000; 62 | component.arcStart = 360; 63 | fixture.detectChanges(); 64 | }).toThrowError( 65 | 'GaugeComponent: The scale arc end and start must be between 0 and 359 degrees.', 66 | ); 67 | }); 68 | 69 | it('should throw an error, if scale arc end is negative', () => { 70 | expect(() => { 71 | const { component, fixture } = createComponent(); 72 | component.max = 5000; 73 | component.arcEnd = -1; 74 | fixture.detectChanges(); 75 | }).toThrowError( 76 | 'GaugeComponent: The scale arc end and start must be between 0 and 359 degrees.', 77 | ); 78 | }); 79 | 80 | it('should throw an error, if scale arc end is above 359', () => { 81 | expect(() => { 82 | const { component, fixture } = createComponent(); 83 | component.max = 5000; 84 | component.arcEnd = 360; 85 | fixture.detectChanges(); 86 | }).toThrowError( 87 | 'GaugeComponent: The scale arc end and start must be between 0 and 359 degrees.', 88 | ); 89 | }); 90 | 91 | it('should throw an error, if the lower bound is greater than or equal to the upper', () => { 92 | expect(() => { 93 | const { component, fixture } = createComponent(); 94 | component.max = 5000; 95 | component.sectors = [ 96 | { 97 | from: 200, 98 | to: 100, 99 | color: 'red', 100 | }, 101 | ]; 102 | fixture.detectChanges(); 103 | }).toThrowError( 104 | 'GaugeComponent: The lower bound of the sector cannot be greater than or equal to the upper one.', 105 | ); 106 | 107 | expect(() => { 108 | const { component, fixture } = createComponent(); 109 | component.max = 5000; 110 | component.sectors = [ 111 | { 112 | from: 100, 113 | to: 100, 114 | color: 'red', 115 | }, 116 | ]; 117 | fixture.detectChanges(); 118 | }).toThrowError( 119 | 'GaugeComponent: The lower bound of the sector cannot be greater than or equal to the upper one.', 120 | ); 121 | }); 122 | 123 | it('should throw an error, if the bounds are negative', () => { 124 | expect(() => { 125 | const { component, fixture } = createComponent(); 126 | component.max = 5000; 127 | component.sectors = [ 128 | { 129 | from: -1, 130 | to: 100, 131 | color: 'red', 132 | }, 133 | ]; 134 | fixture.detectChanges(); 135 | }).toThrowError('GaugeComponent: The sector bounds cannot be negative.'); 136 | 137 | expect(() => { 138 | const { component, fixture } = createComponent(); 139 | component.max = 5000; 140 | component.sectors = [ 141 | { 142 | from: 0, 143 | to: -1, 144 | color: 'red', 145 | }, 146 | ]; 147 | fixture.detectChanges(); 148 | }).toThrowError('GaugeComponent: The sector bounds cannot be negative.'); 149 | }); 150 | 151 | it('should throw an error, if the bounds are greater than the max', () => { 152 | expect(() => { 153 | const { component, fixture } = createComponent(); 154 | component.max = 5000; 155 | component.sectors = [ 156 | { 157 | from: 5001, 158 | to: 5100, 159 | color: 'red', 160 | }, 161 | ]; 162 | fixture.detectChanges(); 163 | }).toThrowError( 164 | 'GaugeComponent: The sector bounds cannot be greater than the max value.', 165 | ); 166 | }); 167 | }); 168 | }); 169 | -------------------------------------------------------------------------------- /projects/ng2-gauge/src/lib/gauge.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | ViewChild, 5 | OnInit, 6 | AfterViewInit, 7 | Renderer2, 8 | ElementRef, 9 | ViewEncapsulation, 10 | } from '@angular/core'; 11 | 12 | import { 13 | Sector, 14 | Line, 15 | CartesianCoor, 16 | RenderSector, 17 | Value, 18 | Separator, 19 | GaugeProps, 20 | } from './shared/interfaces'; 21 | import { DefaultConfig, GaugeConfig } from './shared/config'; 22 | import { validate } from './shared/validators'; 23 | 24 | function copySectors(sectors: Sector[]): Sector[] { 25 | return sectors.map((s) => ({ ...s })); 26 | } 27 | 28 | @Component({ 29 | selector: 'ng2-gauge', 30 | templateUrl: './gauge.component.html', 31 | styleUrl: './gauge.component.css', 32 | encapsulation: ViewEncapsulation.None, 33 | }) 34 | export class GaugeComponent implements OnInit, AfterViewInit, GaugeProps { 35 | @ViewChild('gauge') gauge!: ElementRef; 36 | @ViewChild('arrow', { static: true }) arrow!: ElementRef; 37 | 38 | /** 39 | * Size/width of the gauge _in pixels_. 40 | */ 41 | @Input() size!: number; 42 | 43 | /** 44 | * The start/beginning of the scale arc _in degrees_. Default `225` 45 | */ 46 | @Input() arcStart!: number; 47 | 48 | /** 49 | * The end of the scale arc _in degrees_. Default: `135` 50 | */ 51 | @Input() arcEnd!: number; 52 | 53 | /** 54 | * Defines the coloring of specified sectors 55 | */ 56 | @Input() sectors!: Sector[]; 57 | 58 | /** 59 | * The unit of the gauge (i.e. mph, psi, etc.) 60 | */ 61 | @Input() unit!: string; 62 | 63 | /** 64 | * Displays the current value as digital number inside the gauge 65 | */ 66 | @Input() digitalDisplay!: boolean; 67 | 68 | /** 69 | * Shows a red light when the specified limit is reached 70 | */ 71 | @Input() activateRedLightAfter!: number; 72 | 73 | /** 74 | * Enables the dark theme 75 | */ 76 | @Input() darkTheme!: boolean; 77 | 78 | /** 79 | * _(Not recommended)_ Alters the default configuration; This may lead to unexpected behavior; [GaugeConfig](./src/app/gauge/shared/config.ts) 80 | */ 81 | @Input() config!: GaugeConfig; 82 | 83 | viewBox: string = ''; 84 | scaleLines: Line[] = []; 85 | scaleValues: Value[] = []; 86 | sectorArcs: RenderSector[] = []; 87 | 88 | radius: number = 0; 89 | center: number = 0; 90 | scaleFactor: number = 0; 91 | 92 | private _arcEnd: number = 0; 93 | private _value: number = 0; 94 | private _max: number = 0; 95 | private _mappedSectors: Sector[] = []; 96 | 97 | constructor(private _renderer: Renderer2) {} 98 | 99 | /** 100 | * The current value of the gauge 101 | */ 102 | @Input() 103 | set value(val: number) { 104 | this._value = Math.min(val, this._max); 105 | this._updateArrowPos(this._value); 106 | } 107 | 108 | get value(): number { 109 | return this._value; 110 | } 111 | 112 | /** 113 | * The maximal value of the gauge. It is suggested to use a number that is divisible by 10^n (e.g. 100, 1000, etc.) 114 | */ 115 | // Note(Georgi): Don't use { require: true } since it's v16+ only 116 | @Input() 117 | set max(val: number) { 118 | if (this._max) { 119 | this._max = val; 120 | validate(this); 121 | this._initialize(); 122 | } 123 | this._max = val; 124 | } 125 | 126 | get max(): number { 127 | return this._max; 128 | } 129 | 130 | get arc(): string { 131 | return this._arc(0, this._arcEnd); 132 | } 133 | 134 | get gaugeRotationAngle(): number { 135 | return this._arcEnd - this.arcEnd; 136 | } 137 | 138 | ngOnInit(): void { 139 | this.config = { ...DefaultConfig, ...this.config }; 140 | 141 | if (!this.arcStart) { 142 | this.arcStart = this.config.DEF_START; 143 | } 144 | if (!this.arcEnd) { 145 | this.arcEnd = this.config.DEF_END; 146 | } 147 | 148 | validate(this); 149 | 150 | const width = this.config.WIDTH + this.config.ARC_STROKE; 151 | 152 | this.viewBox = `0 0 ${width} ${width}`; 153 | this.radius = this.config.WIDTH / 2; 154 | this.center = width / 2; 155 | this._arcEnd = this.arcEnd; 156 | 157 | if (this.arcStart > this.arcEnd) { 158 | this._arcEnd += 360 - this.arcStart; 159 | } else { 160 | this._arcEnd -= this.arcStart; 161 | } 162 | 163 | this._initialize(); 164 | } 165 | 166 | ngAfterViewInit(): void { 167 | this._rotateGauge(); 168 | } 169 | 170 | /** 171 | * Initialize gauge. 172 | */ 173 | private _initialize() { 174 | this.scaleLines = []; 175 | this.scaleValues = []; 176 | 177 | this._calculateSectors(); 178 | this._updateArrowPos(this._value); 179 | this.scaleFactor = this._determineScaleFactor(); 180 | this._createScale(); 181 | } 182 | 183 | /** 184 | * Calculate arc. 185 | */ 186 | private _arc(start: number, end: number): string { 187 | const largeArc = end - start <= 180 ? 0 : 1; 188 | const startCoor = this._getAngleCoor(start); 189 | const endCoor = this._getAngleCoor(end); 190 | 191 | return `M ${endCoor.x} ${endCoor.y} A ${this.radius} ${this.radius} 0 ${largeArc} 0 ${startCoor.x} ${startCoor.y}`; 192 | } 193 | 194 | /** 195 | * Get angle coordinates (Cartesian coordinates). 196 | */ 197 | private _getAngleCoor(degrees: number): CartesianCoor { 198 | const rads = ((degrees - 90) * Math.PI) / 180; 199 | return { 200 | x: this.radius * Math.cos(rads) + this.center, 201 | y: this.radius * Math.sin(rads) + this.center, 202 | }; 203 | } 204 | 205 | /** 206 | * Calculate/translate the user-defined sectors to arcs. 207 | */ 208 | private _calculateSectors(): void { 209 | if (!this.sectors) { 210 | return; 211 | } 212 | 213 | this._mappedSectors = copySectors(this.sectors); 214 | this._mappedSectors.forEach((s: Sector) => { 215 | const ratio = this._arcEnd / this.max; 216 | s.from *= ratio; 217 | s.to *= ratio; 218 | }); 219 | 220 | this.sectorArcs = this._mappedSectors.map((s: Sector) => ({ 221 | path: this._arc(s.from, s.to), 222 | color: s.color, 223 | })); 224 | } 225 | 226 | /** 227 | * Update the position of the arrow based on the current value. 228 | */ 229 | private _updateArrowPos(value: number): void { 230 | const pos = (this._arcEnd / this.max) * value; 231 | this._renderer.setStyle( 232 | this.arrow.nativeElement, 233 | 'transform', 234 | `rotate(${pos}deg)`, 235 | ); 236 | } 237 | 238 | /** 239 | * Rotate the gauge based on the start property. The CSS rotation, saves additional calculations with SVG. 240 | */ 241 | private _rotateGauge(): void { 242 | const angle = 360 - this.arcStart; 243 | this._renderer.setStyle( 244 | this.gauge.nativeElement, 245 | 'transform', 246 | `rotate(-${angle}deg)`, 247 | ); 248 | } 249 | 250 | /** 251 | * Determine the scale factor (10^n number; i.e. if max = 9000 then scale_factor = 1000) 252 | */ 253 | private _determineScaleFactor(factor = 10): number { 254 | // Keep smaller factor until 3X 255 | if (this.max / factor > 30) { 256 | return this._determineScaleFactor(factor * 10); 257 | } 258 | return factor; 259 | } 260 | 261 | /** 262 | * Determine the line frequency which represents after what angle we should put a line. 263 | */ 264 | private _determineLineFrequency(): number { 265 | const separators = this.max / this.scaleFactor; 266 | const separateAtAngle = this._arcEnd / separators; 267 | let lineFrequency: number; 268 | 269 | // If separateAtAngle is not an integer, use its value as the line frequency. 270 | if (separateAtAngle % 1 !== 0) { 271 | lineFrequency = separateAtAngle; 272 | } else { 273 | lineFrequency = this.config.INIT_LINE_FREQ * 2; 274 | for (lineFrequency; lineFrequency <= separateAtAngle; lineFrequency++) { 275 | if (separateAtAngle % lineFrequency === 0) { 276 | break; 277 | } 278 | } 279 | } 280 | 281 | return lineFrequency; 282 | } 283 | 284 | /** 285 | * Checks whether the line (based on index) is big or small separator. 286 | */ 287 | private _isSeparatorReached(idx: number, lineFrequency: number): Separator { 288 | const separators = this.max / this.scaleFactor; 289 | const totalSeparators = this._arcEnd / lineFrequency; 290 | const separateAtIdx = totalSeparators / separators; 291 | 292 | if (idx % separateAtIdx === 0) { 293 | return Separator.Big; 294 | } else if (idx % (separateAtIdx / 2) === 0) { 295 | return Separator.Small; 296 | } 297 | return Separator.NA; 298 | } 299 | 300 | /** 301 | * Creates the scale. 302 | */ 303 | private _createScale(): void { 304 | const accumWith = this._determineLineFrequency() / 2; 305 | const isAboveSuitableFactor = this.max / this.scaleFactor > 10; 306 | let placedVals = 0; 307 | 308 | for ( 309 | let alpha = 0, i = 0; 310 | alpha >= -1 * this._arcEnd; 311 | alpha -= accumWith, i++ 312 | ) { 313 | let lineHeight = this.config.SL_NORM; 314 | const sepReached = this._isSeparatorReached(i, accumWith); 315 | 316 | // Set the line height based on its type 317 | switch (sepReached) { 318 | case Separator.Big: 319 | placedVals++; 320 | lineHeight = this.config.SL_SEP; 321 | break; 322 | case Separator.Small: 323 | lineHeight = this.config.SL_MID_SEP; 324 | break; 325 | } 326 | 327 | // Draw the line 328 | const higherEnd = this.center - this.config.ARC_STROKE - 2; 329 | const lowerEnd = higherEnd - lineHeight; 330 | 331 | const alphaRad = (Math.PI / 180) * (alpha + 180); 332 | const sin = Math.sin(alphaRad); 333 | const cos = Math.cos(alphaRad); 334 | const color = this._getScaleLineColor(alpha); 335 | 336 | this._addScaleLine(sin, cos, higherEnd, lowerEnd, color); 337 | 338 | // Put a scale value 339 | if (sepReached === Separator.Big) { 340 | const isValuePosEven = placedVals % 2 === 0; 341 | const isLast = alpha <= -1 * this._arcEnd; 342 | 343 | if (!(isAboveSuitableFactor && isValuePosEven && !isLast)) { 344 | this._addScaleValue(sin, cos, lowerEnd, alpha); 345 | } 346 | } 347 | } 348 | } 349 | 350 | /** 351 | * Get the scale line color from the user-provided sectors definitions. 352 | */ 353 | private _getScaleLineColor(alpha: number): string { 354 | alpha *= -1; 355 | let color = ''; 356 | 357 | if (this._mappedSectors.length) { 358 | this._mappedSectors.forEach((s: Sector) => { 359 | if (s.from <= alpha && alpha <= s.to) { 360 | color = s.color; 361 | } 362 | }); 363 | } 364 | 365 | return color; 366 | } 367 | 368 | /** 369 | * Add a scale line to the list that will be later rendered. 370 | */ 371 | private _addScaleLine( 372 | sin: number, 373 | cos: number, 374 | higherEnd: number, 375 | lowerEnd: number, 376 | color: string, 377 | ): void { 378 | this.scaleLines.push({ 379 | from: { 380 | x: sin * higherEnd + this.center, 381 | y: cos * higherEnd + this.center, 382 | }, 383 | to: { 384 | x: sin * lowerEnd + this.center, 385 | y: cos * lowerEnd + this.center, 386 | }, 387 | color, 388 | }); 389 | } 390 | 391 | /** 392 | * Add a scale value. 393 | */ 394 | private _addScaleValue( 395 | sin: number, 396 | cos: number, 397 | lowerEnd: number, 398 | alpha: number, 399 | ): void { 400 | let val = Math.round(alpha * (this.max / this._arcEnd)) * -1; 401 | let posMargin = this.config.TXT_MARGIN * 2; 402 | 403 | // Use the multiplier instead of the real value, if above MAX_PURE_SCALE_VAL (i.e. 1000) 404 | if (this.max > this.config.MAX_PURE_SCALE_VAL) { 405 | val /= this.scaleFactor; 406 | val = Math.round(val * 100) / 100; 407 | posMargin /= 2; 408 | } 409 | 410 | this.scaleValues.push({ 411 | text: val.toString(), 412 | coor: { 413 | x: sin * (lowerEnd - posMargin) + this.center, 414 | y: cos * (lowerEnd - posMargin) + this.center, 415 | }, 416 | }); 417 | } 418 | } 419 | --------------------------------------------------------------------------------