// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ // ┃ Copyright (c) 2017, the Perspective Authors. ┃ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ // ┃ This file is part of the Perspective library, distributed under the terms ┃ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import type { View } from "@perspective-dev/client"; import type { ColumnDataMap } from "../data/view-reader"; import type { WebGLContextManager } from "../webgl/context-manager"; import type { ZoomController } from "../interaction/zoom-controller"; import type { HostSink } from "../interaction/tooltip-controller"; export interface ChartImplementation { uploadAndRender( glManager: WebGLContextManager, columns: ColumnDataMap, startRow: number, endRow: number, ): void; /** * The single render entrypoint. Every render-triggering caller — * upload chunks, zoom / pan, resize, theme invalidation, * host-driven redraws — calls this. Routes through the * module-level scheduler ([render/scheduler.ts]) so concurrent * calls collapse to one `_fullRender` per RAF and the host * blitter receives one bitmap per frame per chart. * * The returned promise resolves after this entry's `_fullRender` * + `awaitGpuFence` + `endFrame` chain completes — independent * of other charts in the same RAF, which run their fence waits * in parallel. * * The synchronous-render bypass for `snapshotPng` (calls * `_fullRender` directly, skips `endFrame`) is the only * sanctioned exception and lives inside the worker renderer. */ requestRender(glManager: WebGLContextManager): Promise; /** * The chart-specific frame builder. The scheduler wraps this with * fence + `endFrame`; callers must not invoke it directly except * for `snapshotPng`, which needs an intact GL backbuffer for * `gl.readPixels` and so must skip the `endFrame` pair. */ _fullRender(glManager: WebGLContextManager): void; /** * Hand the current View to the chart so it can make on-demand * per-row queries (for lazy tooltip column lookups). Called on * every `draw`; the chart disposes any prior fetcher and clears * dependent UI (pinned tooltip) so stale rows never surface. * * TODO: pinned tooltips are dismissed on view update today. A * future enhancement is to keep a pinned tooltip visible (with its * captured data) until the user dismisses it, even after the * underlying view no longer contains that row. */ setView?(view: View): void; /** * Set the gridline canvas (behind WebGL, for gridlines). */ setGridlineCanvas?(canvas: HTMLCanvasElement | OffscreenCanvas): void; /** * Set the chrome canvas (above WebGL, for axes/labels/legend/tooltip). */ setChromeCanvas?(canvas: HTMLCanvasElement | OffscreenCanvas): void; /** * Hand the chart a pre-computed CSS-variable map produced on the * main thread via `snapshotThemeVars(el)`, which it can decode into * a full `Theme` without touching the DOM (charts always run inside * the renderer scope, which has no `getComputedStyle`). */ setTheme?(vars: Record): void; /** * Set the zoom controller for interactive zoom/pan. */ setZoomController?(zc: ZoomController): void; /** * Wire the chart's `TooltipController` for virtual-dispatch hover / * click events forwarded from the host. The renderer drives * `dispatchHover` / `dispatchLeave` / `dispatchClick` / * `dispatchDblClick` from `InteractionEvent`s; the supplied * `HostSink` posts pin / dismiss / setCursor intents back to the * host so the resulting DOM mutations happen there (the renderer * scope has no DOM in worker mode, and uses the same channel * in-process for symmetry). */ attachTooltip?(host: HostSink): void; /** * Set the column slot config (with nulls for empty slots). */ setColumnSlots?(slots: (string | null)[]): void; /** * Set group_by and split_by config from the viewer. */ setViewPivots?(groupBy: string[], splitBy: string[]): void; /** * Set column type schema from the view (e.g., { "col": "date" }). */ setColumnTypes?(schema: Record): void; /** * Set the source-column types used for `group_by` level lookups — * sourced from `table.schema()` + `view.expression_schema()`. Used * by categorical-axis charts to detect numeric / date / boolean * group_by levels (which are not keyed in `view.schema()` because * they surface as `__ROW_PATH_N__` columns). */ setGroupByTypes?(schema: Record): void; /** * Set per-column render config (the second argument to `plugin.restore`). * Key is the aggregate base name; value is an open object whose * `chart_type` / `stack` fields are consumed by the Y-bar glyph router. */ setColumnsConfig?(cfg: Record): void; /** * Set the plugin's default glyph type. Used by the Y-series chart * family (Y Bar / Y Line / Y Scatter / Y Area): each tag is the same * `BarChart` impl with a different starting `chart_type` applied to * columns that lack an explicit entry in `columns_config`. */ setDefaultChartType?(chartType: string): void; /** * Set the faceting config: one small-multiple sub-plot per * `split_by` group, optional shared axes, coordinated tooltip, and * zoom routing mode. Seeded from `DEFAULT_FACET_CONFIG` at init; * `plugin_config.facet_mode` / `facet_zoom_mode` override the * matching fields via `AbstractChart.setPluginConfig`. */ setFacetConfig?(cfg: FacetConfig): void; /** * Set the plugin-scoped global configuration — the values backing * `plugin_config_schema` / `plugin_config` in `restore`. Replaces * the previous module-level constants (`LINE_WIDTH_PX`, * `POINT_SIZE_PX`, `BAND_INNER_FRAC`, `BAR_INNER_PAD`, * `WICK_WIDTH_PX`, `OHLC_LINE_WIDTH_PX`, `AUTO_ALT_Y_AXIS`) plus * the faceted/series zoom-mode semantics described in * {@link PluginConfig}. */ setPluginConfig?(cfg: PluginConfig): void; /** * Drop any cached theme values so the next render re-reads CSS * variables. Driven from `plugin.restyle()`. */ invalidateTheme?(): void; /** * Clear the `domain_mode: "expand"` accumulator state so the next * data load starts from the current data extent. Driven from * `resetAllZooms` (the user clicked "Reset Zoom"). View-config * mutations route through `AbstractChart`'s `setColumnSlots` / * `setViewPivots` / `setColumnTypes` setters, which call the same * hook internally. */ resetExpandedDomain?(): void; destroy(): void; } export interface FacetConfig { /** * "grid" = small multiples (default); "overlay" = legacy single-plot. */ facet_mode: "grid" | "overlay"; /** * Share one bottom X axis across all columns of facets. */ shared_x_axis: boolean; /** * Share one left Y axis across all rows of facets. */ shared_y_axis: boolean; /** * Paint a tooltip in every facet (otherwise only the source facet). */ coordinated_tooltip: boolean; /** * "shared" = one viewport for all facets; "independent" = per-facet. */ zoom_mode: "shared" | "independent"; /** * Pixel gap between adjacent facet cells in grid mode. */ facet_padding: number; } export const DEFAULT_FACET_CONFIG: FacetConfig = { facet_mode: "grid", shared_x_axis: true, shared_y_axis: true, coordinated_tooltip: false, zoom_mode: "shared", facet_padding: 6, }; /** * Plugin-scoped global configuration — the user-facing settings backing * `plugin_config_schema()` / the `plugin_config` slot in `restore`. * * Each chart type's `plugin_config_schema` returns only the fields that * are applicable for that chart (see `applicable_plugin_fields` on * `ChartTypeConfig`); inapplicable fields are hidden in the UI. The * chart impl receives the full struct on `setPluginConfig` and reads * only the fields its render / build pipeline cares about. * * Some fields overlap with {@link FacetConfig} (`facet_mode`, * `facet_zoom_mode`); the base `AbstractChart.setPluginConfig` syncs * those onto `_facetConfig` so deep render code keeps reading the * single facet struct it already does. `series_zoom_mode` toggles the * categorical-Y chart base's `_autoFitValue` flag. */ export interface PluginConfig { /** * Auto-detect Y dual-axis splits when aggregate magnitudes differ * by more than `DUAL_Y_RATIO_THRESHOLD`×. Series charts only. * Replaces the `AUTO_ALT_Y_AXIS` compile-time toggle. */ auto_alt_y_axis: boolean; /** * Faceting strategy when `split_by` is non-empty. * * - `"grid"` — one small-multiple sub-plot per split group. * - `"overlay"` — single plot with split groups differentiated by * color. Synced into `_facetConfig.facet_mode`. */ facet_mode: "grid" | "overlay"; /** * Faceted-cartesian zoom routing. `"shared"` — one viewport across * all facets; `"independent"` — wheel/pan routes to the facet under * the cursor with its own viewport. Synced into * `_facetConfig.zoom_mode`. */ facet_zoom_mode: "shared" | "independent"; /** * Series-chart value-axis behavior on zoom. * * - `"dynamic"` — value axis refits to the visible categorical * slice (current default; `CategoricalYChart._autoFitValue` = * true). * - `"fixed"` — value axis stays pinned to the full-data extent. */ series_zoom_mode: "fixed" | "dynamic"; /** * Anchor the value axis to zero — when true, `0` is forced into * the rendered domain even if all data sits well above or below * it. Natural for bar / area glyphs (which grow from the zero * baseline) and surprising for line / scatter (where the * interesting variation often lives far from zero). Per-chart-type * defaults route through `ChartTypeConfig.plugin_field_defaults`: * `true` for Y Bar / Y Area / X Bar, `false` elsewhere. */ include_zero: boolean; /** * Domain accumulation policy across successive `View` updates. * * - `"fit"` — every update recomputes the rendered domain (and on * cartesian charts, the X/Y range and color/size scales) from * the current data extent. Can grow or shrink frame-to-frame. * - `"expand"` — the rendered domain monotonically *grows*: each * update unions the new data extent with the previously rendered * extent, so once a value is in scope it stays in scope. Reset * by the "Reset Zoom" button, view-config changes (group_by / * split_by / column-slot / column-type), or toggling back to * `"fit"`. */ domain_mode: "fit" | "expand"; /** * Width of polyline glyphs in CSS pixels (multiplied by DPR at GL * upload). Replaces the duplicated `LINE_WIDTH_PX` constants in * the cartesian + series line glyphs. */ line_width_px: number; /** * Diameter of scatter point glyphs in CSS pixels. Replaces * `POINT_SIZE_PX`. */ point_size_px: number; /** * Fraction of each category band occupied by its slot(s). Replaces * `BAND_INNER_FRAC`. Affects buffer contents — takes effect on * next data load. */ band_inner_frac: number; /** * Relative inner padding between adjacent slots within a band. * Replaces `BAR_INNER_PAD`. Affects buffer contents — takes effect * on next data load. */ bar_inner_pad: number; /** * Candlestick wick stroke width in CSS pixels. Replaces * `WICK_WIDTH_PX`. */ wick_width_px: number; /** * OHLC bar stroke width in CSS pixels. Replaces * `OHLC_LINE_WIDTH_PX`. */ ohlc_line_width_px: number; /** * density splat radius in CSS pixels. Each data point is * rasterized as a soft disk of this radius into the accumulation * FBO before the gradient LUT pass resolves to a heat color. */ gradient_radius_px: number; /** * density per-splat intensity multiplier. Controls how * fast the density grows when points overlap (low values produce * a smoother, more diffuse field; high values produce sharper * peaks). */ gradient_intensity: number; /** * density clamp on the maximum accumulated heat used for * the gradient-LUT lookup. Lower values saturate sooner (more of * the LUT's hot stops show up); higher values stay cooler. In * every mode this controls the alpha (intensity) ramp; in * `density` mode it also drives the hue, and in `signed` mode it * scales the signed-sum-to-hue mapping. */ gradient_heat_max: number; /** * density color-reduction mode. Controls how each pixel's * stack of overlapping splats is reduced to a single LUT-t / alpha * pair in the resolve pass. * * - `mean` (default) — hue is the density-weighted average of * per-point color-t. Reads as "the typical color-column value * in this region." Uses robust (5th/95th-percentile) bounds so * one outlier can't compress the rest of the data toward the * gradient midpoint. * - `density` — ignore the color column even when wired; hue and * alpha both follow density. Reads as "where do points cluster." * Useful when the color column is attached for tooltip lookup * only. * - `extreme` — keep the per-pixel maximum signed deviation of * `t - 0.5` (split into positive and negative channels, MAX- * blended). Reads as "where are the outliers." Density still * drives alpha so a single-point extreme fades naturally. * Requires a second framebuffer; uses MRT on WebGL2 hardware * with `OES_draw_buffers_indexed`, two passes otherwise. * - `signed` — accumulate signed `t - 0.5` and let positive vs * negative cancel out. Reads as "net positive vs net negative * in each region." Requires a float-capable framebuffer; on * `UNSIGNED_BYTE` fallback hardware degrades silently to * `mean` with a one-line console warning. */ gradient_color_mode: "mean" | "density" | "extreme" | "signed"; /** * Map basemap tile provider. Applies only to map plugin tags * (`map-scatter`, `map-line`, `map-density`). Cartesian charts * ignore the field. Surfaced as an enum on the settings panel so * users can switch light/dark/voyager without writing custom * tile-source code. */ map_tile_provider: "carto-positron" | "carto-dark-matter" | "carto-voyager"; /** * Map basemap alpha (0..1). Pre-multiplied into the tile fragment * shader's output so the chart's glyph layer composites over a * dimmer or brighter version of the basemap. `1.0` (default) * shows the tiles at full opacity. */ map_tile_alpha: number; } export const DEFAULT_PLUGIN_CONFIG: PluginConfig = { auto_alt_y_axis: false, facet_mode: "grid", facet_zoom_mode: "shared", series_zoom_mode: "dynamic", include_zero: false, domain_mode: "expand", line_width_px: 2.0, point_size_px: 8.0, band_inner_frac: 0.5, bar_inner_pad: 0.1, wick_width_px: 1.0, ohlc_line_width_px: 1.0, gradient_radius_px: 32.0, gradient_intensity: 0.6, gradient_heat_max: 4.0, gradient_color_mode: "mean", map_tile_provider: "carto-positron", map_tile_alpha: 1.0, };