Dashboard: Add series color shades (#61300)

* 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 <torkel@grafana.com>
Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
Co-authored-by: Kristina Durivage <kristina.durivage@grafana.com>
This commit is contained in:
Jiří Kraml 2023-04-13 23:03:31 +02:00 committed by GitHub
parent 4a7f27489e
commit 8d6314c654
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 62 additions and 3 deletions

View File

@ -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 |

View File

@ -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', () => {

View File

@ -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<FieldColorMode>(() => {
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];
};
}

View File

@ -16,6 +16,7 @@ export enum FieldColorModeId {
ContinuousGreens = 'continuous-greens',
ContinuousPurples = 'continuous-purples',
Fixed = 'fixed',
Shades = 'shades',
}
/**

View File

@ -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 (
<div className={styles.group}>
<Select