From 5d3a1f07c80e43b981414f3d5c5d8a50e37a1002 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 20 Jun 2025 16:13:24 +0100 Subject: [PATCH] Theme: Make viz colors configurable by the theme input (#106974) * make viz colors configurable when creating the theme * fix bug with palette not showing last color * attempt to constrain the types better * better generics * remove reverseMap * ensure there's an empty options default --- .../grafana-data/src/themes/createTheme.ts | 6 ++- .../src/themes/createVisualizationColors.ts | 54 +++++++++++++++---- .../ColorPicker/NamedColorsGroup.tsx | 7 ++- .../grafana-ui/src/utils/reverseMap.test.ts | 15 ------ packages/grafana-ui/src/utils/reverseMap.ts | 9 ---- .../core/components/OptionsUI/fieldColor.tsx | 2 +- 6 files changed, 54 insertions(+), 39 deletions(-) delete mode 100644 packages/grafana-ui/src/utils/reverseMap.test.ts delete mode 100644 packages/grafana-ui/src/utils/reverseMap.ts diff --git a/packages/grafana-data/src/themes/createTheme.ts b/packages/grafana-data/src/themes/createTheme.ts index 554e567c7c4..fd4d8080a4e 100644 --- a/packages/grafana-data/src/themes/createTheme.ts +++ b/packages/grafana-data/src/themes/createTheme.ts @@ -7,7 +7,7 @@ import { createSpacing, ThemeSpacingOptions } from './createSpacing'; import { createTransitions } from './createTransitions'; import { createTypography, ThemeTypographyInput } from './createTypography'; import { createV1Theme } from './createV1Theme'; -import { createVisualizationColors } from './createVisualizationColors'; +import { createVisualizationColors, ThemeVisualizationColorsInput } from './createVisualizationColors'; import { GrafanaTheme2 } from './types'; import { zIndex } from './zIndex'; @@ -18,6 +18,7 @@ export interface NewThemeOptions { spacing?: ThemeSpacingOptions; shape?: ThemeShapeInput; typography?: ThemeTypographyInput; + visualization?: ThemeVisualizationColorsInput; } /** @internal */ @@ -28,6 +29,7 @@ export function createTheme(options: NewThemeOptions = {}): GrafanaTheme2 { spacing: spacingInput = {}, shape: shapeInput = {}, typography: typographyInput = {}, + visualization: visualizationInput = {}, } = options; const colors = createColors(colorsInput); @@ -38,7 +40,7 @@ export function createTheme(options: NewThemeOptions = {}): GrafanaTheme2 { const shadows = createShadows(colors); const transitions = createTransitions(); const components = createComponents(colors, shadows); - const visualization = createVisualizationColors(colors); + const visualization = createVisualizationColors(colors, visualizationInput); const theme = { name: name ?? (colors.mode === 'dark' ? 'Dark' : 'Light'), diff --git a/packages/grafana-data/src/themes/createVisualizationColors.ts b/packages/grafana-data/src/themes/createVisualizationColors.ts index 36b908add01..7c53debb700 100644 --- a/packages/grafana-data/src/themes/createVisualizationColors.ts +++ b/packages/grafana-data/src/themes/createVisualizationColors.ts @@ -17,26 +17,62 @@ export interface ThemeVisualizationColors { /** * @alpha */ -export interface ThemeVizColor { +export interface ThemeVizColor { color: string; - name: string; + name: ThemeVizColorShadeName; aliases?: string[]; primary?: boolean; } +type ThemeVizColorName = 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'purple'; + +type ThemeVizColorShadeName = + | `super-light-${T}` + | `light-${T}` + | T + | `semi-dark-${T}` + | `dark-${T}`; + +type ThemeVizHueGeneric = T extends ThemeVizColorName + ? { + name: T; + shades: Array>; + } + : never; + /** * @alpha */ -export interface ThemeVizHue { - name: string; - shades: ThemeVizColor[]; -} +export type ThemeVizHue = ThemeVizHueGeneric; + +export type ThemeVisualizationColorsInput = { + hues?: ThemeVizHue[]; + palette?: string[]; +}; /** * @internal */ -export function createVisualizationColors(colors: ThemeColors): ThemeVisualizationColors { - const hues = colors.mode === 'light' ? getLightHues() : getDarkHues(); +export function createVisualizationColors( + colors: ThemeColors, + options: ThemeVisualizationColorsInput = {} +): ThemeVisualizationColors { + const baseHues = colors.mode === 'light' ? getLightHues() : getDarkHues(); + const { palette = getClassicPalette(), hues: hueOverrides = [] } = options; + + const hues = [...baseHues]; + // override hues with user provided + for (const hueOverride of hueOverrides) { + const existingHue = hues.find((hue) => hue.name === hueOverride.name); + if (existingHue) { + for (const shadeOverride of hueOverride.shades) { + const existingShade = existingHue.shades.find((shade) => shade.name === shadeOverride.name); + if (existingShade) { + existingShade.color = shadeOverride.color; + } + } + } + } const byNameIndex: Record = {}; @@ -83,8 +119,6 @@ export function createVisualizationColors(colors: ThemeColors): ThemeVisualizati return colorName; }; - const palette = getClassicPalette(); - return { hues, palette, diff --git a/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx b/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx index 3724eccf7ee..376a63775a7 100644 --- a/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx @@ -1,11 +1,11 @@ import { css } from '@emotion/css'; import { Property } from 'csstype'; import { upperFirst } from 'lodash'; +import { useMemo } from 'react'; import { GrafanaTheme2, ThemeVizHue } from '@grafana/data'; import { useStyles2 } from '../../themes/ThemeContext'; -import { reverseMap } from '../../utils/reverseMap'; import { ColorSwatch, ColorSwatchVariant } from './ColorSwatch'; @@ -19,12 +19,15 @@ interface NamedColorsGroupProps { const NamedColorsGroup = ({ hue, selectedColor, onColorSelect, ...otherProps }: NamedColorsGroupProps) => { const label = upperFirst(hue.name); const styles = useStyles2(getStyles); + const reversedShades = useMemo(() => { + return [...hue.shades].reverse(); + }, [hue.shades]); return (
{label}
- {reverseMap(hue.shades, (shade) => ( + {reversedShades.map((shade) => ( { - it('Maps elements in reverse', () => { - const elements = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; - const reversedAndMapped = reverseMap(elements, (i) => i ** 2); - expect(reversedAndMapped).toEqual([100, 81, 64, 49, 36, 25, 16, 9, 4, 1]); - }); - - it('Maps array of objects in reverse', () => { - const elements = [{ title: 'this' }, { title: 'is' }, { title: 'a' }, { title: 'test' }]; - const reversedAndMapped = reverseMap(elements, (v) => ({ title: v.title.toUpperCase() })); - expect(reversedAndMapped).toEqual([{ title: 'TEST' }, { title: 'A' }, { title: 'IS' }, { title: 'THIS' }]); - }); -}); diff --git a/packages/grafana-ui/src/utils/reverseMap.ts b/packages/grafana-ui/src/utils/reverseMap.ts deleted file mode 100644 index f9265535323..00000000000 --- a/packages/grafana-ui/src/utils/reverseMap.ts +++ /dev/null @@ -1,9 +0,0 @@ -export function reverseMap(arr: ArrayLike, callbackfn: (value: T, index: number, array: ArrayLike) => Q) { - const reversedAndMapped = new Array(arr.length); - for (let i = 0; i < arr.length; i++) { - const reverseIndex = arr.length - 1 - i; - reversedAndMapped[i] = callbackfn(arr[reverseIndex], reverseIndex, arr); - } - - return reversedAndMapped; -} diff --git a/public/app/core/components/OptionsUI/fieldColor.tsx b/public/app/core/components/OptionsUI/fieldColor.tsx index 1f5a7c5b240..39833b21c03 100644 --- a/public/app/core/components/OptionsUI/fieldColor.tsx +++ b/public/app/core/components/OptionsUI/fieldColor.tsx @@ -139,7 +139,7 @@ const FieldColorModeViz: FC = ({ mode, theme }) => { if (gradient === '') { gradient = `linear-gradient(90deg, ${color} 0%`; } else { - const valuePercent = i / (colors.length - 1); + const valuePercent = i / colors.length; const pos = valuePercent * 100; gradient += `, ${lastColor} ${pos}%, ${color} ${pos}%`; }