From 0181dc183ba616cea4e49d0d7a8efa2f9a4c63d4 Mon Sep 17 00:00:00 2001 From: Luke Palmer Date: Sat, 15 Apr 2023 02:08:28 -0400 Subject: [PATCH] Visualizations: Choose color based on series name (#66197) --- .../configure-standard-options/index.md | 25 +++++++++--------- packages/grafana-data/package.json | 2 ++ .../grafana-data/src/field/fieldColor.test.ts | 17 ++++++++---- packages/grafana-data/src/field/fieldColor.ts | 26 +++++++++++++++++++ packages/grafana-data/src/types/fieldColor.ts | 1 + yarn.lock | 9 +++++++ 6 files changed, 63 insertions(+), 17 deletions(-) diff --git a/docs/sources/panels-visualizations/configure-standard-options/index.md b/docs/sources/panels-visualizations/configure-standard-options/index.md index e9e1080739e..d82cc74574d 100644 --- a/docs/sources/panels-visualizations/configure-standard-options/index.md +++ b/docs/sources/panels-visualizations/configure-standard-options/index.md @@ -116,18 +116,19 @@ Select one of the following palettes:
-| Color mode | Description | -| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **Single color** | Specify a single color, useful in an override rule | -| **Shades of a color** | Selects shades of a single color, useful in an override rule | -| **From thresholds** | Informs Grafana to take the color from the matching threshold | -| **Classic palette** | Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations | -| **Green-Yellow-Red (by value)** | Continuous color scheme | -| **Blue-Yellow-Red (by value)** | Continuous color scheme | -| **Blues (by value)** | Continuous color scheme (panel background to blue) | -| **Reds (by value)** | Continuous color scheme (panel background color to blue) | -| **Greens (by value)** | Continuous color scheme (panel background color to blue) | -| **Purple (by value)** | Continuous color scheme (panel background color to blue) | +| Color mode | Description | +| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Single color** | Specify a single color, useful in an override rule | +| **Shades of a color** | Selects shades of a single color, useful in an override rule | +| **From thresholds** | Informs Grafana to take the color from the matching threshold | +| **Classic palette** | Grafana will assign color by looking up a color in a palette by series index. Useful for Graphs and pie charts and other categorical data visualizations | +| **Classic palette (by series name)** | Grafana will assign color based on the name of the series. Useful when the series names to be visualized depend on the available data. | +| **Green-Yellow-Red (by value)** | Continuous color scheme | +| **Blue-Yellow-Red (by value)** | Continuous color scheme | +| **Blues (by value)** | Continuous color scheme (panel background to blue) | +| **Reds (by value)** | Continuous color scheme (panel background color to blue) | +| **Greens (by value)** | Continuous color scheme (panel background color to blue) | +| **Purple (by value)** | Continuous color scheme (panel background color to blue) | {{< figure src="/static/img/docs/v73/color_scheme_dropdown.png" max-width="350px" caption="Color scheme" >}} diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 7032827909b..30c421f565b 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -38,6 +38,7 @@ "@braintree/sanitize-url": "6.0.2", "@grafana/schema": "10.0.0-pre", "@types/d3-interpolate": "^3.0.0", + "@types/string-hash": "1.1.1", "d3-interpolate": "3.0.1", "date-fns": "2.29.3", "dompurify": "^2.4.3", @@ -53,6 +54,7 @@ "react-use": "17.4.0", "regenerator-runtime": "0.13.11", "rxjs": "7.8.0", + "string-hash": "^1.1.3", "tinycolor2": "1.6.0", "tslib": "2.5.0", "uplot": "1.6.24", diff --git a/packages/grafana-data/src/field/fieldColor.test.ts b/packages/grafana-data/src/field/fieldColor.test.ts index a279429f997..43813f1f10a 100644 --- a/packages/grafana-data/src/field/fieldColor.test.ts +++ b/packages/grafana-data/src/field/fieldColor.test.ts @@ -3,9 +3,9 @@ import { Field, FieldColorModeId, FieldType } from '../types'; import { fieldColorModeRegistry, FieldValueColorCalculator, getFieldSeriesColor } from './fieldColor'; -function getTestField(mode: string, fixedColor?: string): Field { +function getTestField(mode: string, fixedColor?: string, name = 'name'): Field { return { - name: 'name', + name: name, type: FieldType.number, values: [], config: { @@ -21,11 +21,12 @@ function getTestField(mode: string, fixedColor?: string): Field { interface GetCalcOptions { mode: string; seriesIndex?: number; + name?: string; fixedColor?: string; } function getCalculator(options: GetCalcOptions): FieldValueColorCalculator { - const field = getTestField(options.mode, options.fixedColor); + const field = getTestField(options.mode, options.fixedColor, options.name); const mode = fieldColorModeRegistry.get(options.mode); field.state!.seriesIndex = options.seriesIndex; return mode.getCalculator(field, createTheme()); @@ -38,15 +39,21 @@ describe('fieldColorModeRegistry', () => { }); it('Palette classic with series index 0', () => { - const calcFn = getCalculator({ mode: FieldColorModeId.PaletteClassic, seriesIndex: 0 }); + const calcFn = getCalculator({ mode: FieldColorModeId.PaletteClassic, seriesIndex: 0, name: 'series1' }); expect(calcFn(70, 0, undefined)).toEqual('#73BF69'); }); it('Palette classic with series index 1', () => { - const calcFn = getCalculator({ mode: FieldColorModeId.PaletteClassic, seriesIndex: 1 }); + const calcFn = getCalculator({ mode: FieldColorModeId.PaletteClassic, seriesIndex: 1, name: 'series2' }); expect(calcFn(70, 0, undefined)).toEqual('#F2CC0C'); }); + it('Palette uses name', () => { + const calcFn1 = getCalculator({ mode: FieldColorModeId.PaletteClassicByName, seriesIndex: 0, name: 'same name' }); + const calcFn2 = getCalculator({ mode: FieldColorModeId.PaletteClassicByName, seriesIndex: 1, name: 'same name' }); + expect(calcFn1(12, 34, undefined)).toEqual(calcFn2(56, 78, undefined)); + }); + it('When color.seriesBy is set to last use that instead of v', () => { const field = getTestField('continuous-GrYlRd'); diff --git a/packages/grafana-data/src/field/fieldColor.ts b/packages/grafana-data/src/field/fieldColor.ts index a9d09572998..289177150de 100644 --- a/packages/grafana-data/src/field/fieldColor.ts +++ b/packages/grafana-data/src/field/fieldColor.ts @@ -1,6 +1,8 @@ import { interpolateRgbBasis } from 'd3-interpolate'; +import stringHash from 'string-hash'; import tinycolor from 'tinycolor2'; +import { colorManipulator } from '../themes'; import { GrafanaTheme2 } from '../themes/types'; import { reduceField } from '../transformations/fieldReducer'; import { FALLBACK_COLOR, Field, FieldColorModeId, Threshold } from '../types'; @@ -19,6 +21,7 @@ export interface FieldColorMode extends RegistryItem { getColors?: (theme: GrafanaTheme2) => string[]; isContinuous?: boolean; isByValue?: boolean; + useSeriesName?: boolean; } /** @internal */ @@ -57,6 +60,22 @@ export const fieldColorModeRegistry = new Registry(() => { return theme.visualization.palette; }, }), + new FieldColorSchemeMode({ + id: FieldColorModeId.PaletteClassicByName, + name: 'Classic palette (by series name)', + isContinuous: false, + isByValue: false, + useSeriesName: true, + getColors: (theme: GrafanaTheme2) => { + return theme.visualization.palette.filter( + (color) => + colorManipulator.getContrastRatio( + theme.visualization.getColorByName(color), + theme.colors.background.primary + ) >= theme.colors.contrastThreshold + ); + }, + }), new FieldColorSchemeMode({ id: FieldColorModeId.ContinuousGrYlRd, name: 'Green-Yellow-Red', @@ -137,6 +156,7 @@ interface FieldColorSchemeModeOptions { getColors: (theme: GrafanaTheme2) => string[]; isContinuous: boolean; isByValue: boolean; + useSeriesName?: boolean; } export class FieldColorSchemeMode implements FieldColorMode { @@ -145,6 +165,7 @@ export class FieldColorSchemeMode implements FieldColorMode { description?: string; isContinuous: boolean; isByValue: boolean; + useSeriesName?: boolean; colorCache?: string[]; colorCacheTheme?: GrafanaTheme2; interpolator?: (value: number) => string; @@ -157,6 +178,7 @@ export class FieldColorSchemeMode implements FieldColorMode { this.getNamedColors = options.getColors; this.isContinuous = options.isContinuous; this.isByValue = options.isByValue; + this.useSeriesName = options.useSeriesName; } getColors(theme: GrafanaTheme2): string[] { @@ -195,6 +217,10 @@ export class FieldColorSchemeMode implements FieldColorMode { return colors[percent * (colors.length - 1)]; }; } + } else if (this.useSeriesName) { + return (_: number, _percent: number, _threshold?: Threshold) => { + return colors[Math.abs(stringHash(field.name)) % colors.length]; + }; } else { return (_: number, _percent: number, _threshold?: Threshold) => { const seriesIndex = field.state?.seriesIndex ?? 0; diff --git a/packages/grafana-data/src/types/fieldColor.ts b/packages/grafana-data/src/types/fieldColor.ts index d58bb68ceab..8dcbb526aa3 100644 --- a/packages/grafana-data/src/types/fieldColor.ts +++ b/packages/grafana-data/src/types/fieldColor.ts @@ -4,6 +4,7 @@ export enum FieldColorModeId { Thresholds = 'thresholds', PaletteClassic = 'palette-classic', + PaletteClassicByName = 'palette-classic-by-name', PaletteSaturated = 'palette-saturated', ContinuousGrYlRd = 'continuous-GrYlRd', ContinuousRdYlGr = 'continuous-RdYlGr', diff --git a/yarn.lock b/yarn.lock index 1e0b8d3acd4..920cb076c38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3049,6 +3049,7 @@ __metadata: "@types/react": 18.0.28 "@types/react-dom": 18.0.11 "@types/sinon": 10.0.13 + "@types/string-hash": 1.1.1 "@types/testing-library__jest-dom": 5.14.5 "@types/tinycolor2": 1.4.3 d3-interpolate: 3.0.1 @@ -3076,6 +3077,7 @@ __metadata: rollup-plugin-node-externals: ^5.0.0 rxjs: 7.8.0 sinon: 15.0.1 + string-hash: ^1.1.3 tinycolor2: 1.6.0 tslib: 2.5.0 typescript: 4.8.4 @@ -10398,6 +10400,13 @@ __metadata: languageName: node linkType: hard +"@types/string-hash@npm:1.1.1": + version: 1.1.1 + resolution: "@types/string-hash@npm:1.1.1" + checksum: e7cdba66dc1cfd88033d66a0fd301abd1434f8e3fc22826443073e5c33a26e5c53a26ef4e062d0ebf3f8590f506dec2351c4b952df1d04ba188bc0a48f411f17 + languageName: node + linkType: hard + "@types/symlink-or-copy@npm:^1.2.0": version: 1.2.0 resolution: "@types/symlink-or-copy@npm:1.2.0"