mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TimeSeries: make cursor hover the nearest non-null/undefined datapoint (#34552)
This commit is contained in:
parent
32b74e75a3
commit
4c3e197e26
@ -69,6 +69,7 @@ Object {
|
||||
},
|
||||
],
|
||||
"cursor": Object {
|
||||
"dataIdx": [Function],
|
||||
"drag": Object {
|
||||
"setScale": false,
|
||||
},
|
||||
|
@ -18,7 +18,9 @@ function applySpanNullsThresholds(frame: DataFrame) {
|
||||
let spanNulls = field.config.custom?.spanNulls;
|
||||
|
||||
if (typeof spanNulls === 'number') {
|
||||
field.values = new ArrayVector(nullToUndefThreshold(refValues, field.values.toArray(), spanNulls));
|
||||
if (spanNulls !== -1) {
|
||||
field.values = new ArrayVector(nullToUndefThreshold(refValues, field.values.toArray(), spanNulls));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -262,6 +262,58 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
|
||||
|
||||
builder.scaleKeys = [xScaleKey, yScaleKey];
|
||||
|
||||
// if hovered value is null, how far we may scan left/right to hover nearest non-null
|
||||
const hoverProximityPx = 15;
|
||||
|
||||
let cursor: Partial<uPlot.Cursor> = {
|
||||
// this scans left and right from cursor position to find nearest data index with value != null
|
||||
// TODO: do we want to only scan past undefined values, but halt at explicit null values?
|
||||
dataIdx: (self, seriesIdx, hoveredIdx, cursorXVal) => {
|
||||
let seriesData = self.data[seriesIdx];
|
||||
|
||||
if (seriesData[hoveredIdx] == null) {
|
||||
let nonNullLft = hoveredIdx,
|
||||
nonNullRgt = hoveredIdx,
|
||||
i;
|
||||
|
||||
i = hoveredIdx;
|
||||
while (nonNullLft === hoveredIdx && i-- > 0) {
|
||||
if (seriesData[i] != null) {
|
||||
nonNullLft = i;
|
||||
}
|
||||
}
|
||||
|
||||
i = hoveredIdx;
|
||||
while (nonNullRgt === hoveredIdx && i++ < seriesData.length) {
|
||||
if (seriesData[i] != null) {
|
||||
nonNullRgt = i;
|
||||
}
|
||||
}
|
||||
|
||||
let xVals = self.data[0];
|
||||
|
||||
let curPos = self.valToPos(cursorXVal, 'x');
|
||||
let rgtPos = self.valToPos(xVals[nonNullRgt], 'x');
|
||||
let lftPos = self.valToPos(xVals[nonNullLft], 'x');
|
||||
|
||||
let lftDelta = curPos - lftPos;
|
||||
let rgtDelta = rgtPos - curPos;
|
||||
|
||||
if (lftDelta <= rgtDelta) {
|
||||
if (lftDelta <= hoverProximityPx) {
|
||||
hoveredIdx = nonNullLft;
|
||||
}
|
||||
} else {
|
||||
if (rgtDelta <= hoverProximityPx) {
|
||||
hoveredIdx = nonNullRgt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hoveredIdx;
|
||||
},
|
||||
};
|
||||
|
||||
if (sync !== DashboardCursorSync.Off) {
|
||||
const payload: DataHoverPayload = {
|
||||
point: {
|
||||
@ -271,34 +323,34 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
|
||||
data: frame,
|
||||
};
|
||||
const hoverEvent = new DataHoverEvent(payload);
|
||||
builder.setSync();
|
||||
builder.setCursor({
|
||||
sync: {
|
||||
key: '__global_',
|
||||
filters: {
|
||||
pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => {
|
||||
payload.columnIndex = dataIdx;
|
||||
if (x < 0 && y < 0) {
|
||||
payload.point[xScaleUnit] = null;
|
||||
payload.point[yScaleKey] = null;
|
||||
eventBus.publish(new DataHoverClearEvent(payload));
|
||||
} else {
|
||||
// convert the points
|
||||
payload.point[xScaleUnit] = src.posToVal(x, xScaleKey);
|
||||
payload.point[yScaleKey] = src.posToVal(y, yScaleKey);
|
||||
eventBus.publish(hoverEvent);
|
||||
hoverEvent.payload.down = undefined;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
cursor.sync = {
|
||||
key: '__global_',
|
||||
filters: {
|
||||
pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => {
|
||||
payload.columnIndex = dataIdx;
|
||||
if (x < 0 && y < 0) {
|
||||
payload.point[xScaleUnit] = null;
|
||||
payload.point[yScaleKey] = null;
|
||||
eventBus.publish(new DataHoverClearEvent(payload));
|
||||
} else {
|
||||
// convert the points
|
||||
payload.point[xScaleUnit] = src.posToVal(x, xScaleKey);
|
||||
payload.point[yScaleKey] = src.posToVal(y, yScaleKey);
|
||||
eventBus.publish(hoverEvent);
|
||||
hoverEvent.payload.down = undefined;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
// ??? setSeries: syncMode === DashboardCursorSync.Tooltip,
|
||||
scales: builder.scaleKeys,
|
||||
match: [() => true, () => true],
|
||||
},
|
||||
});
|
||||
// ??? setSeries: syncMode === DashboardCursorSync.Tooltip,
|
||||
scales: builder.scaleKeys,
|
||||
match: [() => true, () => true],
|
||||
};
|
||||
}
|
||||
|
||||
builder.setSync();
|
||||
builder.setCursor(cursor);
|
||||
|
||||
return builder;
|
||||
};
|
||||
|
||||
|
@ -43,6 +43,7 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
||||
const plotCtx = usePlotContext();
|
||||
const [focusedSeriesIdx, setFocusedSeriesIdx] = useState<number | null>(null);
|
||||
const [focusedPointIdx, setFocusedPointIdx] = useState<number | null>(null);
|
||||
const [focusedPointIdxs, setFocusedPointIdxs] = useState<Array<number | null>>([]);
|
||||
const [coords, setCoords] = useState<CartesianCoords2D | null>(null);
|
||||
const plotInstance = plotCtx.plot;
|
||||
|
||||
@ -93,10 +94,13 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
||||
})(u);
|
||||
});
|
||||
} else {
|
||||
config.addHook('setLegend', (u) => {
|
||||
setFocusedPointIdx(u.cursor.idx!);
|
||||
setFocusedPointIdxs(u.cursor.idxs!.slice());
|
||||
});
|
||||
|
||||
// default series/datapoint idx retireval
|
||||
config.addHook('setCursor', (u) => {
|
||||
setFocusedPointIdx(u.cursor.idx === undefined ? u.posToIdx(u.cursor.left || 0) : u.cursor.idx);
|
||||
|
||||
const bbox = plotCtx.getCanvasBoundingBox();
|
||||
if (!bbox) {
|
||||
return;
|
||||
@ -174,7 +178,7 @@ export const TooltipPlugin: React.FC<TooltipPluginProps> = ({
|
||||
continue;
|
||||
}
|
||||
|
||||
const display = field.display!(otherProps.data.fields[i].values.get(focusedPointIdx));
|
||||
const display = field.display!(otherProps.data.fields[i].values.get(focusedPointIdxs[i]!));
|
||||
|
||||
series.push({
|
||||
color: display.color || FALLBACK_COLOR,
|
||||
|
Loading…
Reference in New Issue
Block a user