From 43d3d9756285caec2621707471f7e08a86f6771e Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Thu, 3 Jun 2021 20:05:47 -0500 Subject: [PATCH] Histogram: enable client-side zoom (with bucket snapping) (#35220) --- .../uPlot/config/UPlotConfigBuilder.ts | 46 +++++++++---------- .../app/plugins/panel/histogram/Histogram.tsx | 35 +++++++++++++- 2 files changed, 55 insertions(+), 26 deletions(-) diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index 0c9f705c301..6672233641e 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -1,5 +1,5 @@ import uPlot, { Cursor, Band, Hooks, Select } from 'uplot'; -import { defaultsDeep } from 'lodash'; +import { merge } from 'lodash'; import { DataFrame, DefaultTimeZone, @@ -17,6 +17,24 @@ import { AxisPlacement } from '../config'; import { pluginLog } from '../utils'; import { getThresholdsDrawHook, UPlotThresholdOptions } from './UPlotThresholds'; +const cursorDefaults: Cursor = { + // prevent client-side zoom from triggering at the end of a selection + drag: { setScale: false }, + points: { + /*@ts-ignore*/ + size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2, + /*@ts-ignore*/ + width: (u, seriesIdx, size) => size / 4, + /*@ts-ignore*/ + stroke: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx) + '80', + /*@ts-ignore*/ + fill: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx), + }, + focus: { + prox: 30, + }, +}; + export class UPlotConfigBuilder { private series: UPlotSeriesBuilder[] = []; private axes: Record = {}; @@ -98,7 +116,7 @@ export class UPlotConfigBuilder { } setCursor(cursor?: Cursor) { - this.cursor = { ...this.cursor, ...cursor }; + this.cursor = merge({}, this.cursor, cursor); } setSelect(select: Select) { @@ -153,11 +171,9 @@ export class UPlotConfigBuilder { config.hooks = this.hooks; - /* @ts-ignore */ - // uPlot types don't export the Select interface prior to 1.6.4 config.select = this.select; - config.cursor = this.cursor || {}; + config.cursor = merge({}, cursorDefaults, this.cursor); config.tzDate = this.tzDate; @@ -181,26 +197,6 @@ export class UPlotConfigBuilder { } } - const cursorDefaults: Cursor = { - // prevent client-side zoom from triggering at the end of a selection - drag: { setScale: false }, - points: { - /*@ts-ignore*/ - size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2, - /*@ts-ignore*/ - width: (u, seriesIdx, size) => size / 4, - /*@ts-ignore*/ - stroke: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx) + '80', - /*@ts-ignore*/ - fill: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx), - }, - focus: { - prox: 30, - }, - }; - - defaultsDeep(config.cursor, cursorDefaults); - return config; } diff --git a/public/app/plugins/panel/histogram/Histogram.tsx b/public/app/plugins/panel/histogram/Histogram.tsx index d3bfffb1a67..2895301c217 100644 --- a/public/app/plugins/panel/histogram/Histogram.tsx +++ b/public/app/plugins/panel/histogram/Histogram.tsx @@ -27,6 +27,14 @@ import { import { PanelOptions } from './models.gen'; import { ScaleDistribution } from '@grafana/ui/src/components/uPlot/models.gen'; +function incrRoundDn(num: number, incr: number) { + return Math.floor(num / incr) * incr; +} + +function incrRoundUp(num: number, incr: number) { + return Math.ceil(num / incr) * incr; +} + export interface HistogramProps extends Themeable2 { options: PanelOptions; // used for diff alignedFrame: DataFrame; // This could take HistogramFields @@ -70,7 +78,24 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { distribution: ScaleDistribution.Linear, orientation: ScaleOrientation.Horizontal, direction: ScaleDirection.Right, - range: (u) => [u.data[0][0], u.data[0][u.data[0].length - 1] + bucketSize], + range: (u, wantedMin, wantedMax) => { + let fullRangeMin = u.data[0][0]; + let fullRangeMax = u.data[0][u.data[0].length - 1]; + + // snap to bucket divisors... + + if (wantedMax === fullRangeMax) { + wantedMax += bucketSize; + } else { + wantedMax = incrRoundUp(wantedMax, bucketSize); + } + + if (wantedMin > fullRangeMin) { + wantedMin = incrRoundDn(wantedMin, bucketSize); + } + + return [wantedMin, wantedMax]; + }, }); builder.addScale({ @@ -114,6 +139,14 @@ const prepConfig = (frame: DataFrame, theme: GrafanaTheme2) => { theme, }); + builder.setCursor({ + drag: { + x: true, + y: false, + setScale: true, + }, + }); + let pathBuilder = uPlot.paths.bars!({ align: 1, size: [1, Infinity] }); let seriesIndex = 0;