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>
This commit is contained in:
Ivana Huckova 2024-02-29 09:53:00 +01:00 committed by GitHub
parent 8ed932bb60
commit a76e21c837
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 81 additions and 38 deletions

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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<T extends Record<string, unknown> = {}> = {
// Identifies the shared key for uPlot cursor sync
eventsScope?: string;
hoverProximity?: number;
orientation?: VizOrientation;
} & T;
/** @alpha */

View File

@ -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);

View File

@ -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 });

View File

@ -35,6 +35,7 @@ export class UnthemedTimeSeries extends Component<TimeSeriesProps> {
tweakAxis,
eventsScope,
hoverProximity: options?.tooltip?.hoverProximity,
orientation: options?.orientation,
});
};

View File

@ -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 },

View File

@ -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 ? (
<AnnotationsPlugin2
annotations={data.annotations ?? []}
config={uplotConfig}
@ -178,6 +180,7 @@ export const TimeSeriesPanel = ({
setNewRange={setNewAnnotationRange}
/>
) : (
!isVerticallyOriented &&
data.annotations && (
<AnnotationsPlugin annotations={data.annotations} config={uplotConfig} timeZone={timeZone} />
)
@ -185,7 +188,7 @@ export const TimeSeriesPanel = ({
{/*Enables annotations creation*/}
{!showNewVizTooltips ? (
enableAnnotationCreation ? (
enableAnnotationCreation && !isVerticallyOriented ? (
<AnnotationEditorPlugin data={alignedDataFrame} timeZone={timeZone} config={uplotConfig}>
{({ startAnnotating }) => {
return (
@ -226,7 +229,7 @@ export const TimeSeriesPanel = ({
/>
)
) : undefined}
{data.annotations && (
{data.annotations && !isVerticallyOriented && (
<ExemplarsPlugin
visibleSeries={getVisibleLabels(uplotConfig, frames)}
config={uplotConfig}
@ -235,7 +238,7 @@ export const TimeSeriesPanel = ({
/>
)}
{((canEditThresholds && onThresholdsChange) || showThresholds) && (
{((canEditThresholds && onThresholdsChange) || showThresholds) && !isVerticallyOriented && (
<ThresholdControlsPlugin
config={uplotConfig}
fieldConfig={fieldConfig}

View File

@ -23,8 +23,9 @@ composableKinds: PanelCfg: lineage: {
version: [0, 0]
schema: {
Options: common.OptionsWithTimezones & {
legend: common.VizLegendOptions
tooltip: common.VizTooltipOptions
legend: common.VizLegendOptions
tooltip: common.VizTooltipOptions
orientation?: common.VizOrientation
} @cuetsy(kind="interface")
FieldConfig: common.GraphFieldConfig & {} @cuetsy(kind="interface")

View File

@ -12,6 +12,7 @@ import * as common from '@grafana/schema';
export interface Options extends common.OptionsWithTimezones {
legend: common.VizLegendOptions;
orientation?: common.VizOrientation;
tooltip: common.VizTooltipOptions;
}