StateTimeline: Share cursor with rest of the panels (#41038)

* First working version of shared cursor for state timeline

* Only publish x value for time series

* Don't send legacy graph event

* Don't add y scale to cursor sync

* Snap cursor to the bottom of the canvas when sync is out of bounds

* Fix snapshot
This commit is contained in:
Zoltán Bedi 2021-11-05 18:52:40 +01:00 committed by GitHub
parent d69ffe0e44
commit af61839a26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 63 additions and 7 deletions

View File

@ -95,7 +95,7 @@ Object {
], ],
"scales": Array [ "scales": Array [
"x", "x",
"__fixed", null,
], ],
}, },
}, },

View File

@ -363,7 +363,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
}, },
}, },
// ??? setSeries: syncMode === DashboardCursorSync.Tooltip, // ??? setSeries: syncMode === DashboardCursorSync.Tooltip,
scales: builder.scaleKeys, //TODO: remove any once https://github.com/leeoniya/uPlot/pull/611 got merged or the typing is fixed
scales: [xScaleKey, null as any],
match: [() => true, () => true], match: [() => true, () => true],
}; };
} }

View File

@ -181,6 +181,11 @@ export function findMidPointYPosition(u: uPlot, idx: number) {
y = u.valToPos((min || max)!, u.series[(sMaxIdx || sMinIdx)!].scale!); y = u.valToPos((min || max)!, u.series[(sMaxIdx || sMinIdx)!].scale!);
} }
// if y is out of canvas bounds, snap it to the bottom
if (y !== undefined && y < 0) {
y = u.bbox.height / devicePixelRatio;
}
return y; return y;
} }

View File

@ -1,6 +1,6 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { DataFrame, PanelProps } from '@grafana/data'; import { DataFrame, PanelProps } from '@grafana/data';
import { TooltipPlugin, useTheme2, ZoomPlugin } from '@grafana/ui'; import { TooltipPlugin, useTheme2, ZoomPlugin, usePanelContext } from '@grafana/ui';
import { TimelineMode, TimelineOptions } from './types'; import { TimelineMode, TimelineOptions } from './types';
import { TimelineChart } from './TimelineChart'; import { TimelineChart } from './TimelineChart';
import { prepareTimelineFields, prepareTimelineLegendItems } from './utils'; import { prepareTimelineFields, prepareTimelineLegendItems } from './utils';
@ -22,6 +22,7 @@ export const StateTimelinePanel: React.FC<TimelinePanelProps> = ({
onChangeTimeRange, onChangeTimeRange,
}) => { }) => {
const theme = useTheme2(); const theme = useTheme2();
const { sync } = usePanelContext();
const { frames, warn } = useMemo(() => prepareTimelineFields(data?.series, options.mergeValues ?? true, theme), [ const { frames, warn } = useMemo(() => prepareTimelineFields(data?.series, options.mergeValues ?? true, theme), [
data, data,
@ -103,6 +104,7 @@ export const StateTimelinePanel: React.FC<TimelinePanelProps> = ({
<ZoomPlugin config={config} onZoom={onChangeTimeRange} /> <ZoomPlugin config={config} onZoom={onChangeTimeRange} />
<TooltipPlugin <TooltipPlugin
data={alignedFrame} data={alignedFrame}
sync={sync}
config={config} config={config}
mode={options.tooltip.mode} mode={options.tooltip.mode}
timeZone={timeZone} timeZone={timeZone}

View File

@ -36,12 +36,13 @@ export class TimelineChart extends React.Component<TimelineProps> {
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
this.panelContext = this.context as PanelContext; this.panelContext = this.context as PanelContext;
const { eventBus } = this.panelContext; const { eventBus, sync } = this.panelContext;
return preparePlotConfigBuilder({ return preparePlotConfigBuilder({
frame: alignedFrame, frame: alignedFrame,
getTimeRange, getTimeRange,
eventBus, eventBus,
sync,
allFrames: this.props.frames, allFrames: this.props.frames,
...this.props, ...this.props,

View File

@ -1,4 +1,5 @@
import { OptionsWithTooltip, OptionsWithLegend, HideableFieldConfig, VisibilityMode } from '@grafana/schema'; import { DashboardCursorSync } from '@grafana/data';
import { HideableFieldConfig, OptionsWithLegend, OptionsWithTooltip, VisibilityMode } from '@grafana/schema';
/** /**
* @alpha * @alpha
@ -15,6 +16,8 @@ export interface TimelineOptions extends OptionsWithLegend, OptionsWithTooltip {
mergeValues?: boolean; mergeValues?: boolean;
// only used in "changes" mode (state-timeline) // only used in "changes" mode (state-timeline)
alignValue?: TimelineValueAlignment; alignValue?: TimelineValueAlignment;
sync?: DashboardCursorSync;
} }
export type TimelineValueAlignment = 'center' | 'left' | 'right'; export type TimelineValueAlignment = 'center' | 'left' | 'right';

View File

@ -3,6 +3,10 @@ import { XYFieldMatchers } from '@grafana/ui/src/components/GraphNG/types';
import { import {
ArrayVector, ArrayVector,
DataFrame, DataFrame,
DashboardCursorSync,
DataHoverPayload,
DataHoverEvent,
DataHoverClearEvent,
FALLBACK_COLOR, FALLBACK_COLOR,
Field, Field,
FieldColorModeId, FieldColorModeId,
@ -58,6 +62,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
timeZone, timeZone,
getTimeRange, getTimeRange,
mode, mode,
eventBus,
sync,
rowHeight, rowHeight,
colWidth, colWidth,
showValue, showValue,
@ -65,6 +71,9 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
}) => { }) => {
const builder = new UPlotConfigBuilder(timeZone); const builder = new UPlotConfigBuilder(timeZone);
const xScaleUnit = 'time';
const xScaleKey = 'x';
const isDiscrete = (field: Field) => { const isDiscrete = (field: Field) => {
const mode = field.config?.color?.mode; const mode = field.config?.color?.mode;
return !(mode && field.display && mode.startsWith('continuous-')); return !(mode && field.display && mode.startsWith('continuous-'));
@ -116,6 +125,13 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
let hoveredDataIdx: number | null = null; let hoveredDataIdx: number | null = null;
const coreConfig = getConfig(opts); const coreConfig = getConfig(opts);
const payload: DataHoverPayload = {
point: {
[xScaleUnit]: null,
[FIXED_UNIT]: null,
},
data: frame,
};
builder.addHook('init', coreConfig.init); builder.addHook('init', coreConfig.init);
builder.addHook('drawClear', coreConfig.drawClear); builder.addHook('drawClear', coreConfig.drawClear);
@ -148,7 +164,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
builder.setCursor(coreConfig.cursor); builder.setCursor(coreConfig.cursor);
builder.addScale({ builder.addScale({
scaleKey: 'x', scaleKey: xScaleKey,
isTime: true, isTime: true,
orientation: ScaleOrientation.Horizontal, orientation: ScaleOrientation.Horizontal,
direction: ScaleDirection.Right, direction: ScaleDirection.Right,
@ -164,7 +180,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
}); });
builder.addAxis({ builder.addAxis({
scaleKey: 'x', scaleKey: xScaleKey,
isTime: true, isTime: true,
splits: coreConfig.xSplits!, splits: coreConfig.xSplits!,
placement: AxisPlacement.Bottom, placement: AxisPlacement.Bottom,
@ -219,6 +235,34 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
}); });
} }
if (sync !== DashboardCursorSync.Off) {
let cursor: Partial<uPlot.Cursor> = {};
cursor.sync = {
key: '__global_',
filters: {
pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => {
payload.rowIndex = dataIdx;
if (x < 0 && y < 0) {
payload.point[xScaleUnit] = null;
payload.point[FIXED_UNIT] = null;
eventBus.publish(new DataHoverClearEvent());
} else {
payload.point[xScaleUnit] = src.posToVal(x, xScaleKey);
payload.point.panelRelY = y > 0 ? y / h : 1; // used for old graph panel to position tooltip
payload.down = undefined;
eventBus.publish(new DataHoverEvent(payload));
}
return true;
},
},
//TODO: remove any once https://github.com/leeoniya/uPlot/pull/611 got merged or the typing is fixed
scales: [xScaleKey, null as any],
};
builder.setSync();
builder.setCursor(cursor);
}
return builder; return builder;
}; };