TimeRange: Avoid x-axis pan jump caused by data loading latency (#114496)
* fix(time-range): avoid x-axis pan jump caused by data loading latency * refactor(time-range): use a more semantically meaningful names
This commit is contained in:
@@ -29,7 +29,7 @@ const cursorDefaults: Cursor = {
|
||||
type PrepData = (frames: DataFrame[]) => AlignedData | FacetedData;
|
||||
type PreDataStacked = (frames: DataFrame[], stackingGroups: StackingGroup[]) => AlignedData | FacetedData;
|
||||
|
||||
type PlotState = { isPanning: false } | { isPanning: true; min: number; max: number };
|
||||
type PlotState = { isPanning: false } | { isPanning: true; min: number; max: number; isTimeRangePending?: boolean };
|
||||
|
||||
export class UPlotConfigBuilder {
|
||||
readonly uid = Math.random().toString(36).slice(2);
|
||||
|
||||
+15
-1
@@ -137,7 +137,7 @@ describe('XAxisInteractionAreaPlugin', () => {
|
||||
expect(mockQueryZoom).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set isPanning state during drag and clear on mouseup', () => {
|
||||
it('should set isPanning state during drag and mark isTimeRangePending on mouseup', () => {
|
||||
setupXAxisPan(asUPlot(mockUPlot), asConfigBuilder(mockConfigBuilder), mockQueryZoom);
|
||||
|
||||
xAxisElement.dispatchEvent(new MouseEvent('mousedown', { clientX: 400, bubbles: true }));
|
||||
@@ -153,6 +153,20 @@ describe('XAxisInteractionAreaPlugin', () => {
|
||||
|
||||
document.dispatchEvent(new MouseEvent('mouseup', { clientX: 350, bubbles: true }));
|
||||
|
||||
expect(mockConfigBuilder.setState).toHaveBeenCalledWith({
|
||||
isPanning: true,
|
||||
min: expectedRange.from,
|
||||
max: expectedRange.to,
|
||||
isTimeRangePending: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear isPanning state immediately for small drags below threshold', () => {
|
||||
setupXAxisPan(asUPlot(mockUPlot), asConfigBuilder(mockConfigBuilder), mockQueryZoom);
|
||||
|
||||
xAxisElement.dispatchEvent(new MouseEvent('mousedown', { clientX: 400, bubbles: true }));
|
||||
document.dispatchEvent(new MouseEvent('mouseup', { clientX: 402, bubbles: true }));
|
||||
|
||||
expect(mockConfigBuilder.setState).toHaveBeenCalledWith({ isPanning: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,11 +96,14 @@ export const setupXAxisPan = (
|
||||
|
||||
xAxisEl.style.cursor = 'grab';
|
||||
|
||||
config.setState({ isPanning: false });
|
||||
const isSignificantDrag = Math.abs(dragPixels) >= MIN_PAN_DIST;
|
||||
|
||||
if (Math.abs(dragPixels) >= MIN_PAN_DIST) {
|
||||
if (isSignificantDrag) {
|
||||
const newRange = calculatePanRange(startMin, startMax, dragPixels, u.bbox.width);
|
||||
config.setState({ isPanning: true, min: newRange.from, max: newRange.to, isTimeRangePending: true });
|
||||
queryZoom(newRange);
|
||||
} else {
|
||||
config.setState({ isPanning: false });
|
||||
}
|
||||
|
||||
document.removeEventListener('mousemove', onMove);
|
||||
|
||||
@@ -138,10 +138,26 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
|
||||
range: () => {
|
||||
const state = builder.getState();
|
||||
if (state.isPanning) {
|
||||
if (state.isTimeRangePending) {
|
||||
const timeRange = getTimeRange();
|
||||
const propsFrom = timeRange.from.valueOf();
|
||||
const propsTo = timeRange.to.valueOf();
|
||||
|
||||
const MIN_TIMESPAN_MS = 1;
|
||||
const fromMatches = Math.abs(propsFrom - state.min) <= MIN_TIMESPAN_MS;
|
||||
const toMatches = Math.abs(propsTo - state.max) <= MIN_TIMESPAN_MS;
|
||||
const timeRangeHasUpdated = fromMatches && toMatches;
|
||||
|
||||
if (timeRangeHasUpdated) {
|
||||
builder.setState({ isPanning: false });
|
||||
return [propsFrom, propsTo];
|
||||
}
|
||||
}
|
||||
|
||||
return [state.min, state.max];
|
||||
}
|
||||
const r = getTimeRange();
|
||||
return [r.from.valueOf(), r.to.valueOf()];
|
||||
const timeRange = getTimeRange();
|
||||
return [timeRange.from.valueOf(), timeRange.to.valueOf()];
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -159,6 +159,24 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<UPlotConfigOptions> = (
|
||||
range: (u) => {
|
||||
const state = builder.getState();
|
||||
if (state.isPanning) {
|
||||
if (state.isTimeRangePending) {
|
||||
const propsRange = coreConfig.xRange(u);
|
||||
const propsFrom = propsRange[0];
|
||||
const propsTo = propsRange[1];
|
||||
|
||||
if (propsFrom != null && propsTo != null) {
|
||||
const MIN_TIMESPAN_MS = 1;
|
||||
const fromMatches = Math.abs(propsFrom - state.min) <= MIN_TIMESPAN_MS;
|
||||
const toMatches = Math.abs(propsTo - state.max) <= MIN_TIMESPAN_MS;
|
||||
const timeRangeHasUpdated = fromMatches && toMatches;
|
||||
|
||||
if (timeRangeHasUpdated) {
|
||||
builder.setState({ isPanning: false });
|
||||
return propsRange;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [state.min, state.max];
|
||||
}
|
||||
return coreConfig.xRange(u);
|
||||
|
||||
Reference in New Issue
Block a user