From fcceff99e2d8d8bdea9d77782ff6f12286a6fd08 Mon Sep 17 00:00:00 2001
From: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com>
Date: Mon, 17 Nov 2025 06:36:36 -0800
Subject: [PATCH] CandleStick: Add timeRangePan (#113888)
* Candlestick: Add timeRangePan
* Add tests
* Rename test file
* Optimize time range pan tests
---
.../panels-suite/candlestick.spec.ts | 112 ++++++++++++++++++
.../panel/candlestick/CandlestickPanel.tsx | 2 +
2 files changed, 114 insertions(+)
create mode 100644 e2e-playwright/panels-suite/candlestick.spec.ts
diff --git a/e2e-playwright/panels-suite/candlestick.spec.ts b/e2e-playwright/panels-suite/candlestick.spec.ts
new file mode 100644
index 00000000000..ad29f64ab47
--- /dev/null
+++ b/e2e-playwright/panels-suite/candlestick.spec.ts
@@ -0,0 +1,112 @@
+import { test, expect } from '@grafana/plugin-e2e';
+
+const DASHBOARD_UID = 'MP-Di9F7k';
+
+test.use({
+ featureToggles: {
+ timeRangePan: true,
+ },
+});
+
+test.describe('Panels test: Candlestick X-axis panning', { tag: ['@panels', '@candlestick'] }, () => {
+ test('x-axis panning functionality', async ({ gotoDashboardPage, page, selectors }) => {
+ let centerX: number;
+ let centerY: number;
+ let initialFromTime: number;
+ let initialToTime: number;
+
+ const dashboardPage = await test.step('Load dashboard and verify cursor changes to grab', async () => {
+ const dashboardPage = await gotoDashboardPage({ uid: DASHBOARD_UID });
+
+ const candlestickPanel = page.locator('.uplot').first();
+ await expect(candlestickPanel, 'panel rendered').toBeVisible();
+
+ const xAxis = candlestickPanel.locator('.u-axis').first();
+ await expect(xAxis, 'x-axis rendered').toBeVisible();
+
+ await xAxis.hover();
+
+ const cursorStyle = await xAxis.evaluate((el: HTMLElement) => window.getComputedStyle(el).cursor);
+ expect(cursorStyle, 'cursor is grab').toBe('grab');
+
+ return dashboardPage;
+ });
+
+ await test.step('Capture initial time range', async () => {
+ const candlestickPanel = page.locator('.uplot').first();
+ const xAxis = candlestickPanel.locator('.u-axis').first();
+
+ const timePickerButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton);
+ await timePickerButton.click();
+
+ const fromField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField);
+ const toField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField);
+
+ const initialFrom = await fromField.inputValue();
+ const initialTo = await toField.inputValue();
+ initialFromTime = new Date(initialFrom).getTime();
+ initialToTime = new Date(initialTo).getTime();
+
+ await page.keyboard.press('Escape');
+
+ const axisBox = await xAxis.boundingBox();
+ if (!axisBox) {
+ throw new Error('X-axis bounding box not found');
+ }
+
+ centerX = axisBox.x + axisBox.width / 2;
+ centerY = axisBox.y + axisBox.height / 2;
+ });
+
+ await test.step('Drag right pans backward in time', async () => {
+ await page.mouse.move(centerX, centerY);
+ await page.mouse.down();
+ await page.mouse.move(centerX + 100, centerY);
+ await page.mouse.up();
+
+ await page.waitForTimeout(1000);
+
+ const timePickerButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton);
+ await timePickerButton.click();
+
+ const fromField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField);
+ const toField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField);
+
+ const afterRightFrom = await fromField.inputValue();
+ const afterRightTo = await toField.inputValue();
+ const afterRightFromTime = new Date(afterRightFrom).getTime();
+ const afterRightToTime = new Date(afterRightTo).getTime();
+
+ expect(afterRightFromTime, 'panned backward').toBeLessThan(initialFromTime);
+ expect(afterRightToTime, 'panned backward').toBeLessThan(initialToTime);
+
+ await page.keyboard.press('Escape');
+
+ initialFromTime = afterRightFromTime;
+ initialToTime = afterRightToTime;
+ });
+
+ await test.step('Drag left pans forward in time', async () => {
+ await page.mouse.move(centerX, centerY);
+ await page.mouse.down();
+ await page.mouse.move(centerX - 100, centerY);
+ await page.mouse.up();
+
+ await page.waitForTimeout(1000);
+
+ const timePickerButton = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.openButton);
+ await timePickerButton.click();
+
+ const fromField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.fromField);
+ const toField = dashboardPage.getByGrafanaSelector(selectors.components.TimePicker.toField);
+
+ const afterLeftFrom = await fromField.inputValue();
+ const afterLeftTo = await toField.inputValue();
+ const afterLeftFromTime = new Date(afterLeftFrom).getTime();
+ const afterLeftToTime = new Date(afterLeftTo).getTime();
+
+ expect(afterLeftFromTime, 'panned forward').toBeGreaterThan(initialFromTime);
+ expect(afterLeftToTime, 'panned forward').toBeGreaterThan(initialToTime);
+ });
+ });
+});
diff --git a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx
index e566a8445c6..4f5f7a46b23 100644
--- a/public/app/plugins/panel/candlestick/CandlestickPanel.tsx
+++ b/public/app/plugins/panel/candlestick/CandlestickPanel.tsx
@@ -12,6 +12,7 @@ import {
KeyboardPlugin,
TooltipPlugin2,
UPlotConfigBuilder,
+ XAxisInteractionAreaPlugin,
usePanelContext,
useTheme2,
} from '@grafana/ui';
@@ -276,6 +277,7 @@ export const CandlestickPanel = ({
{cursorSync !== DashboardCursorSync.Off && (
)}
+
{options.tooltip.mode !== TooltipDisplayMode.None && (