TimeSeries: Implement auto decimals for y axes (#52912)
This commit is contained in:
@@ -7,7 +7,7 @@ import { toUtc, dateTimeParse } from '../datetime';
|
||||
import { GrafanaTheme2 } from '../themes/types';
|
||||
import { KeyValue, TimeZone } from '../types';
|
||||
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 { getValueMappingResult } from '../utils/valueMappings';
|
||||
import { getValueFormat, isBooleanUnit } from '../valueFormats/valueFormats';
|
||||
@@ -75,7 +75,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
|
||||
const formatFunc = getValueFormat(unit || 'none');
|
||||
const scaleFunc = getScaleCalculator(field, options.theme);
|
||||
|
||||
return (value: any) => {
|
||||
return (value: any, decimals?: DecimalCount) => {
|
||||
const { mappings } = config;
|
||||
const isStringUnit = unit === 'string';
|
||||
|
||||
@@ -111,7 +111,7 @@ export function getDisplayProcessor(options?: DisplayProcessorOptions): DisplayP
|
||||
|
||||
if (!isNaN(numeric)) {
|
||||
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;
|
||||
suffix = v.suffix;
|
||||
prefix = v.prefix;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ApplyFieldOverrideOptions,
|
||||
DataFrame,
|
||||
DataLink,
|
||||
DecimalCount,
|
||||
DisplayProcessor,
|
||||
DisplayValue,
|
||||
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)
|
||||
// 3. sufficently optimize text formatting and threshold color determinitation
|
||||
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);
|
||||
|
||||
if (!v) {
|
||||
@@ -224,7 +234,7 @@ function cachingDisplayProcessor(disp: DisplayProcessor, maxCacheSize = 2500): D
|
||||
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)
|
||||
// via a simple length check (in colorManipulator) rather using slow parsing via tinycolor
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ScopedVars } from './ScopedVars';
|
||||
import { QueryResultBase, Labels, NullValueMode } from './data';
|
||||
import { DataLink, LinkModel } from './dataLink';
|
||||
import { DisplayProcessor, DisplayValue } from './displayValue';
|
||||
import { DecimalCount, DisplayProcessor, DisplayValue } from './displayValue';
|
||||
import { FieldColor } from './fieldColor';
|
||||
import { ThresholdsConfig } from './thresholds';
|
||||
import { ValueMapping } from './valueMapping';
|
||||
@@ -63,7 +63,7 @@ export interface FieldConfig<TOptions = any> {
|
||||
|
||||
// Numeric Options
|
||||
unit?: string;
|
||||
decimals?: number | null; // Significant digits (for display)
|
||||
decimals?: DecimalCount; // Significant digits (for display)
|
||||
min?: number | null;
|
||||
max?: number | null;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FormattedValue } from '../valueFormats';
|
||||
|
||||
export type DisplayProcessor = (value: any) => DisplayValue;
|
||||
export type DisplayProcessor = (value: any, decimals?: DecimalCount) => DisplayValue;
|
||||
|
||||
export interface DisplayValue extends FormattedValue {
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@ export * from './deprecationWarning';
|
||||
export * from './csv';
|
||||
export * from './logs';
|
||||
export * from './labels';
|
||||
export * from './labels';
|
||||
export * from './numbers';
|
||||
export * from './object';
|
||||
export * from './namedColorsPalette';
|
||||
export * from './series';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
getFieldDisplayName,
|
||||
getDisplayProcessor,
|
||||
FieldColorModeId,
|
||||
DecimalCount,
|
||||
} from '@grafana/data';
|
||||
import {
|
||||
AxisPlacement,
|
||||
@@ -35,7 +36,7 @@ import { UPlotConfigBuilder, UPlotConfigPrepFn } from '../uPlot/config/UPlotConf
|
||||
import { getScaleGradientFn } from '../uPlot/config/gradientFills';
|
||||
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 = {
|
||||
drawStyle: GraphDrawStyle.Line,
|
||||
@@ -268,7 +269,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{
|
||||
label: customConfig.axisLabel,
|
||||
size: customConfig.axisWidth,
|
||||
placement: customConfig.axisPlacement ?? AxisPlacement.Auto,
|
||||
formatValue: (v) => formattedValueToString(fmt(v)),
|
||||
formatValue: (v, decimals) => formattedValueToString(fmt(v, config.decimals ?? decimals)),
|
||||
theme,
|
||||
grid: { show: customConfig.axisGridShow },
|
||||
show: customConfig.hideFrom?.viz === false,
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
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 { measureText } from '../../../utils/measureText';
|
||||
@@ -21,7 +30,7 @@ export interface AxisProps {
|
||||
ticks?: Axis.Ticks;
|
||||
filter?: Axis.Filter;
|
||||
space?: Axis.Space;
|
||||
formatValue?: (v: any) => string;
|
||||
formatValue?: (v: any, decimals?: DecimalCount) => string;
|
||||
incrs?: Axis.Incrs;
|
||||
splits?: Axis.Splits;
|
||||
values?: Axis.Values;
|
||||
@@ -176,7 +185,10 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
||||
} else if (isTime) {
|
||||
config.values = formatTime;
|
||||
} 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
|
||||
|
||||
Reference in New Issue
Block a user