From cfdf9a4abc4bd5c567f0eb3ce6cded3a1aa38aed Mon Sep 17 00:00:00 2001 From: Federico Cervelli Date: Thu, 28 May 2026 23:09:23 +0200 Subject: [PATCH 1/3] Deduplicate overlay and benchmark helpers --- README.md | 2 +- docs/api-reference.md | 2 +- scripts/benchmark-compare.ts | 9 +++-- scripts/benchmark-config.json | 7 ++++ scripts/bundle-size-check.ts | 2 +- scripts/generate-readme-docs.js | 9 +++-- src/core/Histogram.ts | 14 ++------ src/core/search.ts | 5 +++ src/ui/AxisOverlay.ts | 47 +++++++++++++------------ src/ui/Chart.ts | 7 ++-- src/ui/Crosshair.ts | 30 ++++------------ src/ui/OverlayUtils.ts | 46 +++++++++++++++++++++++- src/ui/Selection.ts | 12 ++----- src/ui/Tooltip.ts | 15 ++------ tests/browser/compare/main.ts | 5 +-- tsconfig.json | 1 + website/src/site/previews-controller.ts | 33 ++++++++--------- 17 files changed, 131 insertions(+), 115 deletions(-) create mode 100644 scripts/benchmark-config.json diff --git a/README.md b/README.md index 896f7c5..99537a9 100644 --- a/README.md +++ b/README.md @@ -217,7 +217,7 @@ Generated from `dist/` after the package build. | shared WebGL2Backend chunk | `dist/WebGL2Backend-*.js` | 22 KiB | | shared LinkedChartsCore chunk | `dist/LinkedChartsCore-*.js` | 2 KiB | | lazy screenshot chunk | `dist/screenshot-*.js` | 4 KiB | -| shared OverlayUtils chunk | `dist/OverlayUtils-*.js` | 3 KiB | +| shared OverlayUtils chunk | `dist/OverlayUtils-*.js` | 4 KiB | | shared Tooltip chunk | `dist/Tooltip-*.js` | 6 KiB | | shared Crosshair chunk | `dist/Crosshair-*.js` | 10 KiB | diff --git a/docs/api-reference.md b/docs/api-reference.md index d5d07a2..ada5fea 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -74,7 +74,7 @@ Generated from `dist/` after the package build. | shared WebGL2Backend chunk | `dist/WebGL2Backend-*.js` | 22 KiB | | shared LinkedChartsCore chunk | `dist/LinkedChartsCore-*.js` | 2 KiB | | lazy screenshot chunk | `dist/screenshot-*.js` | 4 KiB | -| shared OverlayUtils chunk | `dist/OverlayUtils-*.js` | 3 KiB | +| shared OverlayUtils chunk | `dist/OverlayUtils-*.js` | 4 KiB | | shared Tooltip chunk | `dist/Tooltip-*.js` | 6 KiB | | shared Crosshair chunk | `dist/Crosshair-*.js` | 10 KiB | diff --git a/scripts/benchmark-compare.ts b/scripts/benchmark-compare.ts index c76b219..6e1a1b6 100644 --- a/scripts/benchmark-compare.ts +++ b/scripts/benchmark-compare.ts @@ -3,6 +3,7 @@ import { existsSync } from "node:fs"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { arch, cpus, platform, release, tmpdir, totalmem } from "node:os"; import { basename, join, resolve } from "node:path"; +import officialConfig from "./benchmark-config.json"; interface Options { scenarios: string[]; @@ -143,11 +144,9 @@ interface LibraryInfo { version: string; } -const OFFICIAL_SCENARIOS = ["line-100k-static", "line-1m-static", "line-1m-pan", "line-1m-stream", "line-10m-accelerated-pan"] as const; -const OFFICIAL_LIBRARIES = ["blazeplot", "uplot", "chartjs"] as const; -const RUNTIME_COMPARISONS = [ - { primaryLibrary: "blazeplot", referenceLibrary: "uplot" }, -] as const; +const OFFICIAL_SCENARIOS = officialConfig.scenarios; +const OFFICIAL_LIBRARIES = officialConfig.libraries; +const RUNTIME_COMPARISONS = officialConfig.runtimeComparisons; interface CompareReport { schemaVersion: 1; diff --git a/scripts/benchmark-config.json b/scripts/benchmark-config.json new file mode 100644 index 0000000..c7963b9 --- /dev/null +++ b/scripts/benchmark-config.json @@ -0,0 +1,7 @@ +{ + "libraries": ["blazeplot", "uplot", "chartjs"], + "scenarios": ["line-100k-static", "line-1m-static", "line-1m-pan", "line-1m-stream", "line-10m-accelerated-pan"], + "runtimeComparisons": [ + { "primaryLibrary": "blazeplot", "referenceLibrary": "uplot" } + ] +} diff --git a/scripts/bundle-size-check.ts b/scripts/bundle-size-check.ts index 6707a94..0fa05cc 100644 --- a/scripts/bundle-size-check.ts +++ b/scripts/bundle-size-check.ts @@ -157,7 +157,7 @@ function printHelpAndExit(): never { } function displayPath(path: string): string { - return path.replace(/-[A-Za-z0-9_]+\.js$/u, "-*.js"); + return path.replace(/-[A-Za-z0-9_-]+\.js$/u, "-*.js"); } function formatBytes(bytes: number): string { diff --git a/scripts/generate-readme-docs.js b/scripts/generate-readme-docs.js index f747263..02a20e9 100644 --- a/scripts/generate-readme-docs.js +++ b/scripts/generate-readme-docs.js @@ -19,11 +19,10 @@ const docsStartMarker = ""; const docsEndMarker = ""; const performanceStartMarker = ""; const performanceEndMarker = ""; -const officialComparisonLibraries = ["blazeplot", "uplot", "chartjs"]; -const officialComparisonScenarios = ["line-100k-static", "line-1m-static", "line-1m-pan", "line-1m-stream", "line-10m-accelerated-pan"]; -const runtimeComparisonPairs = [ - { primaryLibrary: "blazeplot", referenceLibrary: "uplot" }, -]; +const officialComparisonConfig = JSON.parse(readFileSync(resolve(root, "scripts/benchmark-config.json"), "utf-8")); +const officialComparisonLibraries = officialComparisonConfig.libraries; +const officialComparisonScenarios = officialComparisonConfig.scenarios; +const runtimeComparisonPairs = officialComparisonConfig.runtimeComparisons; const args = new Set(process.argv.slice(2)); const check = args.has("--check"); diff --git a/src/core/Histogram.ts b/src/core/Histogram.ts index 7749ce0..22484d7 100644 --- a/src/core/Histogram.ts +++ b/src/core/Histogram.ts @@ -1,3 +1,4 @@ +import { upperBoundArray } from "./search.js"; import { StaticDataset } from "./StaticDataset.js"; import type { XRange, XRangeDataset } from "./types.js"; @@ -130,7 +131,7 @@ export function histogram(values: ArrayLike, options: HistogramOptions = if (binIndex < 0) binIndex = 0; if (binIndex >= counted.length) binIndex = counted.length - 1; } else { - binIndex = upperBound(edges.edges, value) - 1; + binIndex = upperBoundArray(edges.edges, value) - 1; } counted[binIndex]!.count++; @@ -362,17 +363,6 @@ function inferUniformWidth(edges: readonly number[]): number | null { return firstWidth; } -function upperBound(values: readonly number[], needle: number): number { - let low = 0; - let high = values.length; - while (low < high) { - const mid = low + ((high - low) >> 1); - if (values[mid]! <= needle) low = mid + 1; - else high = mid; - } - return low; -} - function hasExplicitEdgeThresholds(options: HistogramOptions): boolean { return Array.isArray(options.thresholds) || isReadonlyNumberArray(options.thresholds); } diff --git a/src/core/search.ts b/src/core/search.ts index 18ed333..ff09689 100644 --- a/src/core/search.ts +++ b/src/core/search.ts @@ -21,3 +21,8 @@ export function upperBound(length: number, valueAt: (index: number) => number, v } return lo; } + +/** Return the first array index whose value is greater than `value`. */ +export function upperBoundArray(values: readonly number[], value: number): number { + return upperBound(values.length, (index) => values[index]!, value); +} diff --git a/src/ui/AxisOverlay.ts b/src/ui/AxisOverlay.ts index f0599a1..2b10365 100644 --- a/src/ui/AxisOverlay.ts +++ b/src/ui/AxisOverlay.ts @@ -15,6 +15,25 @@ type RenderAxis = "x" | "y" | "y2"; const AXIS_LABEL_COLLISION_GAP_PX = 2; +interface AxisLabelInterval { + readonly el: HTMLDivElement; + readonly start: number; + readonly end: number; + readonly edge: boolean; +} + +function hideOverlappingLabels(labels: readonly AxisLabelInterval[]): void { + const placed: Array<{ start: number; end: number }> = []; + for (const label of [...labels].sort((a, b) => Number(b.edge) - Number(a.edge) || a.start - b.start)) { + const overlaps = placed.some((used) => label.start < used.end + AXIS_LABEL_COLLISION_GAP_PX && label.end > used.start - AXIS_LABEL_COLLISION_GAP_PX); + if (overlaps) { + label.el.style.display = "none"; + } else { + placed.push({ start: label.start, end: label.end }); + } + } +} + /** SVG overlay that renders chart axes and ticks. */ export class AxisOverlay { private xPool: HTMLDivElement[] = []; @@ -120,7 +139,7 @@ export class AxisOverlay { } if (axis === "x") { - const labels: Array<{ el: HTMLDivElement; left: number; right: number; edge: boolean }> = []; + const labels: AxisLabelInterval[] = []; for (let i = 0; i < values.length; i++) { const el = pool[i]!; const value = values[i]!; @@ -148,24 +167,16 @@ export class AxisOverlay { el.style.top = "auto"; el.style.bottom = "4px"; } - labels.push({ el, left: labelLeft, right: labelLeft + labelWidth, edge }); + labels.push({ el, start: labelLeft, end: labelLeft + labelWidth, edge }); } - const placed: Array<{ left: number; right: number }> = []; - for (const label of [...labels].sort((a, b) => Number(b.edge) - Number(a.edge) || a.left - b.left)) { - const overlaps = placed.some((used) => label.left < used.right + AXIS_LABEL_COLLISION_GAP_PX && label.right > used.left - AXIS_LABEL_COLLISION_GAP_PX); - if (overlaps) { - label.el.style.display = "none"; - } else { - placed.push({ left: label.left, right: label.right }); - } - } + hideOverlappingLabels(labels); return; } const isRight = axis === "y2"; const config = isRight ? this.config.y2 : this.config.y; - const labels: Array<{ el: HTMLDivElement; top: number; bottom: number; edge: boolean }> = []; + const labels: AxisLabelInterval[] = []; for (let i = 0; i < values.length; i++) { const el = pool[i]!; const value = values[i]!; @@ -193,17 +204,9 @@ export class AxisOverlay { el.style.left = isRight ? "auto" : "4px"; el.style.right = isRight ? "4px" : "auto"; } - labels.push({ el, top: labelTop, bottom: labelTop + labelHeight, edge }); + labels.push({ el, start: labelTop, end: labelTop + labelHeight, edge }); } - const placed: Array<{ top: number; bottom: number }> = []; - for (const label of [...labels].sort((a, b) => Number(b.edge) - Number(a.edge) || a.top - b.top)) { - const overlaps = placed.some((used) => label.top < used.bottom + AXIS_LABEL_COLLISION_GAP_PX && label.bottom > used.top - AXIS_LABEL_COLLISION_GAP_PX); - if (overlaps) { - label.el.style.display = "none"; - } else { - placed.push({ top: label.top, bottom: label.bottom }); - } - } + hideOverlappingLabels(labels); } } diff --git a/src/ui/Chart.ts b/src/ui/Chart.ts index 5cf2b9a..751d9e6 100644 --- a/src/ui/Chart.ts +++ b/src/ui/Chart.ts @@ -978,12 +978,15 @@ export class Chart implements ChartPluginContext { this.requestRender(); } - /** Resume latest-X following after an interaction pause. */ + /** + * Resume latest-X following after an interaction pause. + * @deprecated Use `resumeLatestXFollow()` for clearer latest-data semantics. + */ resumeXFollow(): void { this.resumeLatestXFollow(); } - /** Alias for `resumeXFollow()`. */ + /** Resume latest-X following after an interaction pause. */ resumeLatestXFollow(): void { this.clearXFollowResumeTimer(); this.xFollowPaused = false; diff --git a/src/ui/Crosshair.ts b/src/ui/Crosshair.ts index 7c9753f..ec322ee 100644 --- a/src/ui/Crosshair.ts +++ b/src/ui/Crosshair.ts @@ -1,6 +1,6 @@ import type { SeriesYAxis } from "../core/types.js"; import type { Chart, ChartPickItem, ChartPickMode, ChartPlugin, ChartPluginContext } from "./Chart.js"; -import { createLongPressTouchTracker, createPickMarker, formatCompactNumber, placeAbsoluteWithinBox, renderPickItems, rgba } from "./OverlayUtils.js"; +import { createLongPressTouchTracker, createOverlayLayer, createPickMarker, formatCompactNumber, pickAtDataX, placeAbsoluteWithinBox, renderPickItems, rgba } from "./OverlayUtils.js"; /** Axis drawn by the crosshair overlay. */ export type CrosshairAxis = "x" | "y" | "xy"; @@ -132,12 +132,12 @@ function resolvePosition(chart: ChartPluginContext, clientX: number, clientY: nu function resolveSharedPosition(chart: ChartPluginContext, dataX: number, yAxis: SeriesYAxis): CrosshairPosition | null { const rect = chart.canvas.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) return null; - const viewport = chart.getViewport(yAxis); const dataY = viewport.yMin + (viewport.yMax - viewport.yMin) * 0.5; const [plotX, plotY] = chart.dataToPlot(dataX, dataY, yAxis); - const picked = positionFromPick(chart, rect.left + plotX, rect.top + plotY, "nearest-x"); - if (picked) return picked; + const picked = pickAtDataX(chart, dataX, { yAxis, mode: "nearest-x", group: "none" }); + const item = picked?.items[0]; + if (item) return { dataX: item.x, dataY: item.y, plotX: item.plotX, plotY: item.plotY, items: [item] }; return { dataX, dataY, plotX, plotY, items: [] }; } @@ -159,17 +159,6 @@ function createXRangeHighlight(item: ChartPickItem, chart: Chart, strokeColor: s return marker; } -function createCrosshairLayer(className: string, zIndex: number): HTMLDivElement { - const layer = document.createElement("div"); - layer.className = className; - layer.style.position = "absolute"; - layer.style.inset = "0"; - layer.style.display = "none"; - layer.style.pointerEvents = "none"; - layer.style.zIndex = String(zIndex); - return layer; -} - function renderDefaultLabel( position: CrosshairPosition, container: HTMLElement, @@ -374,8 +363,8 @@ export function crosshairPlugin(options: CrosshairPluginOptions = {}): Crosshair root.style.display = "none"; root.style.pointerEvents = "none"; - lineLayer = createCrosshairLayer("blazeplot-crosshair-lines", options.zIndex ?? 0); - overlayLayer = createCrosshairLayer("blazeplot-crosshair-overlay", options.zIndex ?? 22); + lineLayer = createOverlayLayer("blazeplot-crosshair-lines", { inset: "0", zIndex: options.zIndex ?? 0 }); + overlayLayer = createOverlayLayer("blazeplot-crosshair-overlay", { inset: "0", zIndex: options.zIndex ?? 22 }); vertical = document.createElement("div"); vertical.style.position = "absolute"; @@ -393,12 +382,7 @@ export function crosshairPlugin(options: CrosshairPluginOptions = {}): Crosshair horizontal.style.borderTop = `${width} solid ${color}`; if (dash) horizontal.style.borderTopStyle = "dashed"; - markerLayer = document.createElement("div"); - markerLayer.className = "blazeplot-crosshair-markers"; - markerLayer.style.position = "absolute"; - markerLayer.style.inset = "0"; - markerLayer.style.zIndex = "2"; - markerLayer.style.pointerEvents = "none"; + markerLayer = createOverlayLayer("blazeplot-crosshair-markers", { inset: "0", display: "block", zIndex: 2 }); label = document.createElement("div"); label.style.position = "absolute"; diff --git a/src/ui/OverlayUtils.ts b/src/ui/OverlayUtils.ts index ea98938..af9d1e9 100644 --- a/src/ui/OverlayUtils.ts +++ b/src/ui/OverlayUtils.ts @@ -1,4 +1,5 @@ -import type { ChartPickItem } from "./Chart.js"; +import type { SeriesYAxis } from "../core/types.js"; +import type { ChartHoverState, ChartPickGroup, ChartPickItem, ChartPickMode, ChartPluginContext } from "./Chart.js"; /** Return the display label for a picked series item. */ export function labelOfPickItem(item: ChartPickItem): string { @@ -23,6 +24,26 @@ export function clamp(value: number, min: number, max: number): number { return Math.min(max, Math.max(min, value)); } +/** Shared absolute overlay layer options. */ +export interface OverlayLayerOptions { + readonly zIndex?: number | string; + readonly display?: string; + readonly inset?: string; + readonly pointerEvents?: string; +} + +/** Create a plot overlay layer with consistent positioning and pointer behavior. */ +export function createOverlayLayer(className: string, options: OverlayLayerOptions = {}): HTMLDivElement { + const layer = document.createElement("div"); + layer.className = className; + layer.style.position = "absolute"; + if (options.inset !== undefined) layer.style.inset = options.inset; + layer.style.display = options.display ?? "none"; + layer.style.pointerEvents = options.pointerEvents ?? "none"; + if (options.zIndex !== undefined) layer.style.zIndex = String(options.zIndex); + return layer; +} + /** Position a fixed element near a client point while keeping it onscreen. */ export function placeFixedWithinViewport( element: HTMLElement, @@ -82,6 +103,29 @@ export interface PickMarkerOptions { readonly strokeWidthPx?: number; } +/** Options for picking chart items at a data-X value. */ +export interface PickAtDataXOptions { + readonly yAxis?: SeriesYAxis; + readonly mode?: ChartPickMode; + readonly group?: ChartPickGroup; + readonly maxDistancePx?: number; +} + +/** Pick chart items at a data-X value using midpoint Y for pointer-based pick APIs. */ +export function pickAtDataX(chart: ChartPluginContext, dataX: number, options: PickAtDataXOptions = {}): ChartHoverState | null { + const yAxis = options.yAxis ?? "left"; + const rect = chart.canvas.getBoundingClientRect(); + if (rect.width <= 0 || rect.height <= 0) return null; + const viewport = chart.getViewport(yAxis); + const dataY = viewport.yMin + (viewport.yMax - viewport.yMin) * 0.5; + const [plotX, plotY] = chart.dataToPlot(dataX, dataY, yAxis); + return chart.pick(rect.left + plotX, rect.top + plotY, { + mode: options.mode ?? "nearest-x", + group: options.group ?? "x", + maxDistancePx: options.maxDistancePx, + }); +} + /** Create a marker element for a picked series point. */ export function createPickMarker(item: ChartPickItem, options: PickMarkerOptions = {}): HTMLDivElement { const marker = document.createElement("div"); diff --git a/src/ui/Selection.ts b/src/ui/Selection.ts index 80aa070..a0afe66 100644 --- a/src/ui/Selection.ts +++ b/src/ui/Selection.ts @@ -1,6 +1,7 @@ import type { SeriesSample, SeriesYAxis, Viewport } from "../core/types.js"; import type { SeriesStore } from "../core/SeriesStore.js"; import type { ChartPlugin, ChartPluginContext, ChartSeriesState } from "./Chart.js"; +import { clamp, createOverlayLayer } from "./OverlayUtils.js"; /** Geometry captured by the selection plugin. */ export type SelectionMode = "x-range" | "y-range" | "xy"; @@ -90,10 +91,6 @@ interface DragState { const DEFAULT_FILL = "rgba(59, 130, 246, 0.16)"; const DEFAULT_STROKE = "rgba(147, 197, 253, 0.95)"; -function clamp(value: number, min: number, max: number): number { - return Math.max(min, Math.min(value, max)); -} - function normalizeBounds(a: [number, number], b: [number, number], current: Viewport, mode: SelectionMode): SelectionBounds { const xMin = Math.min(a[0], b[0]); const xMax = Math.max(a[0], b[0]); @@ -233,12 +230,7 @@ export function selectionPlugin(options: SelectionPluginOptions = {}): Selection install(chart: ChartPluginContext) { chartRef = chart; const canvas = chart.canvas; - overlay = document.createElement("div"); - overlay.className = options.className ?? "blazeplot-selection-brush"; - overlay.style.position = "absolute"; - overlay.style.display = "none"; - overlay.style.pointerEvents = "none"; - overlay.style.zIndex = String(options.zIndex ?? 26); + overlay = createOverlayLayer(options.className ?? "blazeplot-selection-brush", { zIndex: options.zIndex ?? 26 }); overlay.style.border = `1px solid ${options.stroke ?? DEFAULT_STROKE}`; overlay.style.background = options.fill ?? DEFAULT_FILL; chart.plotElement.appendChild(overlay); diff --git a/src/ui/Tooltip.ts b/src/ui/Tooltip.ts index 95d83e5..fe211ba 100644 --- a/src/ui/Tooltip.ts +++ b/src/ui/Tooltip.ts @@ -1,5 +1,5 @@ import type { Chart, ChartHoverState, ChartPickGroup, ChartPickItem, ChartPickMode, ChartPlugin, ChartPluginContext } from "./Chart.js"; -import { clamp, createLongPressTouchTracker, createPickMarker, formatCompactNumber, rgba, renderPickItems } from "./OverlayUtils.js"; +import { clamp, createLongPressTouchTracker, createOverlayLayer, createPickMarker, formatCompactNumber, pickAtDataX, rgba, renderPickItems } from "./OverlayUtils.js"; /** Options for the built-in hover tooltip plugin. */ export interface TooltipPluginOptions { @@ -82,12 +82,7 @@ export function tooltipPlugin(options: TooltipPluginOptions = {}): ChartPlugin { const tooltipParent = chart.rootElement.ownerDocument.body ?? chart.rootElement; tooltipParent.appendChild(container); - const markerLayer = document.createElement("div"); - markerLayer.className = "blazeplot-tooltip-markers"; - markerLayer.style.position = "absolute"; - markerLayer.style.inset = "0"; - markerLayer.style.zIndex = "25"; - markerLayer.style.pointerEvents = "none"; + const markerLayer = createOverlayLayer("blazeplot-tooltip-markers", { inset: "0", display: "block", zIndex: 25 }); chart.plotElement.appendChild(markerLayer); let lockedTooltipWidth = 0; @@ -171,11 +166,7 @@ export function tooltipPlugin(options: TooltipPluginOptions = {}): ChartPlugin { }; const renderSharedAtX = (dataX: number): void => { - const viewport = chart.getViewport(); - const dataY = viewport.yMin + (viewport.yMax - viewport.yMin) * 0.5; - const [plotX, plotY] = chart.dataToPlot(dataX, dataY); - const rect = chart.canvas.getBoundingClientRect(); - render(chart.pick(rect.left + plotX, rect.top + plotY, { + render(pickAtDataX(chart, dataX, { mode: options.mode ?? "nearest-x", group: options.group ?? "x", maxDistancePx: options.maxDistancePx, diff --git a/tests/browser/compare/main.ts b/tests/browser/compare/main.ts index 6f6c00b..ef6678e 100644 --- a/tests/browser/compare/main.ts +++ b/tests/browser/compare/main.ts @@ -14,6 +14,7 @@ import uPlot from "uplot"; import "uplot/dist/uPlot.min.css"; import { Chart, StaticDataset } from "@/index.ts"; import type { AcceleratedDataset, Dataset, MinMaxSegmentLayout, SampleCopyLayout, SeriesStore, TimeRange, Viewport } from "@/index.ts"; +import officialConfig from "../../../scripts/benchmark-config.json"; ChartJs.register(LineController, LineElement, PointElement, LinearScale, Decimation, Legend, Tooltip); @@ -154,8 +155,8 @@ interface BenchmarkController { }; } -const DEFAULT_LIBRARIES: readonly LibraryId[] = ["blazeplot", "uplot", "chartjs"]; -const DEFAULT_SCENARIOS = ["line-100k-static", "line-1m-static", "line-1m-pan", "line-1m-stream", "line-10m-accelerated-pan"] as const; +const DEFAULT_LIBRARIES = officialConfig.libraries as readonly LibraryId[]; +const DEFAULT_SCENARIOS = officialConfig.scenarios; const SCENARIOS: Record = { "line-100k-static": { name: "line-100k-static", diff --git a/tsconfig.json b/tsconfig.json index 64e4835..506f517 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ "jsx": "react-jsx", "allowJs": false, "allowImportingTsExtensions": true, + "resolveJsonModule": true, "verbatimModuleSyntax": true, "noEmit": true, diff --git a/website/src/site/previews-controller.ts b/website/src/site/previews-controller.ts index 57d8b4b..bd6bf63 100644 --- a/website/src/site/previews-controller.ts +++ b/website/src/site/previews-controller.ts @@ -38,6 +38,17 @@ export class PreviewChartsController implements ReactiveController { this.disposePreviewCharts(); } + private requireControl(root: ParentNode, selector: string): T { + const control = root.querySelector(selector); + if (!control) throw new Error(`Missing preview control: ${selector}`); + return control; + } + + private addDisposableListener(element: HTMLElement, type: K, listener: (event: HTMLElementEventMap[K]) => void): void { + element.addEventListener(type, listener as EventListener); + this.previewDisposers.push(() => element.removeEventListener(type, listener as EventListener)); + } + private get previewIndex(): number { const index = PREVIEWS.findIndex((preview) => preview.id === this.host.previewId); return index >= 0 ? index : 0; @@ -195,11 +206,7 @@ export class PreviewChartsController implements ReactiveController { private mountLivePreviewChart(target: HTMLElement): void { const liveRoot = target.closest("[data-live-preview-root]") ?? target; - const requireControl = (selector: string): T => { - const element = liveRoot.querySelector(selector); - if (!element) throw new Error(`Missing live preview control ${selector}`); - return element; - }; + const requireControl = (selector: string): T => this.requireControl(liveRoot, selector); const overlay = requireControl("[data-live-overlay]"); const overlayText = requireControl("[data-live-overlay-text]"); const copyIcon = requireControl("[data-live-copy]"); @@ -331,10 +338,7 @@ export class PreviewChartsController implements ReactiveController { dataWorker.addEventListener("message", onWorkerMessage); this.previewDisposers.push(() => dataWorker.removeEventListener("message", onWorkerMessage)); - const addListener = (element: HTMLElement, type: K, listener: (event: HTMLElementEventMap[K]) => void): void => { - element.addEventListener(type, listener as EventListener); - this.previewDisposers.push(() => element.removeEventListener(type, listener as EventListener)); - }; + const addListener = (element: HTMLElement, type: K, listener: (event: HTMLElementEventMap[K]) => void): void => this.addDisposableListener(element, type, listener); const historySamples = (): number => Math.max(1, viewSamples); const sparseHistoryCapacity = (): number => Math.ceil(historySamples() / SPARSE_INTERVAL) + 2; @@ -796,11 +800,7 @@ export class PreviewChartsController implements ReactiveController { const root = target.closest("[data-server-sampled-root]"); if (!root) throw new Error("Missing server sampled root"); - const requireControl = (selector: string): T => { - const element = root.querySelector(selector); - if (!element) throw new Error(`Missing server sampled control ${selector}`); - return element; - }; + const requireControl = (selector: string): T => this.requireControl(root, selector); const sampledChartEl = requireControl("[data-server-sampled-chart]"); const liveChartEl = requireControl("[data-server-live-chart]"); @@ -888,10 +888,7 @@ export class PreviewChartsController implements ReactiveController { liveChart.plotElement.appendChild(candleHighlightOverlay); this.previewDisposers.push(() => candleHighlightOverlay.remove()); - const addListener = (element: HTMLElement, type: K, listener: (event: HTMLElementEventMap[K]) => void): void => { - element.addEventListener(type, listener as EventListener); - this.previewDisposers.push(() => element.removeEventListener(type, listener as EventListener)); - }; + const addListener = (element: HTMLElement, type: K, listener: (event: HTMLElementEventMap[K]) => void): void => this.addDisposableListener(element, type, listener); const loadKlines = async (): Promise => { const symbol = symbolSelect.value; From 68abb24d3caaa160c7b67a767e011bc29eb07787 Mon Sep 17 00:00:00 2001 From: Federico Cervelli Date: Thu, 28 May 2026 23:31:18 +0200 Subject: [PATCH 2/3] chore: website fixups --- website/src/blazeplot-site.ts | 2 +- website/src/site/components/docs-page.ts | 93 +++++++++++++++----- website/src/site/components/previews-page.ts | 63 ++++++++++++- website/src/site/components/site-topbar.ts | 46 +++++++--- 4 files changed, 167 insertions(+), 37 deletions(-) diff --git a/website/src/blazeplot-site.ts b/website/src/blazeplot-site.ts index 44a850c..bd7dc5d 100644 --- a/website/src/blazeplot-site.ts +++ b/website/src/blazeplot-site.ts @@ -43,7 +43,7 @@ export class BlazeplotSite extends LitElement { const doc = getDocPage(this.docSlug) ?? DOC_PAGES[0]!; return html`
- +
${this.section === "home" ? html`` : nothing} ${this.section === "docs" ? html`` : nothing} diff --git a/website/src/site/components/docs-page.ts b/website/src/site/components/docs-page.ts index bee3785..6470a5f 100644 --- a/website/src/site/components/docs-page.ts +++ b/website/src/site/components/docs-page.ts @@ -19,25 +19,39 @@ export class BlazeplotDocsPage extends LitElement { static override styles = siteStyles; static override properties = { doc: { attribute: false }, + docsNavOpen: { state: true }, }; declare doc: DocPage; + declare private docsNavOpen: boolean; private docCharts: Chart[] = []; constructor() { super(); this.doc = DOC_PAGES[0]!; + this.docsNavOpen = false; } private docDisposers: Array<() => void> = []; private mountedDocSlug: string | null = null; + override connectedCallback(): void { + super.connectedCallback(); + window.addEventListener("blazeplot-docs-nav-toggle", this.toggleDocsNav); + window.addEventListener("keydown", this.onKeyDown); + } + override disconnectedCallback(): void { + window.removeEventListener("blazeplot-docs-nav-toggle", this.toggleDocsNav); + window.removeEventListener("keydown", this.onKeyDown); this.disposeDocCharts(); super.disconnectedCallback(); } override updated(changedProperties: PropertyValues): void { - if (changedProperties.has("doc")) this.disposeDocCharts(); + if (changedProperties.has("doc")) { + this.docsNavOpen = false; + this.disposeDocCharts(); + } this.mountDocCharts(this.doc); } @@ -49,28 +63,30 @@ export class BlazeplotDocsPage extends LitElement { all docs source
-
-