StateTimeline: Display false and empty string values (#107059)
* Support boolean false value mappings * Support empty string value mapping
This commit is contained in:
committed by
GitHub
parent
53cb80e58c
commit
dcfb079856
@@ -986,6 +986,101 @@
|
||||
],
|
||||
"title": "special null + NaN value mapping from data",
|
||||
"type": "state-timeline"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 70,
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineWidth": 0,
|
||||
"spanNulls": false
|
||||
},
|
||||
"fieldMinMax": false,
|
||||
"mappings": [
|
||||
{
|
||||
"options": {
|
||||
"match": "false",
|
||||
"result": {
|
||||
"color": "red",
|
||||
"index": 0
|
||||
}
|
||||
},
|
||||
"type": "special"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"match": "null+nan",
|
||||
"result": {
|
||||
"color": "blue",
|
||||
"index": 1,
|
||||
"text": "null + NaN"
|
||||
}
|
||||
},
|
||||
"type": "special"
|
||||
}
|
||||
],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 6,
|
||||
"x": 12,
|
||||
"y": 18
|
||||
},
|
||||
"id": 14,
|
||||
"options": {
|
||||
"alignValue": "center",
|
||||
"legend": {
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"mergeValues": true,
|
||||
"rowHeight": 0.9,
|
||||
"showValue": "auto",
|
||||
"tooltip": {
|
||||
"hideZeros": false,
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "12.1.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"rawFrameContent": "[\n {\n \"schema\": {\n \"refId\": \"A\",\n \"fields\": [\n {\n \"name\": \"time\",\n \"type\": \"time\",\n \"typeInfo\": {\n \"frame\": \"time\",\n \"nullable\": true\n },\n \"config\": {}\n },\n {\n \"name\": \"value\",\n \"type\": \"number\",\n \"typeInfo\": {\n \"frame\": \"int64\",\n \"nullable\": true\n },\n \"config\": {\n \"thresholds\": {\n \"mode\": \"absolute\",\n \"steps\": [\n {\n \"color\": \"green\",\n \"value\": null\n }\n ]\n }\n }\n }\n ]\n },\n \"data\": {\n \"values\": [\n [\n 1674732835000,\n 1674736435000,\n 1674740035000,\n 1674743635000,\n 1674747235000,\n 1674750835000,\n 1674754235000,\n 1674757835000\n ],\n [\n null,\n null,\n false,\n true,\n true,\n false,\n true,\n null\n ]\n ],\n \"entities\": [null, { \"NaN\": [0], \"Undefined\": [1]}]\n }\n }\n]",
|
||||
"refId": "A",
|
||||
"scenarioId": "raw_frame"
|
||||
}
|
||||
],
|
||||
"title": "boolean values from data",
|
||||
"type": "state-timeline"
|
||||
}
|
||||
],
|
||||
"preload": false,
|
||||
@@ -1008,5 +1103,5 @@
|
||||
"timezone": "utc",
|
||||
"title": "StateTimeline - Thresholds & Mappings",
|
||||
"uid": "Kce7z9TVz",
|
||||
"version": 13
|
||||
}
|
||||
"version": 18
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import uPlot from 'uplot';
|
||||
import { getDefaultTimeRange, createTheme } from '@grafana/data';
|
||||
import { VisibilityMode } from '@grafana/schema';
|
||||
|
||||
import { getConfig, TimelineCoreOptions } from './timeline';
|
||||
import { getConfig, TimelineCoreOptions, shouldDrawYValue } from './timeline';
|
||||
import { TimelineMode } from './utils';
|
||||
|
||||
jest.mock('uplot');
|
||||
@@ -208,4 +208,80 @@ describe('StateTimeline uPlot integration', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#shouldDrawYValue', () => {
|
||||
describe.each([
|
||||
[true, undefined, undefined, true, 'boolean true returns true'],
|
||||
[false, undefined, undefined, true, 'boolean false returns true'],
|
||||
])('boolean values', (yValue, mappedNull, mappedNaN, expected, testName) => {
|
||||
it(testName, () => {
|
||||
expect(shouldDrawYValue(yValue, mappedNull, mappedNaN)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[0, undefined, undefined, true, 'zero returns true'],
|
||||
[1, undefined, undefined, true, 'positive integer returns true'],
|
||||
[-2.71, undefined, undefined, true, 'negative float returns true'],
|
||||
[Number.MAX_VALUE, undefined, undefined, true, 'max value returns true'],
|
||||
[Number.MIN_VALUE, undefined, undefined, true, 'min value returns true'],
|
||||
])('finite numeric values', (yValue, mappedNull, mappedNaN, expected, testName) => {
|
||||
it(testName, () => {
|
||||
expect(shouldDrawYValue(yValue, mappedNull, mappedNaN)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[Infinity, undefined, undefined, true, 'positive infinity returns true'],
|
||||
[-Infinity, undefined, undefined, true, 'negative infinity returns true'],
|
||||
[Number.POSITIVE_INFINITY, undefined, undefined, true, 'Number.POSITIVE_INFINITY returns true'],
|
||||
[Number.NEGATIVE_INFINITY, undefined, undefined, true, 'Number.NEGATIVE_INFINITY returns true'],
|
||||
])('non-finite numeric values', (yValue, mappedNull, mappedNaN, expected, testName) => {
|
||||
it(testName, () => {
|
||||
expect(shouldDrawYValue(yValue, mappedNull, mappedNaN)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[null, undefined, undefined, false, 'null without mappings returns false'],
|
||||
[null, false, true, false, 'null with false mappedNull returns false'],
|
||||
[null, true, undefined, true, 'null with ture mappedNull returns true'],
|
||||
])('null values', (yValue, mappedNull, mappedNaN, expected, testName) => {
|
||||
it(testName, () => {
|
||||
expect(shouldDrawYValue(yValue, mappedNull, mappedNaN)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
[NaN, undefined, undefined, false, 'NaN without mappings returns false'],
|
||||
[NaN, true, undefined, false, 'NaN with false mappedNaN returns false'],
|
||||
[NaN, undefined, true, true, 'NaN with true mappedNaN returns true'],
|
||||
])('NaN values', (yValue, mappedNull, mappedNaN, expected, testName) => {
|
||||
it(testName, () => {
|
||||
expect(shouldDrawYValue(yValue, mappedNull, mappedNaN)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([
|
||||
['', undefined, undefined, true, 'empty string returns true'],
|
||||
['to be or not to be', undefined, undefined, true, 'non-empty string returns true'],
|
||||
])('string values', (yValue, mappedNull, mappedNaN, expected, testName) => {
|
||||
it(testName, () => {
|
||||
expect(shouldDrawYValue(yValue, mappedNull, mappedNaN)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each([[undefined, undefined, undefined, false, 'undefined returns false']])(
|
||||
'falsy values',
|
||||
(yValue, mappedNull, mappedNaN, expected, testName) => {
|
||||
it(testName, () => {
|
||||
expect(shouldDrawYValue(yValue, mappedNull, mappedNaN)).toBe(expected);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
describe('non-supported values', () => {
|
||||
it.todo(`TODO: this helper currently returns true for many non-supported truthy values, but should not`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,6 +56,28 @@ export interface TimelineCoreOptions {
|
||||
hoverMulti: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function shouldDrawYValue(yValue: unknown, mappedNull?: boolean, mappedNaN?: boolean): boolean {
|
||||
if (typeof yValue === 'boolean') {
|
||||
return true;
|
||||
}
|
||||
if (typeof yValue === 'string') {
|
||||
return true;
|
||||
}
|
||||
if (typeof yValue === 'number' && !Number.isNaN(yValue)) {
|
||||
return true;
|
||||
}
|
||||
if (yValue === null && mappedNull) {
|
||||
return true;
|
||||
}
|
||||
if (Number.isNaN(yValue) && mappedNaN) {
|
||||
return true;
|
||||
}
|
||||
return !!yValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@@ -210,9 +232,7 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
if (mode === TimelineMode.Changes) {
|
||||
for (let ix = 0; ix < dataY.length; ix++) {
|
||||
let yVal = dataY[ix];
|
||||
|
||||
const shouldDrawY =
|
||||
!!yVal || yVal === 0 || (yVal === null && mappedNull) || (Number.isNaN(yVal) && mappedNaN);
|
||||
const shouldDrawY = shouldDrawYValue(yVal, mappedNull, mappedNaN);
|
||||
|
||||
if (shouldDrawY) {
|
||||
let left = Math.round(valToPosX(dataX[ix], scaleX, xDim, xOff));
|
||||
@@ -257,8 +277,7 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
|
||||
for (let ix = idx0; ix <= idx1; ix++) {
|
||||
let yVal = dataY[ix];
|
||||
const shouldDrawY =
|
||||
!!yVal || yVal === 0 || (yVal === null && mappedNull) || (Number.isNaN(yVal) && mappedNaN);
|
||||
const shouldDrawY = shouldDrawYValue(yVal, mappedNull, mappedNaN);
|
||||
|
||||
if (shouldDrawY) {
|
||||
// TODO: all xPos can be pre-computed once for all series in aligned set
|
||||
@@ -321,8 +340,7 @@ export function getConfig(opts: TimelineCoreOptions) {
|
||||
|
||||
for (let ix = 0; ix < dataY.length; ix++) {
|
||||
const yVal = dataY[ix];
|
||||
const shouldDrawY =
|
||||
!!yVal || yVal === 0 || (yVal == null && mappedNull) || (Number.isNaN(yVal) && mappedNaN);
|
||||
const shouldDrawY = shouldDrawYValue(yVal, mappedNull, mappedNaN);
|
||||
|
||||
if (shouldDrawY) {
|
||||
const boxRect = boxRectsBySeries[sidx - 1][ix];
|
||||
|
||||
Reference in New Issue
Block a user