diff --git a/.config/webpack/webpack.config.ts b/.config/webpack/webpack.config.ts index 53a8805..b3ba675 100644 --- a/.config/webpack/webpack.config.ts +++ b/.config/webpack/webpack.config.ts @@ -197,14 +197,14 @@ const config = async (env): Promise => { modules: [path.resolve(process.cwd(), 'src'), 'node_modules'], unsafeCache: true, }, - }; + } - if (isWSL()) { + if(isWSL()) { baseConfig.watchOptions = { poll: 3000, ignored: /node_modules/, - }; - } + }} + return baseConfig; diff --git a/CHANGELOG.md b/CHANGELOG.md index 468e163..7e61832 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,14 +2,14 @@ All changes noted here. -## v2.0.0 - 2023-09-21 +## v2.0.0 - 2023-09-23 - Rewritten from Angular to React -- NEW: Needle Width can now be set +- NEW: Needle Width can now be specified - NEW: Thresholds now use the standard Grafana threshold mechanics - NEW: Needle can optionally exceed the tick mark (min and max) to show values that are outside of limits -- NEW: Preset gauge types can be selected and then modified +- NEW: Needle Center can use all marker types, with arrow-inverse added to options ## v0.0.9 - 2021-04-21 diff --git a/README.md b/README.md index 083d6a1..9817e65 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,8 @@ See the [CONTRIBUTING.md](CONTRIBUTING.md) doc for more information. ## Acknowledgements -This panel is based on the "SingleStat" panel by Grafana, along with large portions of these excellent D3 examples: +This panel is based on the "SingleStat" panel by Grafana, along with large + portions of these excellent D3 examples: * * diff --git a/src/components/Gauge.tsx b/src/components/Gauge.tsx index a9dd6c7..0532c09 100644 --- a/src/components/Gauge.tsx +++ b/src/components/Gauge.tsx @@ -4,7 +4,7 @@ import { useStyles2, useTheme2 } from '@grafana/ui'; import { css } from '@emotion/css'; import { getActiveThreshold, GrafanaTheme2, Threshold, sortThresholds } from '@grafana/data'; -import { ExpandedThresholdBand, GaugeOptions, MarkerEndShapes, MarkerStartShapes } from './types'; +import { ExpandedThresholdBand, GaugeOptions, Markers } from './types'; import { scaleLinear, line, interpolateString, select } from 'd3'; import { easeQuadIn } from 'd3-ease'; @@ -52,6 +52,12 @@ export const Gauge: React.FC = (options) => { const originY = options.gaugeRadius; let needleElement: JSX.Element | null = null; + /* + useEffect(() => { + console.log(`presetIndex set to ${options.presetIndex}`); + options.innerColor = GaugePresetOptions[options.presetIndex].faceColor; + }, [options]); + */ useEffect(() => { @@ -209,6 +215,8 @@ export const Gauge: React.FC = (options) => { const createNeedle = () => { const pathNeedle = needleCalc(options.zeroNeedleAngle, originX, originY, needlePathStart, needlePathLength); + const markerEndShape = Markers.find(e => e.name === options.markerEndShape) || Markers[0]; + const markerStartShape = Markers.find(e => e.name === options.markerStartShape) || Markers[1]; return ( {pathNeedle.length > 0 && ( @@ -216,8 +224,8 @@ export const Gauge: React.FC = (options) => { = (options) => { needleAngleOld = 0; } if (needleAngleNew + options.zeroNeedleAngle > options.maxTickAngle) { - needleAngleNew = getNeedleAngleMaximum(options.allowNeedleCrossLimits, needleAngleNew, options.zeroTickAngle, options.maxTickAngle, options.needleCrossLimitDegrees); + needleAngleNew = getNeedleAngleMaximum(options.allowNeedleCrossLimits, needleAngleNew, options.zeroTickAngle, options.zeroNeedleAngle, options.maxTickAngle, options.needleCrossLimitDegrees); } if (needleAngleNew + options.zeroNeedleAngle < options.zeroTickAngle) { - needleAngleNew = getNeedleAngleMinimum(options.allowNeedleCrossLimits, needleAngleNew, options.zeroTickAngle, options.needleCrossLimitDegrees); + needleAngleNew = getNeedleAngleMinimum(options.allowNeedleCrossLimits, needleAngleNew, options.zeroTickAngle, options.zeroNeedleAngle, options.needleCrossLimitDegrees); } const needleCentre = originX + ',' + originY; return interpolateString( diff --git a/src/components/needle_utils.test.tsx b/src/components/needle_utils.test.tsx index ccf6bad..c414e8e 100644 --- a/src/components/needle_utils.test.tsx +++ b/src/components/needle_utils.test.tsx @@ -3,49 +3,49 @@ describe('Needle Utils', () => { const crossLimitDegree = 5; describe('Check Min Needle Angle with cross limits disabled', () => { it('minimum angle should be at min', () => { - const atZero = getNeedleAngleMinimum(false, 0, 90, 5); + const atZero = getNeedleAngleMinimum(false, 0, 90, 90, 5); expect(atZero).toEqual(0); - const aboveZero = getNeedleAngleMinimum(false, 30, 90, 5); + const aboveZero = getNeedleAngleMinimum(false, 30, 90, 90, 5); expect(aboveZero).toEqual(30); - const belowZero = getNeedleAngleMinimum(false, -10, 90, 5); + const belowZero = getNeedleAngleMinimum(false, -10, 90, 90, 5); expect(belowZero).toEqual(90); // this will return a value beyond the max (270), and will be caught by code logic - const aboveMax = getNeedleAngleMinimum(false, 300, 90, 5); + const aboveMax = getNeedleAngleMinimum(false, 300, 90, 90, 5); expect(aboveMax).toEqual(300); }); }); describe('Check Min Needle Angle with cross limits enabled', () => { it('minimum angle should be bound by min with limit cross of 5 degrees', () => { - const belowZero = getNeedleAngleMinimum(true, -10, 90, crossLimitDegree); + const belowZero = getNeedleAngleMinimum(true, -10, 90, 90, crossLimitDegree); expect(belowZero).toEqual(-5); - const atZero = getNeedleAngleMinimum(true, 0, 90, crossLimitDegree); + const atZero = getNeedleAngleMinimum(true, 0, 90, 90, crossLimitDegree); expect(atZero).toEqual(0); - const aboveZero = getNeedleAngleMinimum(true, 30, 90, crossLimitDegree); + const aboveZero = getNeedleAngleMinimum(true, 30, 90, 90, crossLimitDegree); expect(aboveZero).toEqual(30); - const aboveMax = getNeedleAngleMinimum(true, 300, 90, crossLimitDegree); + const aboveMax = getNeedleAngleMinimum(true, 300, 90, 90, crossLimitDegree); expect(aboveMax).toEqual(300); }); }); describe('Check Max Needle Angle with cross limits disabled', () => { it('max angle should be bound by maxTickAngle', () => { - const atMax = getNeedleAngleMaximum(false, 270, 90, 270, crossLimitDegree); + const atMax = getNeedleAngleMaximum(false, 270, 90, 90, 270, crossLimitDegree); expect(atMax).toEqual(180); - const belowMax = getNeedleAngleMaximum(false, 30, 90, 270, crossLimitDegree); + const belowMax = getNeedleAngleMaximum(false, 30, 90, 90, 270, crossLimitDegree); expect(belowMax).toEqual(-60); - const aboveMax = getNeedleAngleMaximum(false, 300, 90, 270, crossLimitDegree); + const aboveMax = getNeedleAngleMaximum(false, 300, 90, 90, 270, crossLimitDegree); expect(aboveMax).toEqual(180); }); }); describe('Check Max Needle Angle with cross limits enabled', () => { it('max angle should be bound by max with limit cross of 5 degrees', () => { - const atMax = getNeedleAngleMaximum(true, 270, 90, 270, crossLimitDegree); + const atMax = getNeedleAngleMaximum(true, 270, 90, 90, 270, crossLimitDegree); expect(atMax).toEqual(185); - const belowMax = getNeedleAngleMaximum(true, 30, 90, 270, crossLimitDegree); + const belowMax = getNeedleAngleMaximum(true, 30, 90, 90, 270, crossLimitDegree); expect(belowMax).toEqual(-60); - const aboveMax = getNeedleAngleMaximum(true, 275, 90, 270, crossLimitDegree); + const aboveMax = getNeedleAngleMaximum(true, 275, 90, 90, 270, crossLimitDegree); expect(aboveMax).toEqual(185); - const aboveMax2 = getNeedleAngleMaximum(true, 320, 90, 270, crossLimitDegree); + const aboveMax2 = getNeedleAngleMaximum(true, 320, 90, 90, 270, crossLimitDegree); expect(aboveMax2).toEqual(185); }); }); diff --git a/src/components/needle_utils.tsx b/src/components/needle_utils.tsx index 406c3d4..c517e6d 100644 --- a/src/components/needle_utils.tsx +++ b/src/components/needle_utils.tsx @@ -15,31 +15,29 @@ import React from 'react'; * @param allowNeedleCrossLimits boolean * @param needleAngle angle to test * @param zeroTickAngle angle where the tick label starts + * @param zeroNeedleAngle angle where the needle starts * @param crossLimitDegree how far to cross limits - * @returns angle to be used (relative to the zeroNeedleAnge) + * @returns angle to be used (absolute angle, no longer relative) */ export const getNeedleAngleMinimum = - (allowNeedleCrossLimits: boolean, needleAngle: number, zeroTickAngle: number, crossLimitDegree: number) => { - // the angle is relative to the zeroNeedleAngle - // check if the needleAngle is below the zeroNeedleAngle - if (needleAngle + zeroTickAngle < zeroTickAngle) { + (allowNeedleCrossLimits: boolean, needleAngle: number, zeroTickAngle: number, zeroNeedleAngle: number, crossLimitDegree: number) => { + // check if the needleAngle is below the zeroTickAngle + if (needleAngle + zeroNeedleAngle < zeroTickAngle) { // check if burying the needle is enabled if (allowNeedleCrossLimits) { - // make sure it is not below zero when accounting for the zeroNeedleAngle - if (zeroTickAngle >= crossLimitDegree) { - // allow it to be set to zeroTickAngle minus 5 degrees, without going below zero + if (needleAngle < zeroTickAngle) { return (-crossLimitDegree); } else { - return (zeroTickAngle); + return (zeroNeedleAngle); } } else { - return (zeroTickAngle); + return (zeroNeedleAngle); } } return needleAngle; }; -export const getNeedleAngleMaximum = (allowNeedleCrossLimits: boolean, needleAngle: number, zeroTickAngle: number, maxTickAngle: number, crossLimitDegree: number) => { +export const getNeedleAngleMaximum = (allowNeedleCrossLimits: boolean, needleAngle: number, zeroTickAngle: number, zeroNeedleAngle: number, maxTickAngle: number, crossLimitDegree: number) => { // angle passed in is relative to zeroTickAngle if (needleAngle + zeroTickAngle > maxTickAngle) { if (allowNeedleCrossLimits) { @@ -50,16 +48,15 @@ export const getNeedleAngleMaximum = (allowNeedleCrossLimits: boolean, needleAng // make sure it is not above 360 minus cross limit if (maxTickAngle < (360 - crossLimitDegree)) { // allow it to be set to maxTickAngle plus 5 degrees, without going below zero - return(maxTickAngle + crossLimitDegree - zeroTickAngle); + return (maxTickAngle + crossLimitDegree - zeroNeedleAngle); } else { // console.log(`needle cannot be buried beyond maxNeedleAngle ${testMaxAngle}`); - return(maxTickAngle - zeroTickAngle); + return (maxTickAngle - zeroNeedleAngle); } } } else { - return (maxTickAngle - zeroTickAngle); + return (maxTickAngle - zeroNeedleAngle); } } - // remove the zeroTickAngle to get the relative value again - return (needleAngle - zeroTickAngle); + return (needleAngle - zeroNeedleAngle); }; diff --git a/src/components/types.ts b/src/components/types.ts index 908077b..b668c34 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -15,6 +15,8 @@ export interface GaugeOptions { tickLabelFontSize: number; tickFont: string; + // preset index + // presetIndex: number; // Needle Options animateNeedleValueTransition: boolean; animateNeedleValueTransitionSpeed: number; @@ -84,13 +86,7 @@ export interface GaugeOptions { // tslint:disable-next-line export interface GaugeModel {} -export const MarkerStartShapes = [ - { id: 0, name: 'circle' }, - { id: 1, name: 'square' }, - { id: 2, name: 'stub' }, -]; -export const MarkerEndShapes = [{ id: 0, name: 'arrow' }]; export enum FontFamilies { ARIAL = 'Arial', @@ -178,12 +174,6 @@ export const OperatorOptions: SelectableValue[] = [ { value: 'step', label: 'Step' }, ]; -export const MarkerOptions: SelectableValue[] = [ - { value: 0, label: 'arrow' }, - { value: 1, label: 'circle' }, - { value: 2, label: 'square' }, - { value: 3, label: 'stub' }, -]; export interface MarkerType { id: number; @@ -196,6 +186,27 @@ export const Markers: MarkerType[] = [ { id: 1, name: 'circle', path: 'M 0, 0 m -5, 0 a 5,5 0 1,0 10,0 a 5,5 0 1,0 -10,0', viewBox: '-6 -6 12 12' }, { id: 2, name: 'square', path: 'M 0,0 m -5,-5 L 5,-5 L 5,5 L -5,5 Z', viewBox: '-5 -5 10 10' }, { id: 3, name: 'stub', path: 'M 0,0 m -1,-5 L 1,-5 L 1,5 L -1,5 Z', viewBox: '-1 -5 2 10' }, + { id: 4, name: 'arrow-inverse', path: 'M 0,0 m 5,5 L -5,0 L 5,-5 Z', viewBox: '-5 -5 10 10' }, +]; + +/* +export const MarkerStartShapes = [ + { id: 0, name: 'circle' }, + { id: 1, name: 'square' }, + { id: 2, name: 'stub' }, +]; + +export const MarkerEndShapes = [ + { id: 0, name: 'arrow' } +]; +*/ + +export const MarkerOptions: SelectableValue[] = [ + { value: 'arrow', label: 'arrow' }, + { value: 'circle', label: 'circle' }, + { value: 'square', label: 'square' }, + { value: 'stub', label: 'stub' }, + { value: 'arrow-inverse', label: 'arrow-inverse' }, ]; export interface ExpandedThresholdBand { @@ -204,3 +215,15 @@ export interface ExpandedThresholdBand { max: number; color: string; } + +export interface GaugePresetType { + id: number; + name: string; + faceColor: string; +} + +export const GaugePresetOptions: GaugePresetType[] = [ + { id: 0, name: 'Default', faceColor: '#FFFFFF' }, + { id: 1, name: 'Red', faceColor: '#FF0000' }, + { id: 2, name: 'Compass', faceColor: '#00F0FF' }, +]; diff --git a/src/migrations.test.ts b/src/migrations.test.ts index 9e2b852..56041c1 100644 --- a/src/migrations.test.ts +++ b/src/migrations.test.ts @@ -7,7 +7,7 @@ import { } from './migrations'; describe('D3Gauge -> D3GaugeV2 migrations', () => { - it('only migrates old d3gauge', () => { + it('migrates empty d3gauge', () => { const panel: PanelModel = { id: 0, type: 'panel', @@ -21,6 +21,53 @@ describe('D3Gauge -> D3GaugeV2 migrations', () => { expect(options).toEqual({}); }); + it('migrates start and end markers from angular d3gauge', () => { + const panel: PanelModel = { + id: 0, + type: 'panel', + options: { + markerStartEnabled: true, + markerStartShape: 'circle', + markerEndEnabled: true, + markerEndShape: 'arrow', + }, + fieldConfig: { + defaults: {}, + overrides: [], + }, + }; + const options = PanelMigrationHandler(panel); + expect(options).toEqual({ + markerStartEnabled: true, + markerStartShape: 'circle', + markerEndEnabled: true, + markerEndShape: 'arrow', + }); + }); + it('migrates start and end disabled markers from angular d3gauge', () => { + const panel: PanelModel = { + id: 0, + type: 'panel', + options: { + markerStartEnabled: false, + markerStartShape: 'circle', + markerEndEnabled: true, + markerEndShape: 'arrow', + }, + fieldConfig: { + defaults: {}, + overrides: [], + }, + }; + const options = PanelMigrationHandler(panel); + expect(options).toEqual({ + markerStartEnabled: false, + markerStartShape: 'circle', + markerEndEnabled: true, + markerEndShape: 'arrow', + }); + }); + it('checks if roboto is available to runtime', () => { const versions = new Map([ ['8.4.11', true], diff --git a/src/migrations.ts b/src/migrations.ts index a7ec3af..f188020 100644 --- a/src/migrations.ts +++ b/src/migrations.ts @@ -2,7 +2,7 @@ import { FieldConfigSource, PanelModel, ThresholdsConfig, ThresholdsMode, ValueM import { config } from '@grafana/runtime'; import { satisfies, coerce } from 'semver'; -import { FontFamilies, GaugeOptions } from './components/types'; +import { FontFamilies, GaugeOptions, GaugePresetOptions, Markers } from './components/types'; import { TickMapItemType } from 'components/TickMaps/types'; interface AngularTickMap { @@ -147,8 +147,16 @@ export const PanelMigrationHandler = (panel: PanelModel): Partial< // @ts-ignore delete panel.gaugeDivId; // @ts-ignore + options.markerEndEnabled = panel.markerEndEnabled; + // @ts-ignore + options.markerEndShape = Markers.find(e => e.name === panel.markerEndShape) || Markers[0]; + // @ts-ignore delete panel.markerEndShapes; // @ts-ignore + options.markerStartEnabled = panel.markerStartEnabled; + // @ts-ignore + options.markerStartShape = Markers.find(e => e.name === panel.markerStartShape) || Markers[1]; + // @ts-ignore delete panel.markerStartShapes; // @ts-ignore delete panel.operatorNameOptions; diff --git a/src/module.ts b/src/module.ts index 6b20c0f..22544d4 100644 --- a/src/module.ts +++ b/src/module.ts @@ -1,11 +1,11 @@ import { FieldConfigProperty, PanelPlugin } from '@grafana/data'; import { GaugePanel } from './components/GaugePanel'; -import { FontFamilyOptions, FontSizes, GaugeOptions, MarkerEndShapes, MarkerOptions, MarkerStartShapes, OperatorOptions } from 'components/types'; +import { FontFamilyOptions, FontSizes, GaugeOptions, GaugePresetOptions, MarkerOptions, OperatorOptions } from 'components/types'; import { DataSuggestionsSupplier } from './components/suggestions'; import { PanelMigrationHandler } from './migrations'; import { TickMapEditor } from 'components/TickMaps/TickMapEditor'; import { TickMapItemType } from 'components/TickMaps/types'; -import { Field } from '@grafana/ui'; + export const plugin = new PanelPlugin(GaugePanel) .setMigrationHandler(PanelMigrationHandler) @@ -88,6 +88,24 @@ export const plugin = new PanelPlugin(GaugePanel) options: FontFamilyOptions, }, }) + + // Presets + /* + .addSelect({ + name: 'Preset', + path: 'presetIndex', + description: 'Modify current gauge with preset values', + settings: { + options: [ + { value: GaugePresetOptions[0].id, label: GaugePresetOptions[0].name }, + { value: GaugePresetOptions[1].id, label: GaugePresetOptions[1].name }, + { value: GaugePresetOptions[2].id, label: GaugePresetOptions[2].name }, + ], + }, + defaultValue: GaugePresetOptions[0].id, + category: ['Presets'], + }) + */ // animateNeedleValueTransition .addBooleanSwitch({ name: 'Animate Needle Transition', @@ -178,7 +196,7 @@ export const plugin = new PanelPlugin(GaugePanel) path: 'markerStartShape', description: 'Shape used at the end of the needle', category: ['Needle Options'], - defaultValue: MarkerOptions[2].value, + defaultValue: MarkerOptions[1].value, settings: { options: MarkerOptions, },