├── 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 |
35 |
93 |
94 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ng2-gauge
2 |
3 | SVG gauge component for Angular
4 |
5 |
6 |
7 |
8 |
9 | |
10 |
11 |
12 | Suitable for building virtual dashboards (initially designed for that).
13 | |
14 |
15 |
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 |
--------------------------------------------------------------------------------