From a76e21c837c0551ad2ad2cc91884f650c2e60eab Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Thu, 29 Feb 2024 09:53:00 +0100 Subject: [PATCH] TimeSeries: Support vertical orientation (#83272) * Time series with flipped axes * Ass prop and fix timezone labes * Remove not needed css * Fix zoomplugin * Update comment * Fix zoomplugin * Update * Update TooltipPlugin2 * Update variable names * Update naming and comments * Update public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx Co-authored-by: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> --------- Co-authored-by: Jev Forsberg <46619047+baldm0mma@users.noreply.github.com> --- .../x/TimeSeriesPanelCfg_types.gen.ts | 1 + .../grafana-ui/src/components/uPlot/Plot.scss | 7 ++-- .../uPlot/config/UPlotConfigBuilder.ts | 3 +- .../uPlot/plugins/TooltipPlugin2.tsx | 30 +++++++++++---- .../components/uPlot/plugins/ZoomPlugin.tsx | 18 ++++++--- .../core/components/TimeSeries/TimeSeries.tsx | 1 + .../app/core/components/TimeSeries/utils.ts | 38 ++++++++++++------- .../panel/timeseries/TimeSeriesPanel.tsx | 15 +++++--- .../app/plugins/panel/timeseries/panelcfg.cue | 5 ++- .../plugins/panel/timeseries/panelcfg.gen.ts | 1 + 10 files changed, 81 insertions(+), 38 deletions(-) diff --git a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts index 4e5261704f4..3a5d60e3189 100644 --- a/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts +++ b/packages/grafana-schema/src/raw/composable/timeseries/panelcfg/x/TimeSeriesPanelCfg_types.gen.ts @@ -15,6 +15,7 @@ export const pluginVersion = "11.0.0-pre"; export interface Options extends common.OptionsWithTimezones { legend: common.VizLegendOptions; + orientation?: common.VizOrientation; tooltip: common.VizTooltipOptions; } diff --git a/packages/grafana-ui/src/components/uPlot/Plot.scss b/packages/grafana-ui/src/components/uPlot/Plot.scss index 86614e7496b..895e3329895 100644 --- a/packages/grafana-ui/src/components/uPlot/Plot.scss +++ b/packages/grafana-ui/src/components/uPlot/Plot.scss @@ -9,12 +9,13 @@ background: rgba(120, 120, 130, 0.2); } -.u-cursor-x { +.u-hz .u-cursor-x, +.u-vt .u-cursor-y { border-right: 1px dashed rgba(120, 120, 130, 0.5); } -.u-cursor-y { - width: 100%; +.u-hz .u-cursor-y, +.u-vt .u-cursor-x { border-bottom: 1px dashed rgba(120, 120, 130, 0.5); } diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index df4badf622e..70f48798788 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -11,7 +11,7 @@ import { TimeRange, TimeZone, } from '@grafana/data'; -import { AxisPlacement } from '@grafana/schema'; +import { AxisPlacement, VizOrientation } from '@grafana/schema'; import { FacetedData, PlotConfig, PlotTooltipInterpolator } from '../types'; import { DEFAULT_PLOT_CONFIG, getStackingBands, pluginLog, StackingGroup } from '../utils'; @@ -314,6 +314,7 @@ type UPlotConfigPrepOpts = {}> = { // Identifies the shared key for uPlot cursor sync eventsScope?: string; hoverProximity?: number; + orientation?: VizOrientation; } & T; /** @alpha */ diff --git a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx index 2302c11c642..214a0b3b3e6 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/TooltipPlugin2.tsx @@ -282,7 +282,14 @@ export const TooltipPlugin2 = ({ u.over.addEventListener('click', (e) => { if (e.target === u.over) { if (e.ctrlKey || e.metaKey) { - let xVal = u.posToVal(u.cursor.left!, 'x'); + let xVal; + + const isXAxisHorizontal = u.scales.x.ori === 0; + if (isXAxisHorizontal) { + xVal = u.posToVal(u.cursor.left!, 'x'); + } else { + xVal = u.posToVal(u.select.top + u.select.height, 'x'); + } selectedRange = { from: xVal, @@ -301,14 +308,19 @@ export const TooltipPlugin2 = ({ }); config.addHook('setSelect', (u) => { + const isXAxisHorizontal = u.scales.x.ori === 0; if (!viaSync && (clientZoom || queryZoom != null)) { if (maybeZoomAction(u.cursor!.event)) { if (clientZoom && yDrag) { if (u.select.height >= MIN_ZOOM_DIST) { for (let key in u.scales!) { if (key !== 'x') { - const maxY = u.posToVal(u.select.top, key); - const minY = u.posToVal(u.select.top + u.select.height, key); + const maxY = isXAxisHorizontal + ? u.posToVal(u.select.top, key) + : u.posToVal(u.select.left + u.select.width, key); + const minY = isXAxisHorizontal + ? u.posToVal(u.select.top + u.select.height, key) + : u.posToVal(u.select.left, key); u.setScale(key, { min: minY, max: maxY }); } @@ -320,8 +332,12 @@ export const TooltipPlugin2 = ({ yDrag = false; } else if (queryZoom != null) { if (u.select.width >= MIN_ZOOM_DIST) { - const minX = u.posToVal(u.select.left, 'x'); - const maxX = u.posToVal(u.select.left + u.select.width, 'x'); + const minX = isXAxisHorizontal + ? u.posToVal(u.select.left, 'x') + : u.posToVal(u.select.top + u.select.height, 'x'); + const maxX = isXAxisHorizontal + ? u.posToVal(u.select.left + u.select.width, 'x') + : u.posToVal(u.select.top, 'x'); queryZoom({ from: minX, to: maxX }); @@ -330,8 +346,8 @@ export const TooltipPlugin2 = ({ } } else { selectedRange = { - from: u.posToVal(u.select.left!, 'x'), - to: u.posToVal(u.select.left! + u.select.width, 'x'), + from: isXAxisHorizontal ? u.posToVal(u.select.left!, 'x') : u.posToVal(u.select.top + u.select.height, 'x'), + to: isXAxisHorizontal ? u.posToVal(u.select.left! + u.select.width, 'x') : u.posToVal(u.select.top, 'x'), }; scheduleRender(true); diff --git a/packages/grafana-ui/src/components/uPlot/plugins/ZoomPlugin.tsx b/packages/grafana-ui/src/components/uPlot/plugins/ZoomPlugin.tsx index faed1ae95a9..1e412e41fcc 100644 --- a/packages/grafana-ui/src/components/uPlot/plugins/ZoomPlugin.tsx +++ b/packages/grafana-ui/src/components/uPlot/plugins/ZoomPlugin.tsx @@ -51,14 +51,18 @@ export const ZoomPlugin = ({ onZoom, config, withZoomY = false }: ZoomPluginProp } config.addHook('setSelect', (u) => { + const isXAxisHorizontal = u.scales.x.ori === 0; if (maybeZoomAction(u.cursor!.event)) { if (withZoomY && yDrag) { if (u.select.height >= MIN_ZOOM_DIST) { for (let key in u.scales!) { if (key !== 'x') { - const maxY = u.posToVal(u.select.top, key); - const minY = u.posToVal(u.select.top + u.select.height, key); - + const maxY = isXAxisHorizontal + ? u.posToVal(u.select.top, key) + : u.posToVal(u.select.left + u.select.width, key); + const minY = isXAxisHorizontal + ? u.posToVal(u.select.top + u.select.height, key) + : u.posToVal(u.select.left, key); u.setScale(key, { min: minY, max: maxY }); } } @@ -69,8 +73,12 @@ export const ZoomPlugin = ({ onZoom, config, withZoomY = false }: ZoomPluginProp yDrag = false; } else { if (u.select.width >= MIN_ZOOM_DIST) { - const minX = u.posToVal(u.select.left, 'x'); - const maxX = u.posToVal(u.select.left + u.select.width, 'x'); + const minX = isXAxisHorizontal + ? u.posToVal(u.select.left, 'x') + : u.posToVal(u.select.top + u.select.height, 'x'); + const maxX = isXAxisHorizontal + ? u.posToVal(u.select.left + u.select.width, 'x') + : u.posToVal(u.select.top, 'x'); onZoom({ from: minX, to: maxX }); diff --git a/public/app/core/components/TimeSeries/TimeSeries.tsx b/public/app/core/components/TimeSeries/TimeSeries.tsx index ddc34f0cbd7..3e714e9587d 100644 --- a/public/app/core/components/TimeSeries/TimeSeries.tsx +++ b/public/app/core/components/TimeSeries/TimeSeries.tsx @@ -35,6 +35,7 @@ export class UnthemedTimeSeries extends Component { tweakAxis, eventsScope, hoverProximity: options?.tooltip?.hoverProximity, + orientation: options?.orientation, }); }; diff --git a/public/app/core/components/TimeSeries/utils.ts b/public/app/core/components/TimeSeries/utils.ts index 82ceefbd65a..1baf1bd85e5 100644 --- a/public/app/core/components/TimeSeries/utils.ts +++ b/public/app/core/components/TimeSeries/utils.ts @@ -30,6 +30,7 @@ import { GraphTransform, AxisColorMode, GraphGradientMode, + VizOrientation, } from '@grafana/schema'; // unit lookup needed to determine if we want power-of-2 or power-of-10 axis ticks @@ -90,7 +91,10 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ tweakAxis = (opts) => opts, eventsScope = '__global_', hoverProximity, + orientation = VizOrientation.Horizontal, }) => { + // we want the Auto and Horizontal orientation to default to Horizontal + const isHorizontal = orientation !== VizOrientation.Vertical; const builder = new UPlotConfigBuilder(timeZones[0]); let alignedFrame: DataFrame; @@ -113,15 +117,19 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ let yScaleKey = ''; const xFieldAxisPlacement = - xField.config.custom?.axisPlacement !== AxisPlacement.Hidden ? AxisPlacement.Bottom : AxisPlacement.Hidden; + xField.config.custom?.axisPlacement === AxisPlacement.Hidden + ? AxisPlacement.Hidden + : isHorizontal + ? AxisPlacement.Bottom + : AxisPlacement.Left; const xFieldAxisShow = xField.config.custom?.axisPlacement !== AxisPlacement.Hidden; if (xField.type === FieldType.time) { xScaleUnit = 'time'; builder.addScale({ scaleKey: xScaleKey, - orientation: ScaleOrientation.Horizontal, - direction: ScaleDirection.Right, + orientation: isHorizontal ? ScaleOrientation.Horizontal : ScaleOrientation.Vertical, + direction: isHorizontal ? ScaleDirection.Right : ScaleDirection.Up, isTime: true, range: () => { const r = getTimeRange(); @@ -133,7 +141,10 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ const filterTicks: uPlot.Axis.Filter | undefined = timeZones.length > 1 ? (u, splits) => { - return splits.map((v, i) => (i < 2 ? null : v)); + if (isHorizontal) { + return splits.map((v, i) => (i < 2 ? null : v)); + } + return splits; } : undefined; @@ -157,13 +168,12 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ builder.addHook('drawAxes', (u: uPlot) => { u.ctx.save(); - u.ctx.fillStyle = theme.colors.text.primary; - u.ctx.textAlign = 'left'; - u.ctx.textBaseline = 'bottom'; - let i = 0; u.axes.forEach((a) => { - if (a.side === 2) { + if (isHorizontal && a.side === 2) { + u.ctx.fillStyle = theme.colors.text.primary; + u.ctx.textAlign = 'left'; + u.ctx.textBaseline = 'bottom'; //@ts-ignore let cssBaseline: number = a._pos + a._size; u.ctx.fillText(timeZones[i], u.bbox.left, cssBaseline * uPlot.pxRatio); @@ -182,8 +192,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ builder.addScale({ scaleKey: xScaleKey, - orientation: ScaleOrientation.Horizontal, - direction: ScaleDirection.Right, + orientation: isHorizontal ? ScaleOrientation.Horizontal : ScaleOrientation.Vertical, + direction: isHorizontal ? ScaleDirection.Right : ScaleDirection.Up, range: (u, dataMin, dataMax) => [xField.config.min ?? dataMin, xField.config.max ?? dataMax], }); @@ -243,8 +253,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ tweakScale( { scaleKey, - orientation: ScaleOrientation.Vertical, - direction: ScaleDirection.Up, + orientation: isHorizontal ? ScaleOrientation.Vertical : ScaleOrientation.Horizontal, + direction: isHorizontal ? ScaleDirection.Up : ScaleDirection.Right, distribution: customConfig.scaleDistribution?.type, log: customConfig.scaleDistribution?.log, linearThreshold: customConfig.scaleDistribution?.linearThreshold, @@ -329,7 +339,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ scaleKey, label: customConfig.axisLabel, size: customConfig.axisWidth, - placement: customConfig.axisPlacement ?? AxisPlacement.Auto, + placement: isHorizontal ? customConfig.axisPlacement ?? AxisPlacement.Auto : AxisPlacement.Bottom, formatValue: (v, decimals) => formattedValueToString(fmt(v, decimals)), theme, grid: { show: customConfig.axisGridShow }, diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index 50ee36677b3..36480a53d57 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -2,7 +2,7 @@ import React, { useMemo, useState, useCallback } from 'react'; import { PanelProps, DataFrameType, DashboardCursorSync } from '@grafana/data'; import { PanelDataErrorView } from '@grafana/runtime'; -import { TooltipDisplayMode } from '@grafana/schema'; +import { TooltipDisplayMode, VizOrientation } from '@grafana/schema'; import { KeyboardPlugin, TooltipPlugin, TooltipPlugin2, usePanelContext, ZoomPlugin } from '@grafana/ui'; import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries'; @@ -36,7 +36,9 @@ export const TimeSeriesPanel = ({ }: TimeSeriesPanelProps) => { const { sync, canAddAnnotations, onThresholdsChange, canEditThresholds, showThresholds, dataLinkPostProcessor } = usePanelContext(); - + // Vertical orientation is not available for users through config. + // It is simplified version of horizontal time series panel and it does not support all plugins. + const isVerticallyOriented = options.orientation === VizOrientation.Vertical; const frames = useMemo(() => prepareGraphableFields(data.series, config.theme2, timeRange), [data.series, timeRange]); const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]); const suggestions = useMemo(() => { @@ -169,7 +171,7 @@ export const TimeSeriesPanel = ({ )} {/* Renders annotation markers*/} - {showNewVizTooltips ? ( + {!isVerticallyOriented && showNewVizTooltips ? ( ) : ( + !isVerticallyOriented && data.annotations && ( ) @@ -185,7 +188,7 @@ export const TimeSeriesPanel = ({ {/*Enables annotations creation*/} {!showNewVizTooltips ? ( - enableAnnotationCreation ? ( + enableAnnotationCreation && !isVerticallyOriented ? ( {({ startAnnotating }) => { return ( @@ -226,7 +229,7 @@ export const TimeSeriesPanel = ({ /> ) ) : undefined} - {data.annotations && ( + {data.annotations && !isVerticallyOriented && ( )} - {((canEditThresholds && onThresholdsChange) || showThresholds) && ( + {((canEditThresholds && onThresholdsChange) || showThresholds) && !isVerticallyOriented && (