@perspective-dev/viewer-charts 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +193 -0
- package/dist/cdn/perspective-viewer-charts.js +3 -0
- package/dist/cdn/perspective-viewer-charts.js.map +7 -0
- package/dist/esm/axis/axis-primitives.d.ts +24 -0
- package/dist/esm/axis/bar-axis.d.ts +51 -0
- package/dist/esm/axis/canvas.d.ts +24 -0
- package/dist/esm/axis/categorical-axis-core.d.ts +42 -0
- package/dist/esm/axis/categorical-axis.d.ts +27 -0
- package/dist/esm/axis/facet-chrome.d.ts +13 -0
- package/dist/esm/axis/label-geometry.d.ts +41 -0
- package/dist/esm/axis/legend.d.ts +44 -0
- package/dist/esm/axis/numeric-axis.d.ts +20 -0
- package/dist/esm/charts/candlestick/candlestick-build.d.ts +129 -0
- package/dist/esm/charts/candlestick/candlestick-interact.d.ts +10 -0
- package/dist/esm/charts/candlestick/candlestick-render.d.ts +24 -0
- package/dist/esm/charts/candlestick/candlestick.d.ts +144 -0
- package/dist/esm/charts/candlestick/glyphs/draw-candlesticks.d.ts +36 -0
- package/dist/esm/charts/candlestick/glyphs/draw-ohlc.d.ts +33 -0
- package/dist/esm/charts/canvas-types.d.ts +15 -0
- package/dist/esm/charts/cartesian/cartesian-build.d.ts +14 -0
- package/dist/esm/charts/cartesian/cartesian-interact.d.ts +20 -0
- package/dist/esm/charts/cartesian/cartesian-render.d.ts +26 -0
- package/dist/esm/charts/cartesian/cartesian.d.ts +239 -0
- package/dist/esm/charts/cartesian/glyph.d.ts +53 -0
- package/dist/esm/charts/cartesian/glyphs/density.d.ts +142 -0
- package/dist/esm/charts/cartesian/glyphs/lines.d.ts +23 -0
- package/dist/esm/charts/cartesian/glyphs/points.d.ts +24 -0
- package/dist/esm/charts/cartesian/label-interner.d.ts +21 -0
- package/dist/esm/charts/cartesian/tooltip-lines.d.ts +11 -0
- package/dist/esm/charts/chart-base.d.ts +402 -0
- package/dist/esm/charts/chart.d.ts +338 -0
- package/dist/esm/charts/common/band-layout.d.ts +32 -0
- package/dist/esm/charts/common/categorical-y-chart.d.ts +53 -0
- package/dist/esm/charts/common/category-axis-resolver.d.ts +90 -0
- package/dist/esm/charts/common/chrome-cache.d.ts +18 -0
- package/dist/esm/charts/common/draw-tooltip-box.d.ts +9 -0
- package/dist/esm/charts/common/leaf-color.d.ts +33 -0
- package/dist/esm/charts/common/node-store.d.ts +81 -0
- package/dist/esm/charts/common/tree-chart.d.ts +48 -0
- package/dist/esm/charts/common/tree-chrome.d.ts +31 -0
- package/dist/esm/charts/common/tree-data.d.ts +54 -0
- package/dist/esm/charts/common/visible-extent.d.ts +51 -0
- package/dist/esm/charts/heatmap/heatmap-build.d.ts +86 -0
- package/dist/esm/charts/heatmap/heatmap-interact.d.ts +19 -0
- package/dist/esm/charts/heatmap/heatmap-render.d.ts +19 -0
- package/dist/esm/charts/heatmap/heatmap-y-axis.d.ts +46 -0
- package/dist/esm/charts/heatmap/heatmap.d.ts +117 -0
- package/dist/esm/charts/map/map.d.ts +67 -0
- package/dist/esm/charts/registry.d.ts +14 -0
- package/dist/esm/charts/series/glyphs/draw-areas.d.ts +30 -0
- package/dist/esm/charts/series/glyphs/draw-bars.d.ts +15 -0
- package/dist/esm/charts/series/glyphs/draw-lines.d.ts +34 -0
- package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +33 -0
- package/dist/esm/charts/series/series-build.d.ts +228 -0
- package/dist/esm/charts/series/series-interact.d.ts +35 -0
- package/dist/esm/charts/series/series-render.d.ts +41 -0
- package/dist/esm/charts/series/series-type.d.ts +49 -0
- package/dist/esm/charts/series/series.d.ts +317 -0
- package/dist/esm/charts/sunburst/sunburst-interact.d.ts +7 -0
- package/dist/esm/charts/sunburst/sunburst-layout.d.ts +33 -0
- package/dist/esm/charts/sunburst/sunburst-render.d.ts +22 -0
- package/dist/esm/charts/sunburst/sunburst.d.ts +85 -0
- package/dist/esm/charts/treemap/treemap-interact.d.ts +12 -0
- package/dist/esm/charts/treemap/treemap-layout.d.ts +28 -0
- package/dist/esm/charts/treemap/treemap-render.d.ts +18 -0
- package/dist/esm/charts/treemap/treemap.d.ts +74 -0
- package/dist/esm/config.d.ts +27 -0
- package/dist/esm/data/lazy-row.d.ts +32 -0
- package/dist/esm/data/split-groups.d.ts +20 -0
- package/dist/esm/data/view-reader.d.ts +35 -0
- package/dist/esm/event-detail.d.ts +28 -0
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/interaction/hit-test.d.ts +30 -0
- package/dist/esm/interaction/host-sink-dom.d.ts +19 -0
- package/dist/esm/interaction/host-sink-message.d.ts +46 -0
- package/dist/esm/interaction/lazy-tooltip.d.ts +61 -0
- package/dist/esm/interaction/raw-event-forwarder.d.ts +27 -0
- package/dist/esm/interaction/spatial-grid.d.ts +15 -0
- package/dist/esm/interaction/tooltip-controller.d.ts +193 -0
- package/dist/esm/interaction/zoom-controller.d.ts +106 -0
- package/dist/esm/interaction/zoom-router.d.ts +48 -0
- package/dist/esm/layout/facet-grid.d.ts +126 -0
- package/dist/esm/layout/plot-layout.d.ts +104 -0
- package/dist/esm/layout/ticks.d.ts +17 -0
- package/dist/esm/map/mercator.d.ts +102 -0
- package/dist/esm/map/tile-cache.d.ts +38 -0
- package/dist/esm/map/tile-layer.d.ts +66 -0
- package/dist/esm/map/tile-loader.d.ts +52 -0
- package/dist/esm/map/tile-source.d.ts +66 -0
- package/dist/esm/perspective-viewer-charts.js +3 -0
- package/dist/esm/perspective-viewer-charts.js.map +7 -0
- package/dist/esm/plugin/charts.d.ts +40 -0
- package/dist/esm/plugin/plugin.d.ts +95 -0
- package/dist/esm/render/scheduler.d.ts +41 -0
- package/dist/esm/theme/gradient.d.ts +48 -0
- package/dist/esm/theme/palette.d.ts +13 -0
- package/dist/esm/theme/theme-snapshot.d.ts +7 -0
- package/dist/esm/theme/theme.d.ts +53 -0
- package/dist/esm/transport/protocol.d.ts +430 -0
- package/dist/esm/transport/renderer-transport.d.ts +201 -0
- package/dist/esm/utils/css.d.ts +1 -0
- package/dist/esm/utils/font-snapshot.d.ts +50 -0
- package/dist/esm/webgl/buffer-pool.d.ts +62 -0
- package/dist/esm/webgl/context-manager.d.ts +184 -0
- package/dist/esm/webgl/gradient-texture.d.ts +17 -0
- package/dist/esm/webgl/instanced-attrs.d.ts +44 -0
- package/dist/esm/webgl/plot-frame.d.ts +39 -0
- package/dist/esm/webgl/program-cache.d.ts +13 -0
- package/dist/esm/webgl/shader-manifest.d.ts +53 -0
- package/dist/esm/webgl/shader-registry.d.ts +22 -0
- package/dist/esm/worker/boot.d.ts +0 -0
- package/dist/esm/worker/dispatch.d.ts +9 -0
- package/dist/esm/worker/font-loader.d.ts +2 -0
- package/dist/esm/worker/renderer.worker.d.ts +115 -0
- package/dist/esm/worker/session-host.d.ts +26 -0
- package/package.json +47 -0
- package/src/css/perspective-viewer-charts.css +95 -0
- package/src/ts/axis/axis-primitives.ts +125 -0
- package/src/ts/axis/bar-axis.ts +345 -0
- package/src/ts/axis/canvas.ts +64 -0
- package/src/ts/axis/categorical-axis-core.ts +125 -0
- package/src/ts/axis/categorical-axis.ts +716 -0
- package/src/ts/axis/facet-chrome.ts +42 -0
- package/src/ts/axis/label-geometry.ts +188 -0
- package/src/ts/axis/legend.ts +218 -0
- package/src/ts/axis/numeric-axis.ts +353 -0
- package/src/ts/charts/candlestick/candlestick-build.ts +516 -0
- package/src/ts/charts/candlestick/candlestick-interact.ts +256 -0
- package/src/ts/charts/candlestick/candlestick-render.ts +387 -0
- package/src/ts/charts/candlestick/candlestick.ts +367 -0
- package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +432 -0
- package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +317 -0
- package/src/ts/charts/canvas-types.ts +30 -0
- package/src/ts/charts/cartesian/cartesian-build.ts +616 -0
- package/src/ts/charts/cartesian/cartesian-interact.ts +355 -0
- package/src/ts/charts/cartesian/cartesian-render.ts +948 -0
- package/src/ts/charts/cartesian/cartesian.ts +469 -0
- package/src/ts/charts/cartesian/glyph.ts +81 -0
- package/src/ts/charts/cartesian/glyphs/density.ts +1263 -0
- package/src/ts/charts/cartesian/glyphs/lines.ts +320 -0
- package/src/ts/charts/cartesian/glyphs/points.ts +239 -0
- package/src/ts/charts/cartesian/label-interner.ts +56 -0
- package/src/ts/charts/cartesian/tooltip-lines.ts +80 -0
- package/src/ts/charts/chart-base.ts +840 -0
- package/src/ts/charts/chart.ts +427 -0
- package/src/ts/charts/common/band-layout.ts +63 -0
- package/src/ts/charts/common/categorical-y-chart.ts +81 -0
- package/src/ts/charts/common/category-axis-resolver.ts +314 -0
- package/src/ts/charts/common/chrome-cache.ts +79 -0
- package/src/ts/charts/common/draw-tooltip-box.ts +84 -0
- package/src/ts/charts/common/leaf-color.ts +92 -0
- package/src/ts/charts/common/node-store.ts +235 -0
- package/src/ts/charts/common/tree-chart.ts +76 -0
- package/src/ts/charts/common/tree-chrome.ts +123 -0
- package/src/ts/charts/common/tree-data.ts +623 -0
- package/src/ts/charts/common/visible-extent.ts +112 -0
- package/src/ts/charts/heatmap/heatmap-build.ts +426 -0
- package/src/ts/charts/heatmap/heatmap-interact.ts +274 -0
- package/src/ts/charts/heatmap/heatmap-render.ts +815 -0
- package/src/ts/charts/heatmap/heatmap-y-axis.ts +351 -0
- package/src/ts/charts/heatmap/heatmap.ts +368 -0
- package/src/ts/charts/map/map.ts +201 -0
- package/src/ts/charts/registry.ts +65 -0
- package/src/ts/charts/series/glyphs/draw-areas.ts +331 -0
- package/src/ts/charts/series/glyphs/draw-bars.ts +113 -0
- package/src/ts/charts/series/glyphs/draw-lines.ts +320 -0
- package/src/ts/charts/series/glyphs/draw-scatter.ts +328 -0
- package/src/ts/charts/series/series-build.ts +848 -0
- package/src/ts/charts/series/series-interact.ts +604 -0
- package/src/ts/charts/series/series-render.ts +1109 -0
- package/src/ts/charts/series/series-type.ts +99 -0
- package/src/ts/charts/series/series.ts +794 -0
- package/src/ts/charts/sunburst/sunburst-interact.ts +460 -0
- package/src/ts/charts/sunburst/sunburst-layout.ts +238 -0
- package/src/ts/charts/sunburst/sunburst-render.ts +887 -0
- package/src/ts/charts/sunburst/sunburst.ts +248 -0
- package/src/ts/charts/treemap/treemap-interact.ts +445 -0
- package/src/ts/charts/treemap/treemap-layout.ts +328 -0
- package/src/ts/charts/treemap/treemap-render.ts +886 -0
- package/src/ts/charts/treemap/treemap.ts +247 -0
- package/src/ts/config.ts +41 -0
- package/src/ts/data/lazy-row.ts +140 -0
- package/src/ts/data/split-groups.ts +97 -0
- package/src/ts/data/view-reader.ts +107 -0
- package/src/ts/event-detail.ts +44 -0
- package/src/ts/index.ts +53 -0
- package/src/ts/interaction/hit-test.ts +106 -0
- package/src/ts/interaction/host-sink-dom.ts +85 -0
- package/src/ts/interaction/host-sink-message.ts +75 -0
- package/src/ts/interaction/lazy-tooltip.ts +102 -0
- package/src/ts/interaction/raw-event-forwarder.ts +175 -0
- package/src/ts/interaction/spatial-grid.ts +100 -0
- package/src/ts/interaction/tooltip-controller.ts +407 -0
- package/src/ts/interaction/zoom-controller.ts +468 -0
- package/src/ts/interaction/zoom-router.ts +230 -0
- package/src/ts/layout/facet-grid.ts +346 -0
- package/src/ts/layout/plot-layout.ts +277 -0
- package/src/ts/layout/ticks.ts +168 -0
- package/src/ts/map/mercator.ts +204 -0
- package/src/ts/map/tile-cache.ts +96 -0
- package/src/ts/map/tile-layer.ts +382 -0
- package/src/ts/map/tile-loader.ts +143 -0
- package/src/ts/map/tile-source.ts +156 -0
- package/src/ts/plugin/charts.ts +286 -0
- package/src/ts/plugin/plugin.ts +668 -0
- package/src/ts/render/scheduler.ts +339 -0
- package/src/ts/shaders/area.frag.glsl +20 -0
- package/src/ts/shaders/area.vert.glsl +19 -0
- package/src/ts/shaders/bar.frag.glsl +25 -0
- package/src/ts/shaders/bar.vert.glsl +60 -0
- package/src/ts/shaders/candlestick-body.frag.glsl +19 -0
- package/src/ts/shaders/candlestick-body.vert.glsl +34 -0
- package/src/ts/shaders/density-extreme.frag.glsl +30 -0
- package/src/ts/shaders/density-mrt.frag.glsl +44 -0
- package/src/ts/shaders/density-mrt.vert.glsl +48 -0
- package/src/ts/shaders/density-resolve.frag.glsl +89 -0
- package/src/ts/shaders/density-resolve.vert.glsl +23 -0
- package/src/ts/shaders/density-splat.frag.glsl +34 -0
- package/src/ts/shaders/density-splat.vert.glsl +52 -0
- package/src/ts/shaders/gridline.frag.glsl +18 -0
- package/src/ts/shaders/gridline.vert.glsl +18 -0
- package/src/ts/shaders/heatmap.frag.glsl +23 -0
- package/src/ts/shaders/heatmap.vert.glsl +42 -0
- package/src/ts/shaders/line-uniform.frag.glsl +26 -0
- package/src/ts/shaders/line-uniform.vert.glsl +54 -0
- package/src/ts/shaders/line.frag.glsl +28 -0
- package/src/ts/shaders/line.vert.glsl +87 -0
- package/src/ts/shaders/scatter.frag.glsl +39 -0
- package/src/ts/shaders/scatter.vert.glsl +67 -0
- package/src/ts/shaders/sunburst-arc.frag.glsl +19 -0
- package/src/ts/shaders/sunburst-arc.vert.glsl +79 -0
- package/src/ts/shaders/tile.frag.glsl +27 -0
- package/src/ts/shaders/tile.vert.glsl +35 -0
- package/src/ts/shaders/treemap.frag.glsl +19 -0
- package/src/ts/shaders/treemap.vert.glsl +25 -0
- package/src/ts/shaders/y-scatter.frag.glsl +30 -0
- package/src/ts/shaders/y-scatter.vert.glsl +31 -0
- package/src/ts/theme/gradient.ts +312 -0
- package/src/ts/theme/palette.ts +64 -0
- package/src/ts/theme/theme-snapshot.ts +66 -0
- package/src/ts/theme/theme.ts +166 -0
- package/src/ts/transport/protocol.ts +497 -0
- package/src/ts/transport/renderer-transport.ts +788 -0
- package/src/ts/utils/css.ts +36 -0
- package/src/ts/utils/font-snapshot.ts +159 -0
- package/src/ts/webgl/buffer-pool.ts +163 -0
- package/src/ts/webgl/context-manager.ts +414 -0
- package/src/ts/webgl/gradient-texture.ts +84 -0
- package/src/ts/webgl/instanced-attrs.ts +139 -0
- package/src/ts/webgl/plot-frame.ts +91 -0
- package/src/ts/webgl/program-cache.ts +46 -0
- package/src/ts/webgl/shader-manifest.ts +148 -0
- package/src/ts/webgl/shader-registry.ts +97 -0
- package/src/ts/worker/boot.ts +22 -0
- package/src/ts/worker/dispatch.ts +99 -0
- package/src/ts/worker/font-loader.ts +89 -0
- package/src/ts/worker/renderer.worker.ts +734 -0
- package/src/ts/worker/session-host.ts +118 -0
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
import type { ColumnDataMap } from "../../data/view-reader";
|
|
14
|
+
import { buildSplitGroups } from "../../data/split-groups";
|
|
15
|
+
import type { CategoricalLevel } from "../../axis/categorical-axis";
|
|
16
|
+
import {
|
|
17
|
+
resolveAxisMode,
|
|
18
|
+
resolveCategoryAxis,
|
|
19
|
+
resolveNumericCategoryDomain,
|
|
20
|
+
type AxisMode,
|
|
21
|
+
type NumericCategoryDomain,
|
|
22
|
+
} from "../common/category-axis-resolver";
|
|
23
|
+
import { computeSlotGeometry } from "../common/band-layout";
|
|
24
|
+
import {
|
|
25
|
+
resolveChartType,
|
|
26
|
+
resolveStack,
|
|
27
|
+
resolveAltAxis,
|
|
28
|
+
type ChartType,
|
|
29
|
+
type ColumnChartConfig,
|
|
30
|
+
} from "./series-type";
|
|
31
|
+
|
|
32
|
+
const DUAL_Y_RATIO_THRESHOLD = 50;
|
|
33
|
+
|
|
34
|
+
export interface SeriesInfo {
|
|
35
|
+
seriesId: number;
|
|
36
|
+
aggIdx: number;
|
|
37
|
+
splitIdx: number;
|
|
38
|
+
aggName: string;
|
|
39
|
+
splitKey: string;
|
|
40
|
+
label: string;
|
|
41
|
+
color: [number, number, number];
|
|
42
|
+
axis: 0 | 1;
|
|
43
|
+
chartType: ChartType;
|
|
44
|
+
stack: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Logical bar/area record. Synthesized on demand from {@link BarColumns}
|
|
49
|
+
* via {@link readBarRecord} for tooltip / hover paths. The pipeline never
|
|
50
|
+
* materializes these — see `BarColumns` for the columnar storage that
|
|
51
|
+
* replaces the legacy `SeriesChartRecord[]`.
|
|
52
|
+
*/
|
|
53
|
+
export interface SeriesChartRecord {
|
|
54
|
+
catIdx: number;
|
|
55
|
+
aggIdx: number;
|
|
56
|
+
splitIdx: number;
|
|
57
|
+
seriesId: number;
|
|
58
|
+
xCenter: number;
|
|
59
|
+
halfWidth: number;
|
|
60
|
+
y0: number;
|
|
61
|
+
y1: number;
|
|
62
|
+
value: number;
|
|
63
|
+
axis: 0 | 1;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* `"bar"` quads or `"area"` strip segments both stack via this record.
|
|
67
|
+
*/
|
|
68
|
+
chartType: "bar" | "area";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const BAR_TYPE_BAR = 0;
|
|
72
|
+
export const BAR_TYPE_AREA = 1;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Columnar storage for the bar/area record set. Replaces the legacy
|
|
76
|
+
* `SeriesChartRecord[]` to avoid per-record POJO allocation at scale —
|
|
77
|
+
* with N×M×P potentially in the millions, the array-of-objects layout
|
|
78
|
+
* was the dominant build-time GC pressure.
|
|
79
|
+
*
|
|
80
|
+
* Records are appended in `(catIdx, aggIdx, splitIdx)` lexicographic
|
|
81
|
+
* order — the outer category loop guarantees `catIdx` is monotonically
|
|
82
|
+
* non-decreasing, which the renderer / hit-test use for binary-search
|
|
83
|
+
* narrowing.
|
|
84
|
+
*
|
|
85
|
+
* `count` is the active record count; the underlying typed arrays may
|
|
86
|
+
* be over-allocated for capacity reuse across builds.
|
|
87
|
+
*/
|
|
88
|
+
/**
|
|
89
|
+
* Compact columnar storage for the bar/area record set.
|
|
90
|
+
*
|
|
91
|
+
* Three fields the prior schema carried have been dropped because
|
|
92
|
+
* they're cheaply derivable at hover time:
|
|
93
|
+
* - `aggIdx` ← `seriesId / splitCount` (integer division)
|
|
94
|
+
* - `splitIdx` ← `seriesId % splitCount`
|
|
95
|
+
* - `value` ← `samples[catIdx * S + seriesId]`
|
|
96
|
+
*
|
|
97
|
+
* Per-cell write count drops from 11 to 8 (~27% fewer typed-array
|
|
98
|
+
* stores) and per-record memory drops from 58 B to 42 B (~28% lower
|
|
99
|
+
* footprint at scale). `chartType` is kept (1 B / record) — it's
|
|
100
|
+
* read in tight loops in the render and hit-test paths and a string
|
|
101
|
+
* dispatch via `_series[]` would be slower than the byte compare.
|
|
102
|
+
*/
|
|
103
|
+
export interface BarColumns {
|
|
104
|
+
count: number;
|
|
105
|
+
catIdx: Int32Array;
|
|
106
|
+
seriesId: Int32Array;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 0 = left axis, 1 = right axis.
|
|
110
|
+
*/
|
|
111
|
+
axis: Uint8Array;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* {@link BAR_TYPE_BAR} | {@link BAR_TYPE_AREA}.
|
|
115
|
+
*/
|
|
116
|
+
chartType: Uint8Array;
|
|
117
|
+
xCenter: Float64Array;
|
|
118
|
+
halfWidth: Float64Array;
|
|
119
|
+
y0: Float64Array;
|
|
120
|
+
y1: Float64Array;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function emptyBarColumns(): BarColumns {
|
|
124
|
+
return {
|
|
125
|
+
count: 0,
|
|
126
|
+
catIdx: new Int32Array(0),
|
|
127
|
+
seriesId: new Int32Array(0),
|
|
128
|
+
axis: new Uint8Array(0),
|
|
129
|
+
chartType: new Uint8Array(0),
|
|
130
|
+
xCenter: new Float64Array(0),
|
|
131
|
+
halfWidth: new Float64Array(0),
|
|
132
|
+
y0: new Float64Array(0),
|
|
133
|
+
y1: new Float64Array(0),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Reuse `prev`'s typed arrays when capacity is sufficient, else allocate
|
|
139
|
+
* fresh. Resets `count` to 0 either way; pipeline writes from index 0.
|
|
140
|
+
*/
|
|
141
|
+
export function ensureBarColumnsCapacity(
|
|
142
|
+
prev: BarColumns | null,
|
|
143
|
+
capacity: number,
|
|
144
|
+
): BarColumns {
|
|
145
|
+
if (prev && prev.catIdx.length >= capacity) {
|
|
146
|
+
prev.count = 0;
|
|
147
|
+
return prev;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
count: 0,
|
|
152
|
+
catIdx: new Int32Array(capacity),
|
|
153
|
+
seriesId: new Int32Array(capacity),
|
|
154
|
+
axis: new Uint8Array(capacity),
|
|
155
|
+
chartType: new Uint8Array(capacity),
|
|
156
|
+
xCenter: new Float64Array(capacity),
|
|
157
|
+
halfWidth: new Float64Array(capacity),
|
|
158
|
+
y0: new Float64Array(capacity),
|
|
159
|
+
y1: new Float64Array(capacity),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Synthesize a {@link SeriesChartRecord} POJO for record `i`. Used by
|
|
165
|
+
* tooltip / hover paths that hand out a single record reference; not
|
|
166
|
+
* called in any frame-rate hot loop.
|
|
167
|
+
*
|
|
168
|
+
* `splitCount` is `splitPrefixes.length` from the build result (= `P`
|
|
169
|
+
* in pipeline notation). `samples` + `numSeries` recover the raw
|
|
170
|
+
* value from the unstacked sample grid; `samples[catIdx * S + sid]`
|
|
171
|
+
* always carries the same value the pipeline saw when emitting the
|
|
172
|
+
* record (both writes share the same `v` source).
|
|
173
|
+
*/
|
|
174
|
+
export function readBarRecord(
|
|
175
|
+
cols: BarColumns,
|
|
176
|
+
i: number,
|
|
177
|
+
splitCount: number,
|
|
178
|
+
samples: Float32Array,
|
|
179
|
+
numSeries: number,
|
|
180
|
+
): SeriesChartRecord {
|
|
181
|
+
const sid = cols.seriesId[i];
|
|
182
|
+
const ci = cols.catIdx[i];
|
|
183
|
+
return {
|
|
184
|
+
catIdx: ci,
|
|
185
|
+
aggIdx: Math.floor(sid / splitCount),
|
|
186
|
+
splitIdx: sid % splitCount,
|
|
187
|
+
seriesId: sid,
|
|
188
|
+
xCenter: cols.xCenter[i],
|
|
189
|
+
halfWidth: cols.halfWidth[i],
|
|
190
|
+
y0: cols.y0[i],
|
|
191
|
+
y1: cols.y1[i],
|
|
192
|
+
value: samples[ci * numSeries + sid],
|
|
193
|
+
axis: cols.axis[i] as 0 | 1,
|
|
194
|
+
chartType: cols.chartType[i] === BAR_TYPE_BAR ? "bar" : "area",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Reusable Float64 scratch — chart owns one for `posStack` and one for
|
|
200
|
+
* `negStack`. Pipeline zero-fills the active prefix on entry.
|
|
201
|
+
*/
|
|
202
|
+
export function ensureFloat64Scratch(
|
|
203
|
+
prev: Float64Array | null,
|
|
204
|
+
capacity: number,
|
|
205
|
+
): Float64Array {
|
|
206
|
+
if (prev && prev.length >= capacity) {
|
|
207
|
+
return prev;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return new Float64Array(Math.max(capacity, prev?.length ?? 0));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface SeriesPipelineInput {
|
|
214
|
+
columns: ColumnDataMap;
|
|
215
|
+
numRows: number;
|
|
216
|
+
columnSlots: (string | null)[];
|
|
217
|
+
groupBy: string[];
|
|
218
|
+
splitBy: string[];
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Source-column types for `group_by` columns (table.schema() merged
|
|
222
|
+
* with view.expression_schema()). Used to (a) stringify non-string
|
|
223
|
+
* row-path levels and (b) decide between category and numeric axis
|
|
224
|
+
* mode for single-level group_bys.
|
|
225
|
+
*/
|
|
226
|
+
groupByTypes: Record<string, string>;
|
|
227
|
+
columnsConfig: Record<string, ColumnChartConfig> | undefined;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Plugin-scoped default glyph when a column has no explicit entry.
|
|
231
|
+
*/
|
|
232
|
+
defaultChartType?: ChartType;
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Plugin-config knobs consumed by the build pipeline. Pulled from
|
|
236
|
+
* the chart impl's `_pluginConfig` (sourced from the host's
|
|
237
|
+
* `plugin_config_schema` / `restore({ plugin_config })`):
|
|
238
|
+
*
|
|
239
|
+
* - `autoAltYAxis` — auto-split aggregates onto a secondary Y
|
|
240
|
+
* axis when their magnitude ratio exceeds
|
|
241
|
+
* `DUAL_Y_RATIO_THRESHOLD`. Replaces the `AUTO_ALT_Y_AXIS`
|
|
242
|
+
* compile-time toggle.
|
|
243
|
+
* - `bandInnerFrac` / `barInnerPad` — band-slot geometry forwarded
|
|
244
|
+
* to `computeSlotGeometry`. Replace the `BAND_INNER_FRAC` /
|
|
245
|
+
* `BAR_INNER_PAD` constants.
|
|
246
|
+
*/
|
|
247
|
+
autoAltYAxis: boolean;
|
|
248
|
+
bandInnerFrac: number;
|
|
249
|
+
barInnerPad: number;
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Anchor value-axis extents to zero. When `true` (bar / area
|
|
253
|
+
* default), `leftDomain` / `rightDomain` are guaranteed to enclose
|
|
254
|
+
* `0` so bars and areas render against their natural baseline.
|
|
255
|
+
* When `false` (line / scatter default), the domain is the raw
|
|
256
|
+
* `min`/`max` of the data — the axis tightens around the visible
|
|
257
|
+
* variation. Maps directly to `PluginConfig.include_zero`.
|
|
258
|
+
*/
|
|
259
|
+
includeZero: boolean;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Reusable scratch — pipeline writes records into these in place
|
|
263
|
+
* and zero-fills the stack ladder. Pass the previous build's
|
|
264
|
+
* outputs to amortize allocation across data reloads.
|
|
265
|
+
*/
|
|
266
|
+
scratchBars?: BarColumns | null;
|
|
267
|
+
scratchPosStack?: Float64Array | null;
|
|
268
|
+
scratchNegStack?: Float64Array | null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export type { NumericCategoryDomain };
|
|
272
|
+
|
|
273
|
+
export interface SeriesPipelineResult {
|
|
274
|
+
aggregates: string[];
|
|
275
|
+
splitPrefixes: string[];
|
|
276
|
+
rowPaths: CategoricalLevel[];
|
|
277
|
+
numCategories: number;
|
|
278
|
+
rowOffset: number;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Axis mode discriminator. `category` is the default (zero or
|
|
282
|
+
* many group_by levels, or a single string/boolean level). `numeric`
|
|
283
|
+
* fires for a single non-string non-boolean group_by — bars are
|
|
284
|
+
* positioned by the underlying data value rather than logical
|
|
285
|
+
* category index.
|
|
286
|
+
*/
|
|
287
|
+
axisMode: AxisMode;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Populated only when `axisMode.mode === "numeric"`.
|
|
291
|
+
*/
|
|
292
|
+
numericCategoryDomain: NumericCategoryDomain | null;
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Per-category X coordinate in real data units. Populated only in
|
|
296
|
+
* numeric axis mode — `null` in category mode where catIdx itself
|
|
297
|
+
* is the position. Indexed by `catIdx` (0..numCategories-1).
|
|
298
|
+
*/
|
|
299
|
+
categoryPositions: Float64Array | null;
|
|
300
|
+
series: SeriesInfo[];
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Columnar bar/area records, one per (catIdx, agg, split) for series
|
|
304
|
+
* where `stack === true && chartType in ["bar", "area"]` (stacked) or
|
|
305
|
+
* `chartType in ["bar", "area"]` with non-zero value (unstacked).
|
|
306
|
+
*/
|
|
307
|
+
bars: BarColumns;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Reusable scratch passthrough — these own the stack ladder typed
|
|
311
|
+
* arrays so the next build can reuse capacity.
|
|
312
|
+
*/
|
|
313
|
+
posStack: Float64Array | null;
|
|
314
|
+
negStack: Float64Array | null;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Unstacked sample grid: `samples[catIdx * S + seriesId]` is the raw
|
|
318
|
+
* value for that cell. Only valid for non-stacking series (or for
|
|
319
|
+
* stacking series when you need the raw, pre-stack value); the
|
|
320
|
+
* corresponding bit in `sampleValid` indicates whether the cell carries
|
|
321
|
+
* data. `S === series.length`.
|
|
322
|
+
*/
|
|
323
|
+
samples: Float32Array;
|
|
324
|
+
sampleValid: Uint8Array;
|
|
325
|
+
|
|
326
|
+
leftDomain: { min: number; max: number };
|
|
327
|
+
rightDomain: { min: number; max: number } | null;
|
|
328
|
+
hasRightAxis: boolean;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function setValidBit(valid: Uint8Array, idx: number): void {
|
|
332
|
+
valid[idx >> 3] |= 1 << (idx & 7);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Pure pipeline: turn a raw `ColumnDataMap` into (a) columnar stacked
|
|
337
|
+
* bar/area records and (b) an unstacked `samples` grid for line/scatter
|
|
338
|
+
* glyphs plus non-stacking bar/area series. Holds row_path data as
|
|
339
|
+
* zero-copy views (no materialization of category strings).
|
|
340
|
+
*
|
|
341
|
+
* Automatically splits aggregates across a secondary Y axis when their
|
|
342
|
+
* extents differ by more than {@link DUAL_Y_RATIO_THRESHOLD}×.
|
|
343
|
+
*/
|
|
344
|
+
export function buildSeriesPipeline(
|
|
345
|
+
input: SeriesPipelineInput,
|
|
346
|
+
): SeriesPipelineResult {
|
|
347
|
+
const {
|
|
348
|
+
columns,
|
|
349
|
+
numRows,
|
|
350
|
+
columnSlots,
|
|
351
|
+
groupBy,
|
|
352
|
+
splitBy,
|
|
353
|
+
groupByTypes,
|
|
354
|
+
columnsConfig,
|
|
355
|
+
defaultChartType,
|
|
356
|
+
autoAltYAxis,
|
|
357
|
+
bandInnerFrac,
|
|
358
|
+
barInnerPad,
|
|
359
|
+
includeZero,
|
|
360
|
+
scratchBars,
|
|
361
|
+
scratchPosStack,
|
|
362
|
+
scratchNegStack,
|
|
363
|
+
} = input;
|
|
364
|
+
|
|
365
|
+
const axisMode = resolveAxisMode(groupBy, groupByTypes);
|
|
366
|
+
const empty: SeriesPipelineResult = {
|
|
367
|
+
aggregates: [],
|
|
368
|
+
splitPrefixes: [],
|
|
369
|
+
rowPaths: [],
|
|
370
|
+
numCategories: 0,
|
|
371
|
+
rowOffset: 0,
|
|
372
|
+
axisMode,
|
|
373
|
+
numericCategoryDomain: null,
|
|
374
|
+
categoryPositions: null,
|
|
375
|
+
series: [],
|
|
376
|
+
bars: emptyBarColumns(),
|
|
377
|
+
posStack: scratchPosStack ?? null,
|
|
378
|
+
negStack: scratchNegStack ?? null,
|
|
379
|
+
samples: new Float32Array(0),
|
|
380
|
+
sampleValid: new Uint8Array(0),
|
|
381
|
+
leftDomain: { min: 0, max: 0 },
|
|
382
|
+
rightDomain: null,
|
|
383
|
+
hasRightAxis: false,
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const aggregates = columnSlots.filter((s): s is string => !!s);
|
|
387
|
+
if (aggregates.length === 0) {
|
|
388
|
+
return empty;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const splitPrefixes: string[] = [];
|
|
392
|
+
if (splitBy.length > 0) {
|
|
393
|
+
for (const g of buildSplitGroups(columns, [], aggregates)) {
|
|
394
|
+
if (g.colNames.size > 0) {
|
|
395
|
+
splitPrefixes.push(g.prefix);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (splitPrefixes.length === 0) {
|
|
400
|
+
splitPrefixes.push("");
|
|
401
|
+
}
|
|
402
|
+
} else {
|
|
403
|
+
splitPrefixes.push("");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const levelTypes = groupBy.map((name) => groupByTypes[name] ?? "string");
|
|
407
|
+
const { rowPaths, numCategories, rowOffset } = resolveCategoryAxis(
|
|
408
|
+
columns,
|
|
409
|
+
numRows,
|
|
410
|
+
groupBy.length,
|
|
411
|
+
levelTypes,
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
if (numCategories === 0) {
|
|
415
|
+
return {
|
|
416
|
+
...empty,
|
|
417
|
+
aggregates,
|
|
418
|
+
splitPrefixes,
|
|
419
|
+
rowPaths,
|
|
420
|
+
rowOffset,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const series: SeriesInfo[] = [];
|
|
425
|
+
const M = aggregates.length;
|
|
426
|
+
const P = splitPrefixes.length;
|
|
427
|
+
for (let k = 0; k < M; k++) {
|
|
428
|
+
for (let p = 0; p < P; p++) {
|
|
429
|
+
const aggName = aggregates[k];
|
|
430
|
+
const splitKey = splitPrefixes[p];
|
|
431
|
+
const label =
|
|
432
|
+
splitKey === ""
|
|
433
|
+
? aggName
|
|
434
|
+
: `${splitKey}${M > 1 ? ` | ${aggName}` : ""}`;
|
|
435
|
+
const chartType = resolveChartType(
|
|
436
|
+
aggName,
|
|
437
|
+
columnsConfig,
|
|
438
|
+
defaultChartType,
|
|
439
|
+
);
|
|
440
|
+
const stack = resolveStack(aggName, chartType, columnsConfig);
|
|
441
|
+
series.push({
|
|
442
|
+
seriesId: k * P + p,
|
|
443
|
+
aggIdx: k,
|
|
444
|
+
splitIdx: p,
|
|
445
|
+
aggName,
|
|
446
|
+
splitKey,
|
|
447
|
+
label,
|
|
448
|
+
color: [0.5, 0.5, 0.5],
|
|
449
|
+
axis: 0,
|
|
450
|
+
chartType,
|
|
451
|
+
stack,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// `aggExtents` accumulates per-aggregate value ranges for the
|
|
457
|
+
// dual-axis split heuristic below. `includeZero` decides whether
|
|
458
|
+
// the range starts anchored at zero (bar / area: the bar grows
|
|
459
|
+
// from the zero baseline, so it's part of the natural extent) or
|
|
460
|
+
// open (line / scatter: extent is the raw `min`..`max`).
|
|
461
|
+
const aggExtents: { min: number; max: number }[] = [];
|
|
462
|
+
for (let k = 0; k < M; k++) {
|
|
463
|
+
aggExtents.push(
|
|
464
|
+
includeZero
|
|
465
|
+
? { min: 0, max: 0 }
|
|
466
|
+
: { min: Infinity, max: -Infinity },
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const N = numCategories;
|
|
471
|
+
const S = series.length;
|
|
472
|
+
|
|
473
|
+
// Stacking ladder, keyed by (catIdx, aggIdx). Reuse chart-owned
|
|
474
|
+
// scratch when sized; else allocate. Active prefix is zero-filled.
|
|
475
|
+
const stackLen = N * M;
|
|
476
|
+
const posStack = ensureFloat64Scratch(scratchPosStack ?? null, stackLen);
|
|
477
|
+
const negStack = ensureFloat64Scratch(scratchNegStack ?? null, stackLen);
|
|
478
|
+
posStack.fill(0, 0, stackLen);
|
|
479
|
+
negStack.fill(0, 0, stackLen);
|
|
480
|
+
|
|
481
|
+
const samples = new Float32Array(N * S);
|
|
482
|
+
const sampleValid = new Uint8Array((N * S + 7) >> 3);
|
|
483
|
+
|
|
484
|
+
// Numeric-mode category positions: real data values from __ROW_PATH_0__.
|
|
485
|
+
// null in category mode (catIdx is the position).
|
|
486
|
+
let categoryPositions: Float64Array | null = null;
|
|
487
|
+
let numericCategoryDomain: NumericCategoryDomain | null = null;
|
|
488
|
+
let numericBandWidth = 1;
|
|
489
|
+
if (axisMode.mode === "numeric" && N > 0) {
|
|
490
|
+
const rp = columns.get("__ROW_PATH_0__");
|
|
491
|
+
const resolved = resolveNumericCategoryDomain(
|
|
492
|
+
rp?.values,
|
|
493
|
+
N,
|
|
494
|
+
rowOffset,
|
|
495
|
+
groupBy[0] ?? "",
|
|
496
|
+
axisMode.numericType === "date" ||
|
|
497
|
+
axisMode.numericType === "datetime",
|
|
498
|
+
);
|
|
499
|
+
if (resolved) {
|
|
500
|
+
categoryPositions = resolved.categoryPositions;
|
|
501
|
+
numericCategoryDomain = resolved.numericCategoryDomain;
|
|
502
|
+
numericBandWidth = resolved.numericCategoryDomain.bandWidth;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Per-band slot geometry — `computeSlotGeometry` returns values in
|
|
507
|
+
// band-relative units (band width = 1). In numeric mode scale by
|
|
508
|
+
// the data-unit band width derived above.
|
|
509
|
+
const baseSlot = computeSlotGeometry(M, bandInnerFrac, barInnerPad);
|
|
510
|
+
const slotWidth = baseSlot.slotWidth * numericBandWidth;
|
|
511
|
+
const halfWidth = baseSlot.halfWidth * numericBandWidth;
|
|
512
|
+
|
|
513
|
+
// Pre-build per-aggregate slot offsets. The legacy form recomputed
|
|
514
|
+
// `(k - (M - 1) / 2) * slotWidth` for every (catI, k, p) — N×M×P
|
|
515
|
+
// FMA chains in the inner loop. Hoist to a length-M lookup.
|
|
516
|
+
const slotOffsets = new Float64Array(M);
|
|
517
|
+
const halfWidthOffset = (M - 1) / 2;
|
|
518
|
+
for (let k = 0; k < M; k++) {
|
|
519
|
+
slotOffsets[k] = (k - halfWidthOffset) * slotWidth;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Pre-resolve the (k, p) → column reference + valid mask. The legacy
|
|
523
|
+
// form built the column name string and called `columns.get(...)` for
|
|
524
|
+
// every (catI, k, p) cell — N×M×P string allocs + Map lookups, which
|
|
525
|
+
// dominates for dense data. Hoist outside the row loop to an
|
|
526
|
+
// M*P-shaped flat array of (values, valid) tuples.
|
|
527
|
+
const colValues: (ArrayLike<unknown> | null)[] = new Array(M * P);
|
|
528
|
+
const colValid: (Uint8Array | null)[] = new Array(M * P);
|
|
529
|
+
for (let k = 0; k < M; k++) {
|
|
530
|
+
const aggName = aggregates[k];
|
|
531
|
+
for (let p = 0; p < P; p++) {
|
|
532
|
+
const splitKey = splitPrefixes[p];
|
|
533
|
+
const colName =
|
|
534
|
+
splitKey === "" ? aggName : `${splitKey}|${aggName}`;
|
|
535
|
+
const col = columns.get(colName);
|
|
536
|
+
const idx = k * P + p;
|
|
537
|
+
colValues[idx] = col?.values ?? null;
|
|
538
|
+
colValid[idx] = col?.valid ?? null;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Pre-allocate columnar bar storage at N*M*P upper bound. The
|
|
543
|
+
// pipeline emits at most one record per (cat, agg, split) cell;
|
|
544
|
+
// `bars.count` tracks the active prefix.
|
|
545
|
+
const barCap = N * M * P;
|
|
546
|
+
const bars = ensureBarColumnsCapacity(scratchBars ?? null, barCap);
|
|
547
|
+
let barWrite = 0;
|
|
548
|
+
|
|
549
|
+
for (let catI = 0; catI < N; catI++) {
|
|
550
|
+
const row = catI + rowOffset;
|
|
551
|
+
|
|
552
|
+
// Hoist the category center — same value across all (k, p) for
|
|
553
|
+
// the current catI.
|
|
554
|
+
const catCenter = categoryPositions ? categoryPositions[catI] : catI;
|
|
555
|
+
|
|
556
|
+
for (let k = 0; k < M; k++) {
|
|
557
|
+
const slotOffset = slotOffsets[k];
|
|
558
|
+
const xCenter = catCenter + slotOffset;
|
|
559
|
+
const ext = aggExtents[k];
|
|
560
|
+
|
|
561
|
+
for (let p = 0; p < P; p++) {
|
|
562
|
+
const seriesId = k * P + p;
|
|
563
|
+
const s = series[seriesId];
|
|
564
|
+
const colIdx = k * P + p;
|
|
565
|
+
const values = colValues[colIdx];
|
|
566
|
+
if (!values) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const valid = colValid[colIdx];
|
|
571
|
+
if (valid) {
|
|
572
|
+
const bit = (valid[row >> 3] >> (row & 7)) & 1;
|
|
573
|
+
if (!bit) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const v = values[row] as number;
|
|
579
|
+
if (!isFinite(v)) {
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Record the raw value in the unstacked grid for every
|
|
584
|
+
// glyph that needs it (line, scatter, non-stacking bar/area).
|
|
585
|
+
const sampleIdx = catI * S + seriesId;
|
|
586
|
+
samples[sampleIdx] = v;
|
|
587
|
+
setValidBit(sampleValid, sampleIdx);
|
|
588
|
+
|
|
589
|
+
// Stacking-glyph path: emit a record with running y0/y1.
|
|
590
|
+
if (
|
|
591
|
+
(s.chartType === "bar" || s.chartType === "area") &&
|
|
592
|
+
s.stack
|
|
593
|
+
) {
|
|
594
|
+
if (v === 0) {
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const stackIdx = catI * M + k;
|
|
599
|
+
let y0: number;
|
|
600
|
+
let y1: number;
|
|
601
|
+
if (v >= 0) {
|
|
602
|
+
y0 = posStack[stackIdx];
|
|
603
|
+
y1 = y0 + v;
|
|
604
|
+
posStack[stackIdx] = y1;
|
|
605
|
+
} else {
|
|
606
|
+
y0 = negStack[stackIdx];
|
|
607
|
+
y1 = y0 + v;
|
|
608
|
+
negStack[stackIdx] = y1;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (y0 < ext.min) {
|
|
612
|
+
ext.min = y0;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (y1 < ext.min) {
|
|
616
|
+
ext.min = y1;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (y0 > ext.max) {
|
|
620
|
+
ext.max = y0;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (y1 > ext.max) {
|
|
624
|
+
ext.max = y1;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
bars.catIdx[barWrite] = catI;
|
|
628
|
+
bars.seriesId[barWrite] = seriesId;
|
|
629
|
+
bars.axis[barWrite] = 0;
|
|
630
|
+
bars.chartType[barWrite] =
|
|
631
|
+
s.chartType === "bar" ? BAR_TYPE_BAR : BAR_TYPE_AREA;
|
|
632
|
+
bars.xCenter[barWrite] = xCenter;
|
|
633
|
+
bars.halfWidth[barWrite] = halfWidth;
|
|
634
|
+
bars.y0[barWrite] = y0;
|
|
635
|
+
bars.y1[barWrite] = y1;
|
|
636
|
+
barWrite++;
|
|
637
|
+
} else {
|
|
638
|
+
// Non-stacking: extend extents by raw value against zero
|
|
639
|
+
// baseline so the axis still encloses line/scatter data.
|
|
640
|
+
if (v < ext.min) {
|
|
641
|
+
ext.min = v;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (v > ext.max) {
|
|
645
|
+
ext.max = v;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (includeZero) {
|
|
649
|
+
if (0 < ext.min) {
|
|
650
|
+
ext.min = 0;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if (0 > ext.max) {
|
|
654
|
+
ext.max = 0;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Non-stacking bar/area still needs a record so the
|
|
659
|
+
// glyph draw call has a concrete rect. Unstacked: y0=0,
|
|
660
|
+
// y1=v.
|
|
661
|
+
if (s.chartType === "bar" || s.chartType === "area") {
|
|
662
|
+
if (v === 0) {
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
bars.catIdx[barWrite] = catI;
|
|
667
|
+
bars.seriesId[barWrite] = seriesId;
|
|
668
|
+
bars.axis[barWrite] = 0;
|
|
669
|
+
bars.chartType[barWrite] =
|
|
670
|
+
s.chartType === "bar"
|
|
671
|
+
? BAR_TYPE_BAR
|
|
672
|
+
: BAR_TYPE_AREA;
|
|
673
|
+
bars.xCenter[barWrite] = xCenter;
|
|
674
|
+
bars.halfWidth[barWrite] = halfWidth;
|
|
675
|
+
bars.y0[barWrite] = 0;
|
|
676
|
+
bars.y1[barWrite] = v;
|
|
677
|
+
barWrite++;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
bars.count = barWrite;
|
|
685
|
+
|
|
686
|
+
let hasRightAxis = false;
|
|
687
|
+
if (autoAltYAxis && M >= 2) {
|
|
688
|
+
const extents: number[] = new Array(M);
|
|
689
|
+
let maxExt = 0;
|
|
690
|
+
let minExt = Infinity;
|
|
691
|
+
for (let k = 0; k < M; k++) {
|
|
692
|
+
const e = aggExtents[k];
|
|
693
|
+
const ae = Math.max(Math.abs(e.min), Math.abs(e.max), 1e-12);
|
|
694
|
+
extents[k] = ae;
|
|
695
|
+
if (ae > maxExt) {
|
|
696
|
+
maxExt = ae;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (ae < minExt) {
|
|
700
|
+
minExt = ae;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (maxExt / minExt > DUAL_Y_RATIO_THRESHOLD) {
|
|
705
|
+
const threshold = maxExt / Math.sqrt(DUAL_Y_RATIO_THRESHOLD);
|
|
706
|
+
for (let k = 0; k < M; k++) {
|
|
707
|
+
const onRight = extents[k] < threshold;
|
|
708
|
+
if (onRight) {
|
|
709
|
+
for (const s of series) {
|
|
710
|
+
if (s.aggIdx === k) {
|
|
711
|
+
s.axis = 1;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Propagate axis assignment into bar storage.
|
|
718
|
+
for (let i = 0; i < bars.count; i++) {
|
|
719
|
+
bars.axis[i] = series[bars.seriesId[i]].axis;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
hasRightAxis = series.some((s) => s.axis === 1);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Per-column `alt_axis` override — always wins over the auto
|
|
727
|
+
// split. Runs unconditionally so it works even when
|
|
728
|
+
// `autoAltYAxis` is off or there's only a single aggregate.
|
|
729
|
+
let forcedRight = false;
|
|
730
|
+
for (let k = 0; k < M; k++) {
|
|
731
|
+
if (resolveAltAxis(aggregates[k], columnsConfig)) {
|
|
732
|
+
for (const s of series) {
|
|
733
|
+
if (s.aggIdx === k) {
|
|
734
|
+
s.axis = 1;
|
|
735
|
+
forcedRight = true;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (forcedRight) {
|
|
742
|
+
for (let i = 0; i < bars.count; i++) {
|
|
743
|
+
bars.axis[i] = series[bars.seriesId[i]].axis;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
hasRightAxis = true;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Axis domains: stack records contribute y0/y1; non-stacking
|
|
750
|
+
// samples contribute raw values. When `includeZero` is true the
|
|
751
|
+
// domain starts anchored at zero so bar / area glyphs always have
|
|
752
|
+
// their baseline in view; when false the domain opens to
|
|
753
|
+
// `[Infinity, -Infinity]` and closes around the data extent.
|
|
754
|
+
const leftExtent = includeZero
|
|
755
|
+
? { min: 0, max: 0 }
|
|
756
|
+
: { min: Infinity, max: -Infinity };
|
|
757
|
+
const rightExtent = includeZero
|
|
758
|
+
? { min: 0, max: 0 }
|
|
759
|
+
: { min: Infinity, max: -Infinity };
|
|
760
|
+
for (let i = 0; i < bars.count; i++) {
|
|
761
|
+
const ext = bars.axis[i] === 0 ? leftExtent : rightExtent;
|
|
762
|
+
const y0 = bars.y0[i];
|
|
763
|
+
const y1 = bars.y1[i];
|
|
764
|
+
if (y0 < ext.min) {
|
|
765
|
+
ext.min = y0;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (y1 < ext.min) {
|
|
769
|
+
ext.min = y1;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (y0 > ext.max) {
|
|
773
|
+
ext.max = y0;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (y1 > ext.max) {
|
|
777
|
+
ext.max = y1;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
for (let seriesId = 0; seriesId < S; seriesId++) {
|
|
782
|
+
const s = series[seriesId];
|
|
783
|
+
if (s.stack && (s.chartType === "bar" || s.chartType === "area")) {
|
|
784
|
+
continue; // already counted via bars
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const ext = s.axis === 0 ? leftExtent : rightExtent;
|
|
788
|
+
for (let catI = 0; catI < N; catI++) {
|
|
789
|
+
const sampleIdx = catI * S + seriesId;
|
|
790
|
+
if (!((sampleValid[sampleIdx >> 3] >> (sampleIdx & 7)) & 1)) {
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const v = samples[sampleIdx];
|
|
795
|
+
if (v < ext.min) {
|
|
796
|
+
ext.min = v;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
if (v > ext.max) {
|
|
800
|
+
ext.max = v;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Empty-data fallback: an untouched extent still sits at its
|
|
806
|
+
// sentinel state. `includeZero=true` initializes to `{0, 0}`;
|
|
807
|
+
// `includeZero=false` initializes to `{Infinity, -Infinity}`.
|
|
808
|
+
// Either way, collapse to `{0, 1}` so axis rendering has a finite
|
|
809
|
+
// domain to work with.
|
|
810
|
+
const leftEmpty =
|
|
811
|
+
!isFinite(leftExtent.min) ||
|
|
812
|
+
!isFinite(leftExtent.max) ||
|
|
813
|
+
(leftExtent.min === 0 && leftExtent.max === 0);
|
|
814
|
+
if (leftEmpty) {
|
|
815
|
+
leftExtent.min = 0;
|
|
816
|
+
leftExtent.max = 1;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const rightEmpty =
|
|
820
|
+
!isFinite(rightExtent.min) ||
|
|
821
|
+
!isFinite(rightExtent.max) ||
|
|
822
|
+
(rightExtent.min === 0 && rightExtent.max === 0);
|
|
823
|
+
const rightDomain: { min: number; max: number } | null = hasRightAxis
|
|
824
|
+
? rightEmpty
|
|
825
|
+
? { min: 0, max: 1 }
|
|
826
|
+
: rightExtent
|
|
827
|
+
: null;
|
|
828
|
+
|
|
829
|
+
return {
|
|
830
|
+
aggregates,
|
|
831
|
+
splitPrefixes,
|
|
832
|
+
rowPaths,
|
|
833
|
+
numCategories,
|
|
834
|
+
rowOffset,
|
|
835
|
+
axisMode,
|
|
836
|
+
numericCategoryDomain,
|
|
837
|
+
categoryPositions,
|
|
838
|
+
series,
|
|
839
|
+
bars,
|
|
840
|
+
posStack,
|
|
841
|
+
negStack,
|
|
842
|
+
samples,
|
|
843
|
+
sampleValid,
|
|
844
|
+
leftDomain: leftExtent,
|
|
845
|
+
rightDomain,
|
|
846
|
+
hasRightAxis,
|
|
847
|
+
};
|
|
848
|
+
}
|