TimeSeries: Implement auto decimals for y axes (#52912)

This commit is contained in:
Leon Sorokin
2022-07-28 01:58:42 -05:00
committed by GitHub
parent 51e2a1c0a4
commit 8ee0555bac
15 changed files with 92 additions and 46 deletions

View File

@@ -1971,11 +1971,10 @@ exports[`better eslint`] = {
], ],
"packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts:5381": [ "packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"], [0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"], [0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"]
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
], ],
"packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts:5381": [ "packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "0"],
@@ -4592,12 +4591,13 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "7"], [0, 0, 0, "Unexpected any. Specify a different type.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"], [0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Unexpected any. Specify a different type.", "9"], [0, 0, 0, "Unexpected any. Specify a different type.", "9"],
[0, 0, 0, "Do not use any type assertions.", "10"], [0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Unexpected any. Specify a different type.", "11"], [0, 0, 0, "Do not use any type assertions.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"], [0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"], [0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"], [0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"] [0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"]
], ],
"public/app/features/dashboard/utils/panelMerge.test.ts:5381": [ "public/app/features/dashboard/utils/panelMerge.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@@ -7,7 +7,7 @@ import { toUtc, dateTimeParse } from '../datetime';
import { GrafanaTheme2 } from '../themes/types'; import { GrafanaTheme2 } from '../themes/types';
import { KeyValue, TimeZone } from '../types'; import { KeyValue, TimeZone } from '../types';
import { Field, FieldType } from '../types/dataFrame'; import { Field, FieldType } from '../types/dataFrame';
import { DisplayProcessor, DisplayValue } from '../types/displayValue'; import { DecimalCount, DisplayProcessor, DisplayValue } from '../types/displayValue';
import { anyToNumber } from '../utils/anyToNumber'; import { anyToNumber } from '../utils/anyToNumber';
import { getValueMappingResult } from '../utils/valueMappings'; import { getValueMappingResult } from '../utils/valueMappings';
import { getValueFormat, isBooleanUnit } from '../valueFormats/valueFormats'; import { getValueFormat, isBooleanUnit } from '../valueFormats/valueFormats';
@@ -75,7 +75,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
const formatFunc = getValueFormat(unit || 'none'); const formatFunc = getValueFormat(unit || 'none');
const scaleFunc = getScaleCalculator(field, options.theme); const scaleFunc = getScaleCalculator(field, options.theme);
return (value: any) => { return (value: any, decimals?: DecimalCount) => {
const { mappings } = config; const { mappings } = config;
const isStringUnit = unit === 'string'; const isStringUnit = unit === 'string';
@@ -111,7 +111,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
if (!isNaN(numeric)) { if (!isNaN(numeric)) {
if (text == null && !isBoolean(value)) { if (text == null && !isBoolean(value)) {
const v = formatFunc(numeric, config.decimals, null, options.timeZone, showMs); const v = formatFunc(numeric, decimals ?? config.decimals, null, options.timeZone, showMs);
text = v.text; text = v.text;
suffix = v.suffix; suffix = v.suffix;
prefix = v.prefix; prefix = v.prefix;

View File

@@ -8,6 +8,7 @@ import {
ApplyFieldOverrideOptions, ApplyFieldOverrideOptions,
DataFrame, DataFrame,
DataLink, DataLink,
DecimalCount,
DisplayProcessor, DisplayProcessor,
DisplayValue, DisplayValue,
DynamicConfigValue, DynamicConfigValue,
@@ -213,9 +214,18 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra
// 2. have the ability to selectively get display color or text (but not always both, which are each quite expensive) // 2. have the ability to selectively get display color or text (but not always both, which are each quite expensive)
// 3. sufficently optimize text formatting and threshold color determinitation // 3. sufficently optimize text formatting and threshold color determinitation
function cachingDisplayProcessor(disp: DisplayProcessor, maxCacheSize = 2500): DisplayProcessor { function cachingDisplayProcessor(disp: DisplayProcessor, maxCacheSize = 2500): DisplayProcessor {
const cache = new Map<any, DisplayValue>(); type dispCache = Map<any, DisplayValue>;
// decimals -> cache mapping, -1 is unspecified decimals
const caches = new Map<number, dispCache>();
// pre-init caches for up to 15 decimals
for (let i = -1; i <= 15; i++) {
caches.set(i, new Map());
}
return (value: any, decimals?: DecimalCount) => {
let cache = caches.get(decimals ?? -1)!;
return (value: any) => {
let v = cache.get(value); let v = cache.get(value);
if (!v) { if (!v) {
@@ -224,7 +234,7 @@ function cachingDisplayProcessor(disp: DisplayProcessor, maxCacheSize = 2500): D
cache.clear(); cache.clear();
} }
v = disp(value); v = disp(value, decimals);
// convert to hex6 or hex8 so downstream we can cheaply test for alpha (and set new alpha) // convert to hex6 or hex8 so downstream we can cheaply test for alpha (and set new alpha)
// via a simple length check (in colorManipulator) rather using slow parsing via tinycolor // via a simple length check (in colorManipulator) rather using slow parsing via tinycolor

View File

@@ -1,7 +1,7 @@
import { ScopedVars } from './ScopedVars'; import { ScopedVars } from './ScopedVars';
import { QueryResultBase, Labels, NullValueMode } from './data'; import { QueryResultBase, Labels, NullValueMode } from './data';
import { DataLink, LinkModel } from './dataLink'; import { DataLink, LinkModel } from './dataLink';
import { DisplayProcessor, DisplayValue } from './displayValue'; import { DecimalCount, DisplayProcessor, DisplayValue } from './displayValue';
import { FieldColor } from './fieldColor'; import { FieldColor } from './fieldColor';
import { ThresholdsConfig } from './thresholds'; import { ThresholdsConfig } from './thresholds';
import { ValueMapping } from './valueMapping'; import { ValueMapping } from './valueMapping';
@@ -63,7 +63,7 @@ export interface FieldConfig<TOptions = any> {
// Numeric Options // Numeric Options
unit?: string; unit?: string;
decimals?: number | null; // Significant digits (for display) decimals?: DecimalCount; // Significant digits (for display)
min?: number | null; min?: number | null;
max?: number | null; max?: number | null;

View File

@@ -1,6 +1,6 @@
import { FormattedValue } from '../valueFormats'; import { FormattedValue } from '../valueFormats';
export type DisplayProcessor = (value: any) => DisplayValue; export type DisplayProcessor = (value: any, decimals?: DecimalCount) => DisplayValue;
export interface DisplayValue extends FormattedValue { export interface DisplayValue extends FormattedValue {
/** /**

View File

@@ -6,7 +6,7 @@ export * from './deprecationWarning';
export * from './csv'; export * from './csv';
export * from './logs'; export * from './logs';
export * from './labels'; export * from './labels';
export * from './labels'; export * from './numbers';
export * from './object'; export * from './object';
export * from './namedColorsPalette'; export * from './namedColorsPalette';
export * from './series'; export * from './series';

View File

@@ -0,0 +1,29 @@
/**
* Round half away from zero ('commercial' rounding)
* Uses correction to offset floating-point inaccuracies.
* Works symmetrically for positive and negative numbers.
*
* ref: https://stackoverflow.com/a/48764436
*/
export function roundDecimals(val: number, dec = 0) {
if (Number.isInteger(val)) {
return val;
}
let p = 10 ** dec;
let n = val * p * (1 + Number.EPSILON);
return Math.round(n) / p;
}
/**
* Tries to guess number of decimals needed to format a number
*
* used for determining minimum decimals required to uniformly
* format a numric sequence, e.g. 10, 10.125, 10.25, 10.5
*
* good for precisce increments: 0.125 -> 3
* bad for arbitrary floats: 371.499999999999 -> 12
*/
export function guessDecimals(num: number) {
return (('' + num).split('.')[1] || '').length;
}

View File

@@ -15,6 +15,7 @@ import {
getFieldDisplayName, getFieldDisplayName,
getDisplayProcessor, getDisplayProcessor,
FieldColorModeId, FieldColorModeId,
DecimalCount,
} from '@grafana/data'; } from '@grafana/data';
import { import {
AxisPlacement, AxisPlacement,
@@ -35,7 +36,7 @@ import { UPlotConfigBuilder, UPlotConfigPrepFn } from '../uPlot/config/UPlotConf
import { getScaleGradientFn } from '../uPlot/config/gradientFills'; import { getScaleGradientFn } from '../uPlot/config/gradientFills';
import { getStackingGroups, preparePlotData2 } from '../uPlot/utils'; import { getStackingGroups, preparePlotData2 } from '../uPlot/utils';
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1)); const defaultFormatter = (v: any, decimals: DecimalCount = 1) => (v == null ? '-' : v.toFixed(decimals));
const defaultConfig: GraphFieldConfig = { const defaultConfig: GraphFieldConfig = {
drawStyle: GraphDrawStyle.Line, drawStyle: GraphDrawStyle.Line,
@@ -268,7 +269,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
label: customConfig.axisLabel, label: customConfig.axisLabel,
size: customConfig.axisWidth, size: customConfig.axisWidth,
placement: customConfig.axisPlacement ?? AxisPlacement.Auto, placement: customConfig.axisPlacement ?? AxisPlacement.Auto,
formatValue: (v) => formattedValueToString(fmt(v)), formatValue: (v, decimals) => formattedValueToString(fmt(v, config.decimals ?? decimals)),
theme, theme,
grid: { show: customConfig.axisGridShow }, grid: { show: customConfig.axisGridShow },
show: customConfig.hideFrom?.viz === false, show: customConfig.hideFrom?.viz === false,

View File

@@ -1,6 +1,15 @@
import uPlot, { Axis } from 'uplot'; import uPlot, { Axis } from 'uplot';
import { dateTimeFormat, GrafanaTheme2, isBooleanUnit, systemDateFormats, TimeZone } from '@grafana/data'; import {
dateTimeFormat,
DecimalCount,
GrafanaTheme2,
guessDecimals,
isBooleanUnit,
roundDecimals,
systemDateFormats,
TimeZone,
} from '@grafana/data';
import { AxisPlacement } from '@grafana/schema'; import { AxisPlacement } from '@grafana/schema';
import { measureText } from '../../../utils/measureText'; import { measureText } from '../../../utils/measureText';
@@ -21,7 +30,7 @@ export interface AxisProps {
ticks?: Axis.Ticks; ticks?: Axis.Ticks;
filter?: Axis.Filter; filter?: Axis.Filter;
space?: Axis.Space; space?: Axis.Space;
formatValue?: (v: any) => string; formatValue?: (v: any, decimals?: DecimalCount) => string;
incrs?: Axis.Incrs; incrs?: Axis.Incrs;
splits?: Axis.Splits; splits?: Axis.Splits;
values?: Axis.Values; values?: Axis.Values;
@@ -176,7 +185,10 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
} else if (isTime) { } else if (isTime) {
config.values = formatTime; config.values = formatTime;
} else if (formatValue) { } else if (formatValue) {
config.values = (u: uPlot, vals: any[]) => vals.map(formatValue!); config.values = (u: uPlot, splits, axisIdx, tickSpace, tickIncr) => {
let decimals = guessDecimals(roundDecimals(tickIncr, 6));
return splits.map((v) => formatValue!(v, decimals > 0 ? decimals : undefined));
};
} }
// store timezone // store timezone

View File

@@ -1,28 +1,22 @@
const { abs, round, pow } = Math; import { guessDecimals, roundDecimals } from '@grafana/data';
export function roundDec(val: number, dec: number) { const { abs, pow } = Math;
return round(val * (dec = 10 ** dec)) / dec;
}
export const fixedDec = new Map(); export const fixedDec = new Map();
export function guessDec(num: number) {
return (('' + num).split('.')[1] || '').length;
}
export function genIncrs(base: number, minExp: number, maxExp: number, mults: number[]) { export function genIncrs(base: number, minExp: number, maxExp: number, mults: number[]) {
let incrs = []; let incrs = [];
let multDec = mults.map(guessDec); let multDec = mults.map(guessDecimals);
for (let exp = minExp; exp < maxExp; exp++) { for (let exp = minExp; exp < maxExp; exp++) {
let expa = abs(exp); let expa = abs(exp);
let mag = roundDec(pow(base, exp), expa); let mag = roundDecimals(pow(base, exp), expa);
for (let i = 0; i < mults.length; i++) { for (let i = 0; i < mults.length; i++) {
let _incr = mults[i] * mag; let _incr = mults[i] * mag;
let dec = (_incr >= 0 && exp >= 0 ? 0 : expa) + (exp >= multDec[i] ? 0 : multDec[i]); let dec = (_incr >= 0 && exp >= 0 ? 0 : expa) + (exp >= multDec[i] ? 0 : multDec[i]);
let incr = roundDec(_incr, dec); let incr = roundDecimals(_incr, dec);
incrs.push(incr); incrs.push(incr);
fixedDec.set(incr, dec); fixedDec.set(incr, dec);
} }

View File

@@ -1,12 +1,10 @@
function roundDec(val: number, dec: number) { import { roundDecimals } from '@grafana/data';
return Math.round(val * (dec = 10 ** dec)) / dec;
}
export const SPACE_BETWEEN = 1; export const SPACE_BETWEEN = 1;
export const SPACE_AROUND = 2; export const SPACE_AROUND = 2;
export const SPACE_EVENLY = 3; export const SPACE_EVENLY = 3;
const coord = (i: number, offs: number, iwid: number, gap: number) => roundDec(offs + i * (iwid + gap), 6); const coord = (i: number, offs: number, iwid: number, gap: number) => roundDecimals(offs + i * (iwid + gap), 6);
export type Each = (idx: number, offPct: number, dimPct: number) => void; export type Each = (idx: number, offPct: number, dimPct: number) => void;
@@ -37,7 +35,7 @@ export function distribute(numItems: number, sizeFactor: number, justify: number
/* eslint-enable */ /* eslint-enable */
let iwid = sizeFactor / numItems; let iwid = sizeFactor / numItems;
let _iwid = roundDec(iwid, 6); let _iwid = roundDecimals(iwid, 6);
if (onlyIdx == null) { if (onlyIdx == null) {
for (let i = 0; i < numItems; i++) { for (let i = 0; i < numItems; i++) {

View File

@@ -246,7 +246,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptionsEX> = ({
label: customConfig.axisLabel, label: customConfig.axisLabel,
size: customConfig.axisWidth, size: customConfig.axisWidth,
placement, placement,
formatValue: (v) => formattedValueToString(field.display!(v)), formatValue: (v, decimals) => formattedValueToString(field.display!(v, field.config.decimals ?? decimals)),
theme, theme,
grid: { show: customConfig.axisGridShow }, grid: { show: customConfig.axisGridShow },
}); });

View File

@@ -19,6 +19,7 @@ import {
DataHoverClearEvent, DataHoverClearEvent,
DataHoverEvent, DataHoverEvent,
DataHoverPayload, DataHoverPayload,
DecimalCount,
FieldDisplay, FieldDisplay,
FieldType, FieldType,
formattedValueToString, formattedValueToString,
@@ -945,11 +946,7 @@ class GraphElement {
return ticks; return ticks;
} }
configureAxisMode( configureAxisMode(axis: { tickFormatter: (val: any, axis: any) => string }, format: string, decimals?: DecimalCount) {
axis: { tickFormatter: (val: any, axis: any) => string },
format: string,
decimals?: number | null
) {
axis.tickFormatter = (val, axis) => { axis.tickFormatter = (val, axis) => {
const formatter = getValueFormat(format); const formatter = getValueFormat(format);

View File

@@ -407,7 +407,7 @@ export function prepConfig(opts: PrepConfigOpts) {
size: yAxisConfig.axisWidth || null, size: yAxisConfig.axisWidth || null,
label: yAxisConfig.axisLabel, label: yAxisConfig.axisLabel,
theme: theme, theme: theme,
formatValue: (v: number) => formattedValueToString(dispY(v)), formatValue: (v, decimals) => formattedValueToString(dispY(v, yField.config.decimals ?? decimals)),
splits: isOrdianalY splits: isOrdianalY
? (self: uPlot) => { ? (self: uPlot) => {
const meta = readHeatmapRowsCustomMeta(dataRef.current?.heatmap); const meta = readHeatmapRowsCustomMeta(dataRef.current?.heatmap);

View File

@@ -144,10 +144,15 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => {
theme, theme,
}); });
// assumes BucketMax is [1]
let countField = frame.fields[2];
let dispY = countField.display;
builder.addAxis({ builder.addAxis({
scaleKey: 'y', scaleKey: 'y',
isTime: false, isTime: false,
placement: AxisPlacement.Left, placement: AxisPlacement.Left,
formatValue: (v, decimals) => formattedValueToString(dispY!(v, countField.config.decimals ?? decimals)),
//splits: config.xSplits, //splits: config.xSplits,
//values: config.xValues, //values: config.xValues,
//grid: false, //grid: false,