Visualizations: Choose color based on series name (#66197)
This commit is contained in:
@@ -116,18 +116,19 @@ Select one of the following palettes:
|
|||||||
|
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
|
|
||||||
| Color mode | Description |
|
| Color mode | Description |
|
||||||
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| **Single color** | Specify a single color, useful in an override rule |
|
| **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 |
|
| **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 |
|
| **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** | 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 |
|
| **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. |
|
||||||
| **Blue-Yellow-Red (by value)** | Continuous color scheme |
|
| **Green-Yellow-Red (by value)** | Continuous color scheme |
|
||||||
| **Blues (by value)** | Continuous color scheme (panel background to blue) |
|
| **Blue-Yellow-Red (by value)** | Continuous color scheme |
|
||||||
| **Reds (by value)** | Continuous color scheme (panel background color to blue) |
|
| **Blues (by value)** | Continuous color scheme (panel background to blue) |
|
||||||
| **Greens (by value)** | Continuous color scheme (panel background color to blue) |
|
| **Reds (by value)** | Continuous color scheme (panel background color to blue) |
|
||||||
| **Purple (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" >}}
|
{{< figure src="/static/img/docs/v73/color_scheme_dropdown.png" max-width="350px" caption="Color scheme" >}}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"@braintree/sanitize-url": "6.0.2",
|
"@braintree/sanitize-url": "6.0.2",
|
||||||
"@grafana/schema": "10.0.0-pre",
|
"@grafana/schema": "10.0.0-pre",
|
||||||
"@types/d3-interpolate": "^3.0.0",
|
"@types/d3-interpolate": "^3.0.0",
|
||||||
|
"@types/string-hash": "1.1.1",
|
||||||
"d3-interpolate": "3.0.1",
|
"d3-interpolate": "3.0.1",
|
||||||
"date-fns": "2.29.3",
|
"date-fns": "2.29.3",
|
||||||
"dompurify": "^2.4.3",
|
"dompurify": "^2.4.3",
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
"react-use": "17.4.0",
|
"react-use": "17.4.0",
|
||||||
"regenerator-runtime": "0.13.11",
|
"regenerator-runtime": "0.13.11",
|
||||||
"rxjs": "7.8.0",
|
"rxjs": "7.8.0",
|
||||||
|
"string-hash": "^1.1.3",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tslib": "2.5.0",
|
"tslib": "2.5.0",
|
||||||
"uplot": "1.6.24",
|
"uplot": "1.6.24",
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { Field, FieldColorModeId, FieldType } from '../types';
|
|||||||
|
|
||||||
import { fieldColorModeRegistry, FieldValueColorCalculator, getFieldSeriesColor } from './fieldColor';
|
import { fieldColorModeRegistry, FieldValueColorCalculator, getFieldSeriesColor } from './fieldColor';
|
||||||
|
|
||||||
function getTestField(mode: string, fixedColor?: string): Field {
|
function getTestField(mode: string, fixedColor?: string, name = 'name'): Field {
|
||||||
return {
|
return {
|
||||||
name: 'name',
|
name: name,
|
||||||
type: FieldType.number,
|
type: FieldType.number,
|
||||||
values: [],
|
values: [],
|
||||||
config: {
|
config: {
|
||||||
@@ -21,11 +21,12 @@ function getTestField(mode: string, fixedColor?: string): Field {
|
|||||||
interface GetCalcOptions {
|
interface GetCalcOptions {
|
||||||
mode: string;
|
mode: string;
|
||||||
seriesIndex?: number;
|
seriesIndex?: number;
|
||||||
|
name?: string;
|
||||||
fixedColor?: string;
|
fixedColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCalculator(options: GetCalcOptions): FieldValueColorCalculator {
|
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);
|
const mode = fieldColorModeRegistry.get(options.mode);
|
||||||
field.state!.seriesIndex = options.seriesIndex;
|
field.state!.seriesIndex = options.seriesIndex;
|
||||||
return mode.getCalculator(field, createTheme());
|
return mode.getCalculator(field, createTheme());
|
||||||
@@ -38,15 +39,21 @@ describe('fieldColorModeRegistry', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Palette classic with series index 0', () => {
|
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');
|
expect(calcFn(70, 0, undefined)).toEqual('#73BF69');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Palette classic with series index 1', () => {
|
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');
|
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', () => {
|
it('When color.seriesBy is set to last use that instead of v', () => {
|
||||||
const field = getTestField('continuous-GrYlRd');
|
const field = getTestField('continuous-GrYlRd');
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { interpolateRgbBasis } from 'd3-interpolate';
|
import { interpolateRgbBasis } from 'd3-interpolate';
|
||||||
|
import stringHash from 'string-hash';
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
|
|
||||||
|
import { colorManipulator } from '../themes';
|
||||||
import { GrafanaTheme2 } from '../themes/types';
|
import { GrafanaTheme2 } from '../themes/types';
|
||||||
import { reduceField } from '../transformations/fieldReducer';
|
import { reduceField } from '../transformations/fieldReducer';
|
||||||
import { FALLBACK_COLOR, Field, FieldColorModeId, Threshold } from '../types';
|
import { FALLBACK_COLOR, Field, FieldColorModeId, Threshold } from '../types';
|
||||||
@@ -19,6 +21,7 @@ export interface FieldColorMode extends RegistryItem {
|
|||||||
getColors?: (theme: GrafanaTheme2) => string[];
|
getColors?: (theme: GrafanaTheme2) => string[];
|
||||||
isContinuous?: boolean;
|
isContinuous?: boolean;
|
||||||
isByValue?: boolean;
|
isByValue?: boolean;
|
||||||
|
useSeriesName?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
@@ -57,6 +60,22 @@ export const fieldColorModeRegistry = new Registry<FieldColorMode>(() => {
|
|||||||
return theme.visualization.palette;
|
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({
|
new FieldColorSchemeMode({
|
||||||
id: FieldColorModeId.ContinuousGrYlRd,
|
id: FieldColorModeId.ContinuousGrYlRd,
|
||||||
name: 'Green-Yellow-Red',
|
name: 'Green-Yellow-Red',
|
||||||
@@ -137,6 +156,7 @@ interface FieldColorSchemeModeOptions {
|
|||||||
getColors: (theme: GrafanaTheme2) => string[];
|
getColors: (theme: GrafanaTheme2) => string[];
|
||||||
isContinuous: boolean;
|
isContinuous: boolean;
|
||||||
isByValue: boolean;
|
isByValue: boolean;
|
||||||
|
useSeriesName?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FieldColorSchemeMode implements FieldColorMode {
|
export class FieldColorSchemeMode implements FieldColorMode {
|
||||||
@@ -145,6 +165,7 @@ export class FieldColorSchemeMode implements FieldColorMode {
|
|||||||
description?: string;
|
description?: string;
|
||||||
isContinuous: boolean;
|
isContinuous: boolean;
|
||||||
isByValue: boolean;
|
isByValue: boolean;
|
||||||
|
useSeriesName?: boolean;
|
||||||
colorCache?: string[];
|
colorCache?: string[];
|
||||||
colorCacheTheme?: GrafanaTheme2;
|
colorCacheTheme?: GrafanaTheme2;
|
||||||
interpolator?: (value: number) => string;
|
interpolator?: (value: number) => string;
|
||||||
@@ -157,6 +178,7 @@ export class FieldColorSchemeMode implements FieldColorMode {
|
|||||||
this.getNamedColors = options.getColors;
|
this.getNamedColors = options.getColors;
|
||||||
this.isContinuous = options.isContinuous;
|
this.isContinuous = options.isContinuous;
|
||||||
this.isByValue = options.isByValue;
|
this.isByValue = options.isByValue;
|
||||||
|
this.useSeriesName = options.useSeriesName;
|
||||||
}
|
}
|
||||||
|
|
||||||
getColors(theme: GrafanaTheme2): string[] {
|
getColors(theme: GrafanaTheme2): string[] {
|
||||||
@@ -195,6 +217,10 @@ export class FieldColorSchemeMode implements FieldColorMode {
|
|||||||
return colors[percent * (colors.length - 1)];
|
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 {
|
} else {
|
||||||
return (_: number, _percent: number, _threshold?: Threshold) => {
|
return (_: number, _percent: number, _threshold?: Threshold) => {
|
||||||
const seriesIndex = field.state?.seriesIndex ?? 0;
|
const seriesIndex = field.state?.seriesIndex ?? 0;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
export enum FieldColorModeId {
|
export enum FieldColorModeId {
|
||||||
Thresholds = 'thresholds',
|
Thresholds = 'thresholds',
|
||||||
PaletteClassic = 'palette-classic',
|
PaletteClassic = 'palette-classic',
|
||||||
|
PaletteClassicByName = 'palette-classic-by-name',
|
||||||
PaletteSaturated = 'palette-saturated',
|
PaletteSaturated = 'palette-saturated',
|
||||||
ContinuousGrYlRd = 'continuous-GrYlRd',
|
ContinuousGrYlRd = 'continuous-GrYlRd',
|
||||||
ContinuousRdYlGr = 'continuous-RdYlGr',
|
ContinuousRdYlGr = 'continuous-RdYlGr',
|
||||||
|
|||||||
@@ -3049,6 +3049,7 @@ __metadata:
|
|||||||
"@types/react": 18.0.28
|
"@types/react": 18.0.28
|
||||||
"@types/react-dom": 18.0.11
|
"@types/react-dom": 18.0.11
|
||||||
"@types/sinon": 10.0.13
|
"@types/sinon": 10.0.13
|
||||||
|
"@types/string-hash": 1.1.1
|
||||||
"@types/testing-library__jest-dom": 5.14.5
|
"@types/testing-library__jest-dom": 5.14.5
|
||||||
"@types/tinycolor2": 1.4.3
|
"@types/tinycolor2": 1.4.3
|
||||||
d3-interpolate: 3.0.1
|
d3-interpolate: 3.0.1
|
||||||
@@ -3076,6 +3077,7 @@ __metadata:
|
|||||||
rollup-plugin-node-externals: ^5.0.0
|
rollup-plugin-node-externals: ^5.0.0
|
||||||
rxjs: 7.8.0
|
rxjs: 7.8.0
|
||||||
sinon: 15.0.1
|
sinon: 15.0.1
|
||||||
|
string-hash: ^1.1.3
|
||||||
tinycolor2: 1.6.0
|
tinycolor2: 1.6.0
|
||||||
tslib: 2.5.0
|
tslib: 2.5.0
|
||||||
typescript: 4.8.4
|
typescript: 4.8.4
|
||||||
@@ -10398,6 +10400,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@types/symlink-or-copy@npm:^1.2.0":
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
resolution: "@types/symlink-or-copy@npm:1.2.0"
|
resolution: "@types/symlink-or-copy@npm:1.2.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user