Chart cursor, range picker in the header, line smoothing
A dashboard canvas is CSS-scaled to fit its panel while uPlot maps the pointer against its own unscaled plot width, so the cursor drifted further right the further into a chart it went. A `cursor.move` refiner divides the visual offset back into layout pixels; unscaled hosts get a no-op. A querying chart's range picker moves onto the frame's title line through a new `useHeaderSlot`, giving the plot back the row it spent. The editor's drag handle is the header, so the picker is exempted from it. Charts can be drawn as a monotone cubic spline — uPlot's own path builder, monotone so a smoothed line never invents a reading between two real ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ZeGnqVsf5VHQqvz4HdUhN
This commit is contained in:
@@ -401,6 +401,57 @@ test("a chart's axis title is kept", async ({ page }) => {
|
||||
await expect(field).toHaveValue("kW")
|
||||
})
|
||||
|
||||
/**
|
||||
* The cursor has to land under the pointer.
|
||||
*
|
||||
* A panel is drawn at its own pixel size and CSS-scaled to fit the screen it
|
||||
* landed on, while uPlot maps the pointer against its own unscaled plot width
|
||||
* — so without a correction the cursor lags further behind the further into
|
||||
* the chart it is. The cursor line's box is in screen pixels, which is the
|
||||
* same space the mouse was moved in, so the two are directly comparable.
|
||||
*
|
||||
* The readings are published with the panel already open: nothing keeps a ring
|
||||
* for this tile, so the live tail is what puts a line on it.
|
||||
*/
|
||||
test("a chart's cursor follows the pointer", async ({ page }) => {
|
||||
await openPanel(page)
|
||||
// The socket carries the tail, so it has to be listening first.
|
||||
await page.waitForTimeout(1000)
|
||||
for (const value of [40, 60, 50, 70]) {
|
||||
await publish(page, w("level"), value)
|
||||
await page.waitForTimeout(250)
|
||||
}
|
||||
|
||||
const chart = page.getByTestId("widget-frame").filter({ hasText: "Trend" })
|
||||
const over = chart.locator(".u-over")
|
||||
await over.waitFor({ timeout: 15000 })
|
||||
const box = (await over.boundingBox()) as {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
// Worth asserting only while the panel really is scaled, which is what the
|
||||
// correction is for.
|
||||
const drawnAt = await over.evaluate(
|
||||
(el) => el.getBoundingClientRect().width / el.clientWidth,
|
||||
)
|
||||
expect(drawnAt, "a panel is scaled to fit the screen").toBeLessThan(0.95)
|
||||
|
||||
// Well inside: uPlot snaps the last pixel at either edge to the edge itself.
|
||||
const x = box.x + box.width * 0.6
|
||||
await page.mouse.move(x, box.y + box.height / 2)
|
||||
|
||||
const cursor = chart.locator(".u-cursor-x")
|
||||
await expect(cursor).toBeVisible()
|
||||
const line = (await cursor.boundingBox()) as { x: number }
|
||||
expect(
|
||||
Math.abs(line.x - x),
|
||||
`the cursor is drawn at ${line.x.toFixed(1)}, the pointer is at ${x.toFixed(1)}`,
|
||||
).toBeLessThan(3)
|
||||
})
|
||||
|
||||
for (const scheme of ["light", "dark"] as const) {
|
||||
test.describe(`${scheme} theme`, () => {
|
||||
test.use({ colorScheme: scheme })
|
||||
|
||||
Reference in New Issue
Block a user