make xychart color compiler handle text and icon, and de-duplicate states
This commit is contained in:
@@ -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]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user