mirror of
https://github.com/grafana/grafana.git
synced 2026-01-14 13:21:26 +00:00
Compare commits
6 Commits
sriram/SQL
...
leeoniya/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2723cdf08f | ||
|
|
9c946c41b1 | ||
|
|
34d6434785 | ||
|
|
70570389d1 | ||
|
|
df4ca9cab5 | ||
|
|
2815a5631a |
@@ -2,6 +2,7 @@ import tinycolor from 'tinycolor2';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import {
|
||||
EnumFieldConfig,
|
||||
FALLBACK_COLOR,
|
||||
Field,
|
||||
FieldType,
|
||||
@@ -433,7 +434,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()
|
||||
@@ -444,8 +449,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;
|
||||
@@ -555,7 +560,7 @@ function getHex8Color(color: string, theme: GrafanaTheme2) {
|
||||
}
|
||||
|
||||
interface FieldColorValues {
|
||||
index: unknown[];
|
||||
index: EnumFieldConfig;
|
||||
getOne: GetOneValue;
|
||||
getAll: GetAllValues;
|
||||
}
|
||||
@@ -566,9 +571,40 @@ interface FieldColorValuesWithCache extends FieldColorValues {
|
||||
type GetAllValues = (values: unknown[], min?: number, max?: number) => number[];
|
||||
type GetOneValue = (value: unknown, min?: number, max?: number) => number;
|
||||
|
||||
function getLabelForRange(from: number | null, to: number | null) {
|
||||
let text: string;
|
||||
|
||||
if (from != null) {
|
||||
if (to != null) {
|
||||
text = `${from} - ${to}`;
|
||||
} else {
|
||||
text = `≥ ${from}`;
|
||||
}
|
||||
} else {
|
||||
if (to != null) {
|
||||
text = `≤ ${to}`;
|
||||
} else {
|
||||
text = '';
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
// percent enum configs can be combined, percent threasholds are global across multi frames and fields
|
||||
// classic palette by value
|
||||
// get and getAll, should accept shared min/max to scale percentage
|
||||
// merge enums for legend by combo of color+icon+text
|
||||
// auto-threshold by % into 10 bukkits?
|
||||
|
||||
/** 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;
|
||||
|
||||
@@ -578,46 +614,72 @@ 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 { from, to } = m.options;
|
||||
text ??= getLabelForRange(from, to);
|
||||
|
||||
let range = [];
|
||||
|
||||
if (m.options.from != null) {
|
||||
range.push(`v >= ${Number(m.options.from)}`);
|
||||
if (from != null) {
|
||||
range.push(`v >= ${Number(from)}`);
|
||||
}
|
||||
|
||||
if (m.options.to != null) {
|
||||
range.push(`v <= ${Number(m.options.to)}`);
|
||||
if (to != null) {
|
||||
range.push(`v <= ${Number(to)}`);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (spl === SpecialValueMatch.NaN) {
|
||||
text ??= 'NaN';
|
||||
conds += `isNaN(v)`;
|
||||
} else if (spl === SpecialValueMatch.NullAndNaN) {
|
||||
text ??= 'null/NaN';
|
||||
conds += `v == null || isNaN(v)`;
|
||||
} else {
|
||||
conds += `v ${
|
||||
let cond =
|
||||
spl === SpecialValueMatch.True
|
||||
? '=== true'
|
||||
: spl === SpecialValueMatch.False
|
||||
@@ -626,12 +688,14 @@ function fieldValueColors(f: Field, theme: GrafanaTheme2): FieldColorValues {
|
||||
? '== null'
|
||||
: spl === SpecialValueMatch.Empty
|
||||
? '=== ""'
|
||||
: '== null'
|
||||
}`;
|
||||
: '== null';
|
||||
|
||||
conds += `v ${cond}`;
|
||||
text ??= cond.replace(/[= ]+/g, '');
|
||||
}
|
||||
|
||||
conds += ` ? ${index.length} : `;
|
||||
index.push(getHex8Color(color, theme));
|
||||
let idx = indexOf(color, text, icon);
|
||||
conds += ` ? ${idx} : `;
|
||||
} else if (m.type === MappingType.RegexToText) {
|
||||
// TODO
|
||||
}
|
||||
@@ -639,7 +703,7 @@ function fieldValueColors(f: Field, theme: GrafanaTheme2): FieldColorValues {
|
||||
}
|
||||
|
||||
conds += '-1'; // ?? what default here? null? FALLBACK_COLOR?
|
||||
} else if (f.config.color?.mode === FieldColorModeId.Thresholds) {
|
||||
} else if (f.config.color?.mode === FieldColorModeId.Thresholds && (f.config.thresholds?.steps.length ?? 0) > 1) {
|
||||
if (f.config.thresholds?.mode === ThresholdsMode.Absolute) {
|
||||
let steps = f.config.thresholds.steps;
|
||||
let lasti = steps.length - 1;
|
||||
@@ -651,18 +715,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 = steps.map((s, i) => (i === 0 ? `< ${steps[i + 1].value}` : getLabelForRange(s.value, null)));
|
||||
index.icon = Array(steps.length).fill('');
|
||||
} 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!);
|
||||
|
||||
155
public/app/plugins/panel/xychart/utils.test.ts
Normal file
155
public/app/plugins/panel/xychart/utils.test.ts
Normal file
@@ -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: ['< 30', '≥ 30'] });
|
||||
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