make xychart color compiler handle text and icon, and de-duplicate states

This commit is contained in:
Leon Sorokin
2025-01-22 21:04:07 -06:00
parent dfc565db8d
commit 2815a5631a
2 changed files with 208 additions and 20 deletions
+53 -20
View File
@@ -2,6 +2,7 @@ import tinycolor from 'tinycolor2';
import uPlot from 'uplot';
import {
EnumFieldConfig,
FALLBACK_COLOR,
Field,
FieldType,
@@ -429,7 +430,11 @@ export const prepConfig = (xySeries: XYSeries[], theme: GrafanaTheme2) => {
const dispColors = xySeries.map((s): FieldColorValuesWithCache => {
const cfg: FieldColorValuesWithCache = {
index: [],
index: {
color: [],
text: [],
icon: [],
},
getAll: () => [],
getOne: () => -1,
// cache for renderer, refreshed in prepData()
@@ -440,8 +445,8 @@ export const prepConfig = (xySeries: XYSeries[], theme: GrafanaTheme2) => {
const f = s.color.field;
if (f != null) {
Object.assign(cfg, fieldValueColors(f, theme));
cfg.hasAlpha = cfg.index.some((v) => !(v as string).endsWith('ff'));
Object.assign(cfg, getEnumConfig(f, theme));
cfg.hasAlpha = cfg.index.color!.some((v) => !(v as string).endsWith('ff'));
}
return cfg;
@@ -551,7 +556,7 @@ function getHex8Color(color: string, theme: GrafanaTheme2) {
}
interface FieldColorValues {
index: unknown[];
index: EnumFieldConfig;
getOne: GetOneValue;
getAll: GetAllValues;
}
@@ -563,8 +568,13 @@ type GetAllValues = (values: unknown[], min?: number, max?: number) => number[];
type GetOneValue = (value: unknown, min?: number, max?: number) => number;
/** compiler for values to palette color idxs (from thresholds, mappings, by-value gradients) */
function fieldValueColors(f: Field, theme: GrafanaTheme2): FieldColorValues {
let index: unknown[] = [];
export function getEnumConfig(f: Field, theme: GrafanaTheme2): FieldColorValues {
const index: EnumFieldConfig = {
color: [],
text: [],
icon: [],
};
let getAll: GetAllValues = () => [];
let getOne: GetOneValue = () => -1;
@@ -574,21 +584,42 @@ function fieldValueColors(f: Field, theme: GrafanaTheme2): FieldColorValues {
if (f.config.mappings?.length ?? 0 > 0) {
let mappings = f.config.mappings!;
// this is color+text+icon that deduplicates the index above
// e.g. if multiple values + ranges map "OK"+"green", this ensures they map to same state by key
let keys: string[] = [];
function indexOf(color = '', text = '', icon = '') {
let key = `${color}|${text}|${icon}`;
let idx = keys.indexOf(key);
if (idx === -1) {
idx = keys.length;
keys.push(key);
index.color!.push(getHex8Color(color, theme));
index.text!.push(text);
index.icon!.push(icon);
}
return idx;
}
for (let i = 0; i < mappings.length; i++) {
let m = mappings[i];
if (m.type === MappingType.ValueToText) {
for (let k in m.options) {
let { color } = m.options[k];
let { color, text, icon } = m.options[k];
if (color != null) {
let rhs = f.type === FieldType.string ? JSON.stringify(k) : Number(k);
conds += `v === ${rhs} ? ${index.length} : `;
index.push(getHex8Color(color, theme));
let idx = indexOf(color, text, icon);
let rhs = k.toLowerCase() === 'null' ? 'null' : f.type === FieldType.string ? JSON.stringify(k) : Number(k);
conds += `v === ${rhs} ? ${idx} : `;
}
}
} else if (m.options.result.color != null) {
let { color } = m.options.result;
let { color, text, icon } = m.options.result;
if (m.type === MappingType.RangeToText) {
let range = [];
@@ -602,8 +633,8 @@ function fieldValueColors(f: Field, theme: GrafanaTheme2): FieldColorValues {
}
if (range.length > 0) {
conds += `${range.join(' && ')} ? ${index.length} : `;
index.push(getHex8Color(color, theme));
let idx = indexOf(color, text, icon);
conds += `${range.join(' && ')} ? ${idx} : `;
}
} else if (m.type === MappingType.SpecialValue) {
let spl = m.options.match;
@@ -626,8 +657,8 @@ function fieldValueColors(f: Field, theme: GrafanaTheme2): FieldColorValues {
}`;
}
conds += ` ? ${index.length} : `;
index.push(getHex8Color(color, theme));
let idx = indexOf(color, text, icon);
conds += ` ? ${idx} : `;
} else if (m.type === MappingType.RegexToText) {
// TODO
}
@@ -646,18 +677,20 @@ function fieldValueColors(f: Field, theme: GrafanaTheme2): FieldColorValues {
conds += '0';
index = steps.map((s) => getHex8Color(s.color, theme));
index.color = steps.map((s) => getHex8Color(s.color, theme));
index.text = Array(steps.length).fill('');
index.icon = index.text!.slice();
} else {
// TODO: percent thresholds?
}
} else if (f.config.color?.mode?.startsWith('continuous')) {
let calc = getFieldColorModeForField(f).getCalculator(f, theme);
index = Array(32);
index.color = Array(32);
for (let i = 0; i < index.length; i++) {
let pct = i / (index.length - 1);
index[i] = getHex8Color(calc(pct, pct), theme);
for (let i = 0; i < index.color.length; i++) {
let pct = i / (index.color.length - 1);
index.color[i] = getHex8Color(calc(pct, pct), theme);
}
getAll = (vals, min, max) => valuesToFills(vals as number[], index as string[], min!, max!);
@@ -0,0 +1,155 @@
import { createTheme, FieldType, Field, ThresholdsMode, MappingType } from '@grafana/data';
import { FieldColorModeId } from '@grafana/schema/dist/esm/index.gen';
import { getEnumConfig } from './scatter';
describe('value mapping function', () => {
it('thresholds', () => {
const field: Field<number | null> = {
name: 'A',
type: FieldType.number,
values: [0, 10, 20, 30, 40, 50],
config: {
mappings: undefined,
thresholds: {
mode: ThresholdsMode.Absolute,
steps: [
{
value: -Infinity,
color: 'green',
},
{
value: 30,
color: 'red',
},
],
},
color: {
mode: FieldColorModeId.Thresholds,
},
},
};
const { index, getAll } = getEnumConfig(field, createTheme());
expect(index).toEqual({ color: ['#73bf69ff', '#f2495cff'], icon: ['', ''], text: ['', ''] });
expect(getAll(field.values)).toEqual([0, 0, 0, 1, 1, 1]);
});
it('mappings (with dedupe)', () => {
const field: Field<number | null> = {
name: 'A',
type: FieldType.number,
values: [5, 6, 7, 8, 9, 10, 11, 32, 40, null],
config: {
mappings: [
{
options: {
'21': {
color: '#fade2a',
index: 10,
text: 'Manual Stop',
},
'22': {
color: '#f2495c',
index: 9,
text: 'Instant Shutdown',
},
'23': {
color: '#ff9830',
index: 8,
text: 'Delayed Shutdown',
},
'30': {
color: '#5794f2',
index: 7,
text: 'Propel',
},
'31': {
color: '#ffa6b0',
index: 6,
text: 'Limits Mode',
},
'32': {
color: '#73bf69',
index: 5,
text: 'Production',
},
'33': {
color: '#ffcb7d',
index: 4,
text: 'Motivator Mode',
},
'40': {
color: '#73bf69',
index: 3,
text: 'Production',
},
null: {
color: '#808080',
index: 2,
text: 'N/A',
},
},
type: MappingType.ValueToText,
},
{
options: {
from: 41,
result: {
color: '#a352cc',
index: 0,
text: 'Maintenance Mode',
},
to: 45,
},
type: MappingType.RangeToText,
},
{
options: {
from: 5,
result: {
color: '#73bf69',
index: 1,
text: 'Production',
},
to: 11,
},
type: MappingType.RangeToText,
},
],
color: {
mode: FieldColorModeId.Fixed,
},
},
};
// this should merge states with equal text+color+icon
const { index, getAll } = getEnumConfig(field, createTheme());
expect(index).toEqual({
color: [
'#fade2aff',
'#f2495cff',
'#ff9830ff',
'#5794f2ff',
'#ffa6b0ff',
'#73bf69ff',
'#ffcb7dff',
'#808080ff',
'#a352ccff',
],
icon: ['', '', '', '', '', '', '', '', ''],
text: [
'Manual Stop',
'Instant Shutdown',
'Delayed Shutdown',
'Propel',
'Limits Mode',
'Production',
'Motivator Mode',
'N/A',
'Maintenance Mode',
],
});
expect(getAll(field.values)).toEqual([5, 5, 5, 5, 5, 5, 5, 5, 5, 7]);
});
});