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
This commit is contained in:
@@ -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'),
|
||||
|
||||
@@ -17,26 +17,62 @@ export interface ThemeVisualizationColors {
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface ThemeVizColor {
|
||||
export interface ThemeVizColor<T extends ThemeVizColorName> {
|
||||
color: string;
|
||||
name: string;
|
||||
name: ThemeVizColorShadeName<T>;
|
||||
aliases?: string[];
|
||||
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
|
||||
*/
|
||||
export interface ThemeVizHue {
|
||||
name: string;
|
||||
shades: ThemeVizColor[];
|
||||
}
|
||||
export type ThemeVizHue = ThemeVizHueGeneric<ThemeVizColorName>;
|
||||
|
||||
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<string, string> = {};
|
||||
|
||||
@@ -83,8 +119,6 @@ export function createVisualizationColors(colors: ThemeColors): ThemeVisualizati
|
||||
return colorName;
|
||||
};
|
||||
|
||||
const palette = getClassicPalette();
|
||||
|
||||
return {
|
||||
hues,
|
||||
palette,
|
||||
|
||||
@@ -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 (
|
||||
<div className={styles.colorRow}>
|
||||
<div className={styles.colorLabel}>{label}</div>
|
||||
<div {...otherProps} className={styles.swatchRow}>
|
||||
{reverseMap(hue.shades, (shade) => (
|
||||
{reversedShades.map((shade) => (
|
||||
<ColorSwatch
|
||||
key={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 === '') {
|
||||
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}%`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user