From 8d6314c6549586c794bfee249e1b5fb23662d6d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Kraml?= <117438585+jkraml-staffbase@users.noreply.github.com> Date: Thu, 13 Apr 2023 23:03:31 +0200 Subject: [PATCH] Dashboard: Add series color shades (#61300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Dashboard: Add series color shades Add color option "Shades of a color" which gives each series a color derived from a user-selected base color. * Documentation: Add entry for color shades Describe color option "Shades of a color" in documentation. * Chore: formatting fixes * Dashboard: Use better fallback color for color shades in fieldColor.ts Fall back to a gray color if the configured color cannot be parsed. * Chore: fix typo in fieldColor.test.ts * Documentation: fix a sentence * remove custom color parsing and change logic a bit * Fix prettier issue --------- Co-authored-by: Torkel Ödegaard Co-authored-by: nmarrs Co-authored-by: Kristina Durivage --- .../configure-standard-options/index.md | 1 + .../grafana-data/src/field/fieldColor.test.ts | 18 +++++++- packages/grafana-data/src/field/fieldColor.ts | 43 +++++++++++++++++++ packages/grafana-data/src/types/fieldColor.ts | 1 + .../core/components/OptionsUI/fieldColor.tsx | 2 +- 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/docs/sources/panels-visualizations/configure-standard-options/index.md b/docs/sources/panels-visualizations/configure-standard-options/index.md index cc15a0b91b3..e9e1080739e 100644 --- a/docs/sources/panels-visualizations/configure-standard-options/index.md +++ b/docs/sources/panels-visualizations/configure-standard-options/index.md @@ -119,6 +119,7 @@ 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 | diff --git a/packages/grafana-data/src/field/fieldColor.test.ts b/packages/grafana-data/src/field/fieldColor.test.ts index bf20bce0cf8..9652e91fb4e 100644 --- a/packages/grafana-data/src/field/fieldColor.test.ts +++ b/packages/grafana-data/src/field/fieldColor.test.ts @@ -4,7 +4,7 @@ import { ArrayVector } from '../vector/ArrayVector'; import { fieldColorModeRegistry, FieldValueColorCalculator, getFieldSeriesColor } from './fieldColor'; -function getTestField(mode: string): Field { +function getTestField(mode: string, fixedColor?: string): Field { return { name: 'name', type: FieldType.number, @@ -12,6 +12,7 @@ function getTestField(mode: string): Field { config: { color: { mode: mode, + fixedColor: fixedColor, }, }, state: {}, @@ -21,10 +22,11 @@ function getTestField(mode: string): Field { interface GetCalcOptions { mode: string; seriesIndex?: number; + fixedColor?: string; } function getCalculator(options: GetCalcOptions): FieldValueColorCalculator { - const field = getTestField(options.mode); + const field = getTestField(options.mode, options.fixedColor); const mode = fieldColorModeRegistry.get(options.mode); field.state!.seriesIndex = options.seriesIndex; return mode.getCalculator(field, createTheme()); @@ -59,6 +61,18 @@ describe('fieldColorModeRegistry', () => { expect(color.color).toEqual(calcFn(4, 0.75)); }); + + it('Shades should return selected color for index 0', () => { + const color = '#123456'; + const calcFn = getCalculator({ mode: FieldColorModeId.Shades, seriesIndex: 0, fixedColor: color }); + expect(calcFn(70, 0, undefined)).toEqual(color); + }); + + it('Shades should return different than selected color for index 1', () => { + const color = '#123456'; + const calcFn = getCalculator({ mode: FieldColorModeId.Shades, seriesIndex: 1, fixedColor: color }); + expect(calcFn(70, 0, undefined)).not.toEqual(color); + }); }); describe('getFieldSeriesColor', () => { diff --git a/packages/grafana-data/src/field/fieldColor.ts b/packages/grafana-data/src/field/fieldColor.ts index 8cd714e7f7a..a9d09572998 100644 --- a/packages/grafana-data/src/field/fieldColor.ts +++ b/packages/grafana-data/src/field/fieldColor.ts @@ -1,4 +1,5 @@ import { interpolateRgbBasis } from 'd3-interpolate'; +import tinycolor from 'tinycolor2'; import { GrafanaTheme2 } from '../themes/types'; import { reduceField } from '../transformations/fieldReducer'; @@ -29,6 +30,12 @@ export const fieldColorModeRegistry = new Registry(() => { description: 'Set a specific color', getCalculator: getFixedColor, }, + { + id: FieldColorModeId.Shades, + name: 'Shades of a color', + description: 'Select shades of a specific color', + getCalculator: getShadedColor, + }, { id: FieldColorModeId.Thresholds, name: 'From thresholds', @@ -236,3 +243,39 @@ function getFixedColor(field: Field, theme: GrafanaTheme2) { return theme.visualization.getColorByName(field.config.color?.fixedColor ?? FALLBACK_COLOR); }; } + +function getShadedColor(field: Field, theme: GrafanaTheme2) { + return () => { + const baseColorString: string = theme.visualization.getColorByName( + field.config.color?.fixedColor ?? FALLBACK_COLOR + ); + + const colors: string[] = [ + baseColorString, // start with base color + ]; + + const shadesCount = 6; + const maxHueSpin = 10; // hue spin, max is 360 + const maxDarken = 35; // max 100% + const maxBrighten = 35; // max 100% + + for (let i = 1; i < shadesCount; i++) { + // push alternating darker and brighter shades + colors.push( + tinycolor(baseColorString) + .spin((i / shadesCount) * maxHueSpin) + .brighten((i / shadesCount) * maxDarken) + .toHexString() + ); + colors.push( + tinycolor(baseColorString) + .spin(-(i / shadesCount) * maxHueSpin) + .darken((i / shadesCount) * maxBrighten) + .toHexString() + ); + } + + const seriesIndex = field.state?.seriesIndex ?? 0; + return colors[seriesIndex % colors.length]; + }; +} diff --git a/packages/grafana-data/src/types/fieldColor.ts b/packages/grafana-data/src/types/fieldColor.ts index 465c268e1a5..d58bb68ceab 100644 --- a/packages/grafana-data/src/types/fieldColor.ts +++ b/packages/grafana-data/src/types/fieldColor.ts @@ -16,6 +16,7 @@ export enum FieldColorModeId { ContinuousGreens = 'continuous-greens', ContinuousPurples = 'continuous-purples', Fixed = 'fixed', + Shades = 'shades', } /** diff --git a/public/app/core/components/OptionsUI/fieldColor.tsx b/public/app/core/components/OptionsUI/fieldColor.tsx index 72855b33699..3576efc7d0e 100644 --- a/public/app/core/components/OptionsUI/fieldColor.tsx +++ b/public/app/core/components/OptionsUI/fieldColor.tsx @@ -68,7 +68,7 @@ export const FieldColorEditor = ({ value, onChange, item, id }: Props) => { const mode = value?.mode ?? FieldColorModeId.Thresholds; - if (mode === FieldColorModeId.Fixed) { + if (mode === FieldColorModeId.Fixed || mode === FieldColorModeId.Shades) { return (