diff --git a/.betterer.results b/.betterer.results index 861464310c9..459e6e95fc4 100644 --- a/.betterer.results +++ b/.betterer.results @@ -1971,11 +1971,10 @@ exports[`better eslint`] = { ], "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.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Unexpected any. Specify a different type.", "3"], - [0, 0, 0, "Do not use any type assertions.", "4"], - [0, 0, 0, "Unexpected any. Specify a different type.", "5"] + [0, 0, 0, "Do not use any type assertions.", "1"], + [0, 0, 0, "Unexpected any. Specify a different type.", "2"], + [0, 0, 0, "Do not use any type assertions.", "3"], + [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], "packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts:5381": [ [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.", "8"], [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.", "11"], + [0, 0, 0, "Unexpected any. Specify a different type.", "10"], + [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.", "13"], [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": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/packages/grafana-data/src/field/displayProcessor.ts b/packages/grafana-data/src/field/displayProcessor.ts index cce5e73b24d..99a751504da 100644 --- a/packages/grafana-data/src/field/displayProcessor.ts +++ b/packages/grafana-data/src/field/displayProcessor.ts @@ -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; diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index e656aeea158..5ca3c92becb 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -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(); + type dispCache = Map; + // decimals -> cache mapping, -1 is unspecified decimals + const caches = new Map(); + + // 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 diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index 75d7482a455..1dba22854dc 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -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 { // Numeric Options unit?: string; - decimals?: number | null; // Significant digits (for display) + decimals?: DecimalCount; // Significant digits (for display) min?: number | null; max?: number | null; diff --git a/packages/grafana-data/src/types/displayValue.ts b/packages/grafana-data/src/types/displayValue.ts index 2effad55d78..65cf2230617 100644 --- a/packages/grafana-data/src/types/displayValue.ts +++ b/packages/grafana-data/src/types/displayValue.ts @@ -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 { /** diff --git a/packages/grafana-data/src/utils/index.ts b/packages/grafana-data/src/utils/index.ts index 6df8d8f21b7..036916fde6c 100644 --- a/packages/grafana-data/src/utils/index.ts +++ b/packages/grafana-data/src/utils/index.ts @@ -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'; diff --git a/packages/grafana-data/src/utils/numbers.ts b/packages/grafana-data/src/utils/numbers.ts new file mode 100644 index 00000000000..ed42d37e220 --- /dev/null +++ b/packages/grafana-data/src/utils/numbers.ts @@ -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; +} diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.ts b/packages/grafana-ui/src/components/TimeSeries/utils.ts index 51703830a03..af621e30e6c 100644 --- a/packages/grafana-ui/src/components/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/components/TimeSeries/utils.ts @@ -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, diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts index 05a924173d3..bb024fa18fb 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts @@ -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 { } 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 diff --git a/public/app/features/transformers/calculateHeatmap/utils.ts b/public/app/features/transformers/calculateHeatmap/utils.ts index 3851395f197..07a457d2361 100644 --- a/public/app/features/transformers/calculateHeatmap/utils.ts +++ b/public/app/features/transformers/calculateHeatmap/utils.ts @@ -1,28 +1,22 @@ -const { abs, round, pow } = Math; +import { guessDecimals, roundDecimals } from '@grafana/data'; -export function roundDec(val: number, dec: number) { - return round(val * (dec = 10 ** dec)) / dec; -} +const { abs, pow } = Math; 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[]) { let incrs = []; - let multDec = mults.map(guessDec); + let multDec = mults.map(guessDecimals); for (let exp = minExp; exp < maxExp; 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++) { let _incr = mults[i] * mag; 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); fixedDec.set(incr, dec); } diff --git a/public/app/plugins/panel/barchart/distribute.ts b/public/app/plugins/panel/barchart/distribute.ts index 6a4b419b813..2eddf47fcce 100644 --- a/public/app/plugins/panel/barchart/distribute.ts +++ b/public/app/plugins/panel/barchart/distribute.ts @@ -1,12 +1,10 @@ -function roundDec(val: number, dec: number) { - return Math.round(val * (dec = 10 ** dec)) / dec; -} +import { roundDecimals } from '@grafana/data'; export const SPACE_BETWEEN = 1; export const SPACE_AROUND = 2; 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; @@ -37,7 +35,7 @@ export function distribute(numItems: number, sizeFactor: number, justify: number /* eslint-enable */ let iwid = sizeFactor / numItems; - let _iwid = roundDec(iwid, 6); + let _iwid = roundDecimals(iwid, 6); if (onlyIdx == null) { for (let i = 0; i < numItems; i++) { diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index 9d24655bf16..1f12e968a8f 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -246,7 +246,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ label: customConfig.axisLabel, size: customConfig.axisWidth, placement, - formatValue: (v) => formattedValueToString(field.display!(v)), + formatValue: (v, decimals) => formattedValueToString(field.display!(v, field.config.decimals ?? decimals)), theme, grid: { show: customConfig.axisGridShow }, }); diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 26281d8e9f9..d3c63d37d82 100644 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -19,6 +19,7 @@ import { DataHoverClearEvent, DataHoverEvent, DataHoverPayload, + DecimalCount, FieldDisplay, FieldType, formattedValueToString, @@ -945,11 +946,7 @@ class GraphElement { return ticks; } - configureAxisMode( - axis: { tickFormatter: (val: any, axis: any) => string }, - format: string, - decimals?: number | null - ) { + configureAxisMode(axis: { tickFormatter: (val: any, axis: any) => string }, format: string, decimals?: DecimalCount) { axis.tickFormatter = (val, axis) => { const formatter = getValueFormat(format); diff --git a/public/app/plugins/panel/heatmap/utils.ts b/public/app/plugins/panel/heatmap/utils.ts index e181ccf8ebe..969833b8e39 100644 --- a/public/app/plugins/panel/heatmap/utils.ts +++ b/public/app/plugins/panel/heatmap/utils.ts @@ -407,7 +407,7 @@ export function prepConfig(opts: PrepConfigOpts) { size: yAxisConfig.axisWidth || null, label: yAxisConfig.axisLabel, theme: theme, - formatValue: (v: number) => formattedValueToString(dispY(v)), + formatValue: (v, decimals) => formattedValueToString(dispY(v, yField.config.decimals ?? decimals)), splits: isOrdianalY ? (self: uPlot) => { const meta = readHeatmapRowsCustomMeta(dataRef.current?.heatmap); diff --git a/public/app/plugins/panel/histogram/Histogram.tsx b/public/app/plugins/panel/histogram/Histogram.tsx index 6c8bc260ee1..f766351680f 100644 --- a/public/app/plugins/panel/histogram/Histogram.tsx +++ b/public/app/plugins/panel/histogram/Histogram.tsx @@ -144,10 +144,15 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { theme, }); + // assumes BucketMax is [1] + let countField = frame.fields[2]; + let dispY = countField.display; + builder.addAxis({ scaleKey: 'y', isTime: false, placement: AxisPlacement.Left, + formatValue: (v, decimals) => formattedValueToString(dispY!(v, countField.config.decimals ?? decimals)), //splits: config.xSplits, //values: config.xValues, //grid: false,