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
pull/107030/merge
Ashley Harrison 1 week ago committed by GitHub
parent ac2d2bb2b1
commit 5d3a1f07c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      packages/grafana-data/src/themes/createTheme.ts
  2. 54
      packages/grafana-data/src/themes/createVisualizationColors.ts
  3. 7
      packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx
  4. 15
      packages/grafana-ui/src/utils/reverseMap.test.ts
  5. 9
      packages/grafana-ui/src/utils/reverseMap.ts
  6. 2
      public/app/core/components/OptionsUI/fieldColor.tsx

@ -7,7 +7,7 @@ import { createSpacing, ThemeSpacingOptions } from './createSpacing';
import { createTransitions } from './createTransitions'; import { createTransitions } from './createTransitions';
import { createTypography, ThemeTypographyInput } from './createTypography'; import { createTypography, ThemeTypographyInput } from './createTypography';
import { createV1Theme } from './createV1Theme'; import { createV1Theme } from './createV1Theme';
import { createVisualizationColors } from './createVisualizationColors'; import { createVisualizationColors, ThemeVisualizationColorsInput } from './createVisualizationColors';
import { GrafanaTheme2 } from './types'; import { GrafanaTheme2 } from './types';
import { zIndex } from './zIndex'; import { zIndex } from './zIndex';
@ -18,6 +18,7 @@ export interface NewThemeOptions {
spacing?: ThemeSpacingOptions; spacing?: ThemeSpacingOptions;
shape?: ThemeShapeInput; shape?: ThemeShapeInput;
typography?: ThemeTypographyInput; typography?: ThemeTypographyInput;
visualization?: ThemeVisualizationColorsInput;
} }
/** @internal */ /** @internal */
@ -28,6 +29,7 @@ export function createTheme(options: NewThemeOptions = {}): GrafanaTheme2 {
spacing: spacingInput = {}, spacing: spacingInput = {},
shape: shapeInput = {}, shape: shapeInput = {},
typography: typographyInput = {}, typography: typographyInput = {},
visualization: visualizationInput = {},
} = options; } = options;
const colors = createColors(colorsInput); const colors = createColors(colorsInput);
@ -38,7 +40,7 @@ export function createTheme(options: NewThemeOptions = {}): GrafanaTheme2 {
const shadows = createShadows(colors); const shadows = createShadows(colors);
const transitions = createTransitions(); const transitions = createTransitions();
const components = createComponents(colors, shadows); const components = createComponents(colors, shadows);
const visualization = createVisualizationColors(colors); const visualization = createVisualizationColors(colors, visualizationInput);
const theme = { const theme = {
name: name ?? (colors.mode === 'dark' ? 'Dark' : 'Light'), name: name ?? (colors.mode === 'dark' ? 'Dark' : 'Light'),

@ -17,26 +17,62 @@ export interface ThemeVisualizationColors {
/** /**
* @alpha * @alpha
*/ */
export interface ThemeVizColor { export interface ThemeVizColor<T extends ThemeVizColorName> {
color: string; color: string;
name: string; name: ThemeVizColorShadeName<T>;
aliases?: string[]; aliases?: string[];
primary?: boolean; primary?: boolean;
} }
type ThemeVizColorName = 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'purple';
type ThemeVizColorShadeName<T extends ThemeVizColorName> =
| `super-light-${T}`
| `light-${T}`
| T
| `semi-dark-${T}`
| `dark-${T}`;
type ThemeVizHueGeneric<T> = T extends ThemeVizColorName
? {
name: T;
shades: Array<ThemeVizColor<T>>;
}
: never;
/** /**
* @alpha * @alpha
*/ */
export interface ThemeVizHue { export type ThemeVizHue = ThemeVizHueGeneric<ThemeVizColorName>;
name: string;
shades: ThemeVizColor[]; export type ThemeVisualizationColorsInput = {
} hues?: ThemeVizHue[];
palette?: string[];
};
/** /**
* @internal * @internal
*/ */
export function createVisualizationColors(colors: ThemeColors): ThemeVisualizationColors { export function createVisualizationColors(
const hues = colors.mode === 'light' ? getLightHues() : getDarkHues(); 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<string, string> = {}; const byNameIndex: Record<string, string> = {};
@ -83,8 +119,6 @@ export function createVisualizationColors(colors: ThemeColors): ThemeVisualizati
return colorName; return colorName;
}; };
const palette = getClassicPalette();
return { return {
hues, hues,
palette, palette,

@ -1,11 +1,11 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { Property } from 'csstype'; import { Property } from 'csstype';
import { upperFirst } from 'lodash'; import { upperFirst } from 'lodash';
import { useMemo } from 'react';
import { GrafanaTheme2, ThemeVizHue } from '@grafana/data'; import { GrafanaTheme2, ThemeVizHue } from '@grafana/data';
import { useStyles2 } from '../../themes/ThemeContext'; import { useStyles2 } from '../../themes/ThemeContext';
import { reverseMap } from '../../utils/reverseMap';
import { ColorSwatch, ColorSwatchVariant } from './ColorSwatch'; import { ColorSwatch, ColorSwatchVariant } from './ColorSwatch';
@ -19,12 +19,15 @@ interface NamedColorsGroupProps {
const NamedColorsGroup = ({ hue, selectedColor, onColorSelect, ...otherProps }: NamedColorsGroupProps) => { const NamedColorsGroup = ({ hue, selectedColor, onColorSelect, ...otherProps }: NamedColorsGroupProps) => {
const label = upperFirst(hue.name); const label = upperFirst(hue.name);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const reversedShades = useMemo(() => {
return [...hue.shades].reverse();
}, [hue.shades]);
return ( return (
<div className={styles.colorRow}> <div className={styles.colorRow}>
<div className={styles.colorLabel}>{label}</div> <div className={styles.colorLabel}>{label}</div>
<div {...otherProps} className={styles.swatchRow}> <div {...otherProps} className={styles.swatchRow}>
{reverseMap(hue.shades, (shade) => ( {reversedShades.map((shade) => (
<ColorSwatch <ColorSwatch
key={shade.name} key={shade.name}
aria-label={shade.name} aria-label={shade.name}

@ -1,15 +0,0 @@
import { reverseMap } from './reverseMap';
describe('Reverse map', () => {
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' }]);
});
});

@ -1,9 +0,0 @@
export function reverseMap<T, Q>(arr: ArrayLike<T>, callbackfn: (value: T, index: number, array: ArrayLike<T>) => Q) {
const reversedAndMapped = new Array<Q>(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;
}

@ -139,7 +139,7 @@ const FieldColorModeViz: FC<ModeProps> = ({ mode, theme }) => {
if (gradient === '') { if (gradient === '') {
gradient = `linear-gradient(90deg, ${color} 0%`; gradient = `linear-gradient(90deg, ${color} 0%`;
} else { } else {
const valuePercent = i / (colors.length - 1); const valuePercent = i / colors.length;
const pos = valuePercent * 100; const pos = valuePercent * 100;
gradient += `, ${lastColor} ${pos}%, ${color} ${pos}%`; gradient += `, ${lastColor} ${pos}%, ${color} ${pos}%`;
} }

Loading…
Cancel
Save