@perspective-dev/viewer-charts 4.3.0 → 4.5.1

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.
Files changed (72) hide show
  1. package/dist/cdn/perspective-viewer-charts.js +2 -2
  2. package/dist/cdn/perspective-viewer-charts.js.map +3 -3
  3. package/dist/esm/axis/bar-axis.d.ts +9 -1
  4. package/dist/esm/axis/categorical-axis.d.ts +0 -2
  5. package/dist/esm/charts/cartesian/cartesian.d.ts +26 -0
  6. package/dist/esm/charts/common/category-axis-resolver.d.ts +43 -1
  7. package/dist/esm/charts/common/expand-domain.d.ts +20 -0
  8. package/dist/esm/charts/common/tree-chart.d.ts +7 -0
  9. package/dist/esm/charts/common/tree-chrome.d.ts +23 -1
  10. package/dist/esm/charts/common/tree-interact.d.ts +46 -0
  11. package/dist/esm/charts/series/glyphs/draw-lines.d.ts +11 -4
  12. package/dist/esm/charts/series/series-build.d.ts +38 -2
  13. package/dist/esm/charts/series/series-render.d.ts +1 -4
  14. package/dist/esm/charts/series/series-type.d.ts +19 -17
  15. package/dist/esm/charts/series/series.d.ts +16 -0
  16. package/dist/esm/charts/sunburst/sunburst-interact.d.ts +1 -1
  17. package/dist/esm/charts/treemap/treemap-interact.d.ts +1 -6
  18. package/dist/esm/interaction/host-sink-message.d.ts +10 -28
  19. package/dist/esm/interaction/raw-event-forwarder.d.ts +6 -7
  20. package/dist/esm/interaction/zoom-controller.d.ts +31 -20
  21. package/dist/esm/interaction/zoom-router.d.ts +3 -26
  22. package/dist/esm/perspective-viewer-charts.js +2 -2
  23. package/dist/esm/perspective-viewer-charts.js.map +3 -3
  24. package/dist/esm/plugin/plugin.d.ts +0 -1
  25. package/dist/esm/theme/palette.d.ts +0 -5
  26. package/dist/esm/transport/protocol.d.ts +2 -7
  27. package/dist/esm/worker/renderer.worker.d.ts +2 -4
  28. package/package.json +1 -1
  29. package/src/ts/axis/bar-axis.ts +74 -45
  30. package/src/ts/axis/categorical-axis.ts +0 -2
  31. package/src/ts/charts/candlestick/candlestick-render.ts +10 -7
  32. package/src/ts/charts/candlestick/candlestick.ts +10 -29
  33. package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +36 -2
  34. package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +36 -2
  35. package/src/ts/charts/cartesian/cartesian-build.ts +143 -9
  36. package/src/ts/charts/cartesian/cartesian-render.ts +205 -30
  37. package/src/ts/charts/cartesian/cartesian.ts +43 -4
  38. package/src/ts/charts/cartesian/glyphs/density.ts +36 -41
  39. package/src/ts/charts/cartesian/glyphs/lines.ts +13 -15
  40. package/src/ts/charts/cartesian/glyphs/points.ts +12 -17
  41. package/src/ts/charts/chart-base.ts +20 -6
  42. package/src/ts/charts/chart.ts +1 -1
  43. package/src/ts/charts/common/category-axis-resolver.ts +135 -1
  44. package/src/ts/charts/common/expand-domain.ts +40 -0
  45. package/src/ts/charts/common/tree-chart.ts +16 -0
  46. package/src/ts/charts/common/tree-chrome.ts +86 -1
  47. package/src/ts/charts/common/tree-interact.ts +209 -0
  48. package/src/ts/charts/heatmap/heatmap-render.ts +9 -11
  49. package/src/ts/charts/series/glyphs/draw-areas.ts +30 -1
  50. package/src/ts/charts/series/glyphs/draw-lines.ts +151 -76
  51. package/src/ts/charts/series/series-build.ts +394 -21
  52. package/src/ts/charts/series/series-render.ts +159 -38
  53. package/src/ts/charts/series/series-type.ts +37 -17
  54. package/src/ts/charts/series/series.ts +63 -68
  55. package/src/ts/charts/sunburst/sunburst-interact.ts +18 -162
  56. package/src/ts/charts/sunburst/sunburst-render.ts +24 -89
  57. package/src/ts/charts/sunburst/sunburst.ts +1 -15
  58. package/src/ts/charts/treemap/treemap-interact.ts +22 -189
  59. package/src/ts/charts/treemap/treemap-render.ts +19 -46
  60. package/src/ts/charts/treemap/treemap.ts +1 -16
  61. package/src/ts/interaction/host-sink-message.ts +33 -22
  62. package/src/ts/interaction/raw-event-forwarder.ts +10 -12
  63. package/src/ts/interaction/zoom-controller.ts +120 -83
  64. package/src/ts/interaction/zoom-router.ts +3 -126
  65. package/src/ts/map/tile-layer.ts +13 -13
  66. package/src/ts/plugin/plugin.ts +100 -184
  67. package/src/ts/shaders/line-uniform.frag.glsl +2 -1
  68. package/src/ts/shaders/line-uniform.vert.glsl +19 -0
  69. package/src/ts/theme/palette.ts +1 -4
  70. package/src/ts/transport/protocol.ts +3 -8
  71. package/src/ts/worker/dispatch.ts +0 -1
  72. package/src/ts/worker/renderer.worker.ts +10 -46
@@ -29,6 +29,7 @@ import {
29
29
  renderBarAxesChrome,
30
30
  renderBarGridlines,
31
31
  type BarCategoryAxis,
32
+ type BarValueAxis,
32
33
  } from "../../axis/bar-axis";
33
34
  import {
34
35
  measureCategoricalAxisHeight,
@@ -38,10 +39,7 @@ import {
38
39
  import { buildBarTooltipLines } from "./series-interact";
39
40
 
40
41
  /**
41
- * Reusable scratch for bar instance uploads. Sized lazily at the first
42
- * use; grown on demand. Avoids `new Float32Array(n)` × 7 buffers per
43
- * legend-toggle / data-load; size is bounded by the bar-typed subset
44
- * of `_bars.count`.
42
+ * Reusable scratch for bar instance uploads.
45
43
  */
46
44
  interface BarInstanceScratch {
47
45
  xCenters: Float32Array;
@@ -78,10 +76,7 @@ function ensureBarInstanceScratch(n: number): BarInstanceScratch {
78
76
  }
79
77
 
80
78
  /**
81
- * Upload bar instance buffers from the columnar `_bars` storage. Filters
82
- * to bar-typed records only (areas draw as triangle strips). Skips
83
- * hidden series. Re-called from data-load and legend-toggle paths; the
84
- * scratch buffers and `_visibleBarIndices` are reused across calls.
79
+ * Upload bar instance buffers from the columnar `_bars` storage.
85
80
  */
86
81
  export function uploadBarInstances(
87
82
  chart: SeriesChart,
@@ -103,12 +98,6 @@ export function uploadBarInstances(
103
98
  const indices = chart._visibleBarIndices;
104
99
 
105
100
  // Rebase each xCenter by `_categoryOrigin` before f32 narrowing.
106
- // For datetime numeric category axes the absolute xCenter is
107
- // ~1.7e12 and f32 narrowing collapses adjacent bars onto the
108
- // same value; subtracting the origin brings every value into
109
- // the seconds range where f32 has full precision. The matching
110
- // projection matrix is built with the same origin so the shader
111
- // math is consistent.
112
101
  const xOrigin = chart._categoryOrigin;
113
102
  const series = chart._series;
114
103
  const hidden = chart._hiddenSeries;
@@ -350,14 +339,15 @@ export function renderBarFrame(
350
339
  }
351
340
 
352
341
  // Auto-fit the value axis to the visible categorical window. Gated
353
- // on `_autoFitValue` + non-default zoom: at default zoom the refit
354
- // result always equals `_leftDomain`/`_rightDomain`, so walking
355
- // would be wasted work (and would shift test baselines).
356
- if (
357
- chart._autoFitValue &&
358
- chart._zoomController &&
359
- !chart._zoomController.isDefault()
360
- ) {
342
+ // on `_autoFitValue` + the categorical axis being non-default: the
343
+ // refit only narrows when the categorical axis is itself zoomed
344
+ // (otherwise the visible window equals the data extent and the
345
+ // refit collapses back to `_leftDomain`/`_rightDomain`). Vertical
346
+ // charts put the category on X; horizontal charts put it on Y.
347
+ const catNonDefault = horizontal
348
+ ? !chart._zoomController?.isYDefault()
349
+ : !chart._zoomController?.isXDefault();
350
+ if (chart._autoFitValue && chart._zoomController && catNonDefault) {
361
351
  const fit = computeVisibleValueExtent(chart, visCatMin, visCatMax);
362
352
  if (fit.hasLeft) {
363
353
  visValMin = fit.leftMin;
@@ -405,32 +395,58 @@ export function renderBarFrame(
405
395
  levelLabels: chart._groupBy.slice(),
406
396
  };
407
397
 
398
+ // Categorical value-axis sizing. Y Bar puts the value axis on the
399
+ // left (so the category labels need extra `leftExtra` width); X Bar
400
+ // puts it on the bottom (extra `bottomExtra` height for the leaf
401
+ // labels). We additionally override the category-axis gutter on
402
+ // the opposite side via the existing `provisionalDomain` path.
403
+ const valueCatDomain = chart._leftValueCategoryDomain;
404
+ const valueCatActive =
405
+ chart._leftValueAxisMode === "category" &&
406
+ valueCatDomain !== null &&
407
+ valueCatDomain.numRows > 0;
408
+
408
409
  let layout: PlotLayout;
409
410
  if (horizontal) {
410
- // Numeric category axis on the Y side: the gutter just needs
411
- // standard numeric tick width (~55px), no per-row label
412
- // measurement.
411
+ // X Bar: category axis on the left (Y side), value axis on the
412
+ // bottom (X side). Categorical value axis grows the bottom
413
+ // gutter; numeric value axis uses the fixed 24px row.
413
414
  const leftExtra = numericCat
414
415
  ? 55
415
416
  : measureCategoricalAxisWidth(provisionalDomain);
416
-
417
+ const estLeft = leftExtra + (hasCatLabel ? 16 : 0);
418
+ const estRight = hasLegend ? 80 : 16;
419
+ const estPlotWidthH = Math.max(1, cssWidth - estLeft - estRight);
420
+ const bottomExtra = valueCatActive
421
+ ? measureCategoricalAxisHeight(valueCatDomain, estPlotWidthH)
422
+ : undefined;
417
423
  layout = new PlotLayout(cssWidth, cssHeight, {
418
424
  hasXLabel: true,
419
425
  hasYLabel: hasCatLabel,
420
426
  hasLegend,
421
427
  leftExtra,
428
+ bottomExtra,
422
429
  });
423
430
  } else if (numericCat) {
424
- // Numeric category axis on the X side: bottom gutter is a
425
- // fixed numeric-axis row (~24px), no leaf-rotation measurement.
431
+ // Y Bar with numeric category axis on X. Value axis (Y, left)
432
+ // may still be categorical when all aggregates are string.
433
+ const leftExtra = valueCatActive
434
+ ? measureCategoricalAxisWidth(valueCatDomain)
435
+ : undefined;
426
436
  layout = new PlotLayout(cssWidth, cssHeight, {
427
437
  hasXLabel: hasCatLabel,
428
438
  hasYLabel: true,
429
439
  hasLegend,
430
440
  bottomExtra: 24,
441
+ leftExtra,
431
442
  });
432
443
  } else {
433
- const estLeft = 55 + 16;
444
+ // Y Bar with categorical X. Value axis on the left may be
445
+ // categorical too — independently sized.
446
+ const leftExtraBase = valueCatActive
447
+ ? measureCategoricalAxisWidth(valueCatDomain)
448
+ : 55;
449
+ const estLeft = leftExtraBase + 16;
434
450
  const estRight = hasLegend ? 80 : 16;
435
451
  const estPlotWidth = Math.max(1, cssWidth - estLeft - estRight);
436
452
  const bottomExtra = measureCategoricalAxisHeight(
@@ -442,6 +458,7 @@ export function renderBarFrame(
442
458
  hasYLabel: true,
443
459
  hasLegend,
444
460
  bottomExtra,
461
+ leftExtra: valueCatActive ? leftExtraBase : undefined,
445
462
  });
446
463
  }
447
464
 
@@ -635,16 +652,44 @@ export function renderBarChromeOverlay(chart: SeriesChart): void {
635
652
  const primarySeries = chart._series.find((s) => s.axis === 0);
636
653
  const altSeries = chart._series.find((s) => s.axis === 1);
637
654
  const xColumn = chart._groupBy[0];
655
+
656
+ // Discriminate each value-axis side independently: a side becomes
657
+ // categorical when every aggregate on it is post-aggregation
658
+ // `string`-typed (the build pipeline already applied this
659
+ // all-or-nothing rule and stamped `_*ValueAxisMode`).
660
+ const valueAxis: BarValueAxis =
661
+ chart._leftValueAxisMode === "category" &&
662
+ chart._leftValueCategoryDomain
663
+ ? { mode: "category", domain: chart._leftValueCategoryDomain }
664
+ : {
665
+ mode: "numeric",
666
+ domain: chart._lastYDomain,
667
+ ticks: chart._lastYTicks,
668
+ };
669
+ let altAxis: BarValueAxis | undefined;
670
+ if (chart._lastAltYDomain && chart._lastAltYTicks) {
671
+ altAxis =
672
+ chart._rightValueAxisMode === "category" &&
673
+ chart._rightValueCategoryDomain
674
+ ? {
675
+ mode: "category",
676
+ domain: chart._rightValueCategoryDomain,
677
+ }
678
+ : {
679
+ mode: "numeric",
680
+ domain: chart._lastAltYDomain,
681
+ ticks: chart._lastAltYTicks,
682
+ };
683
+ }
684
+
638
685
  renderBarAxesChrome(
639
686
  chart._chromeCanvas,
640
687
  catAxis,
641
- chart._lastYDomain,
642
- chart._lastYTicks,
688
+ valueAxis,
643
689
  chart._lastLayout,
644
690
  theme,
645
691
  chart._glManager?.dpr ?? 1,
646
- chart._lastAltYDomain ?? undefined,
647
- chart._lastAltYTicks ?? undefined,
692
+ altAxis,
648
693
  chart._isHorizontal,
649
694
  {
650
695
  value: chart.getColumnFormatter(
@@ -880,11 +925,34 @@ function computeVisibleValueExtent(
880
925
  let hasRight = false;
881
926
 
882
927
  if (buckets.n > 0) {
883
- // Clamp to the populated [0, n-1] range. `visCat*` is in
884
- // continuous coords (numeric or category index space), so
885
- // floor/ceil to integer bucket indices.
886
- const lo = Math.max(0, Math.floor(visCatMin));
887
- const hi = Math.min(buckets.n - 1, Math.ceil(visCatMax));
928
+ // Resolve the visible catIdx range. Category mode: `visCat*` are
929
+ // already in catIdx space, so floor/ceil into `[0, n-1]`.
930
+ // Numeric mode (`date | datetime | integer | float` group_by):
931
+ // `visCat*` are absolute data values from the zoom controller's
932
+ // visible domain for a datetime axis they're ~1.7e12-magnitude
933
+ // timestamps. A blind `Math.floor(visCatMin)` of that gives `lo
934
+ // ≫ n`, the loop body never executes, and the value-axis refit
935
+ // silently no-ops (chart looks the same horizontally-zoomed as
936
+ // unzoomed). Map the data range back to catIdx via the sorted
937
+ // `_categoryPositions`. See [series-interact.ts:239-250] for the
938
+ // parallel hit-test branch.
939
+ const positions = chart._categoryPositions;
940
+ let lo: number;
941
+ let hi: number;
942
+ if (positions) {
943
+ const r = mapDomainToCatRange(
944
+ positions,
945
+ buckets.n,
946
+ visCatMin,
947
+ visCatMax,
948
+ );
949
+ lo = r.lo;
950
+ hi = r.hi;
951
+ } else {
952
+ lo = Math.max(0, Math.floor(visCatMin));
953
+ hi = Math.min(buckets.n - 1, Math.ceil(visCatMax));
954
+ }
955
+
888
956
  const lMin = buckets.leftMin;
889
957
  const lMax = buckets.leftMax;
890
958
  const rMin = buckets.rightMin;
@@ -937,6 +1005,59 @@ function computeVisibleValueExtent(
937
1005
  return next;
938
1006
  }
939
1007
 
1008
+ /**
1009
+ * Map a numeric visible domain `[visMin, visMax]` to the inclusive catIdx
1010
+ * range `[lo, hi]` that intersects it, using a sorted `categoryPositions`
1011
+ * vector (ASC, per the pivot order). Returns an empty range (`lo > hi`)
1012
+ * when the domain misses every category.
1013
+ *
1014
+ * Edges are expanded by one catIdx on each side so a category whose
1015
+ * center sits just outside the visible window — but whose band-half
1016
+ * still overlaps it — still contributes to the auto-fit extent.
1017
+ */
1018
+ function mapDomainToCatRange(
1019
+ positions: Float64Array,
1020
+ n: number,
1021
+ visMin: number,
1022
+ visMax: number,
1023
+ ): { lo: number; hi: number } {
1024
+ if (n === 0 || visMin > visMax) {
1025
+ return { lo: 0, hi: -1 };
1026
+ }
1027
+
1028
+ // Lower bound: smallest idx where positions[idx] >= visMin.
1029
+ let l = 0;
1030
+ let r = n;
1031
+ while (l < r) {
1032
+ const m = (l + r) >>> 1;
1033
+ if (positions[m] < visMin) {
1034
+ l = m + 1;
1035
+ } else {
1036
+ r = m;
1037
+ }
1038
+ }
1039
+
1040
+ const lo = Math.max(0, l - 1);
1041
+
1042
+ // Upper bound: smallest idx where positions[idx] > visMax (`l` after
1043
+ // loop). `l` itself is one past the last in-range catIdx, so the
1044
+ // inclusive `hi` for an exactly-overlapping band is `l - 1`; the
1045
+ // `+1`-then-clamp expands by one to capture partial-overlap bands.
1046
+ l = 0;
1047
+ r = n;
1048
+ while (l < r) {
1049
+ const m = (l + r) >>> 1;
1050
+ if (positions[m] <= visMax) {
1051
+ l = m + 1;
1052
+ } else {
1053
+ r = m;
1054
+ }
1055
+ }
1056
+
1057
+ const hi = Math.min(n - 1, l);
1058
+ return { lo, hi };
1059
+ }
1060
+
940
1061
  function newSeriesAutoFitCache(): SeriesAutoFitCache {
941
1062
  return {
942
1063
  catMin: 0,
@@ -13,10 +13,12 @@
13
13
  export type ChartType = "bar" | "line" | "scatter" | "area";
14
14
 
15
15
  /**
16
- * Per-column entry inside the viewer's `columns_config` map. The map itself
17
- * is typed as `Record<string, any>` at the plugin boundary because
18
- * `columns_config` is shared across plugins; this interface documents the
19
- * keys the Y-bar glyph router consumes.
16
+ * Per-column interpolation mode for line / area glyphs.
17
+ */
18
+ export type InterpolateMode = "skip" | "solid" | "transparent";
19
+
20
+ /**
21
+ * Per-column entry inside the viewer's `columns_config` map.
20
22
  */
21
23
  export interface ColumnChartConfig {
22
24
  /**
@@ -25,18 +27,24 @@ export interface ColumnChartConfig {
25
27
  chart_type?: string;
26
28
 
27
29
  /**
28
- * Explicit stack override. If omitted: bar / area stack by default,
29
- * line / scatter do not.
30
+ * Explicit stack override.
30
31
  */
31
32
  stack?: boolean;
32
33
 
33
34
  /**
34
35
  * Force this aggregate onto the secondary (right) Y axis,
35
36
  * independent of `autoAltYAxis` and the dual-axis ratio
36
- * heuristic. Missing / false → axis assignment is driven by
37
- * `autoAltYAxis` alone.
37
+ * heuristic.
38
38
  */
39
39
  alt_axis?: boolean;
40
+
41
+ /**
42
+ * Interpolation mode for line / area glyphs. See
43
+ * {@link InterpolateMode}. Legacy values `true` / `false` are also
44
+ * accepted by {@link resolveInterpolate} (mapped to `"solid"` /
45
+ * `"skip"`). Default `"skip"`. No effect on bar / scatter.
46
+ */
47
+ interpolate?: InterpolateMode;
40
48
  }
41
49
 
42
50
  /**
@@ -44,10 +52,6 @@ export interface ColumnChartConfig {
44
52
  * *base* (e.g. `"Sales"`); composite arrow columns like `"North|Sales"`
45
53
  * should strip the prefix before calling — the bar pipeline already
46
54
  * tracks aggregates as base names, so call sites pass the base directly.
47
- *
48
- * `fallback` is the plugin's default glyph (e.g. `"line"` for Y Line),
49
- * supplied by the plugin element via `setDefaultChartType`. Falls back to
50
- * `"bar"` when the plugin never set one.
51
55
  */
52
56
  export function resolveChartType(
53
57
  aggName: string,
@@ -69,8 +73,6 @@ export function resolveChartType(
69
73
 
70
74
  /**
71
75
  * Resolve whether a series stacks with its aggregate siblings.
72
- * Default: `true` for bar/area, `false` for line/scatter. Overridable
73
- * per column via `columns_config[aggName].stack`.
74
76
  */
75
77
  export function resolveStack(
76
78
  aggName: string,
@@ -87,9 +89,7 @@ export function resolveStack(
87
89
 
88
90
  /**
89
91
  * Resolve whether a column is pinned to the secondary Y axis via
90
- * `columns_config[aggName].alt_axis`. Independent of `autoAltYAxis`:
91
- * when `true`, the per-column override forces axis 1 regardless of
92
- * the auto-split heuristic.
92
+ * `columns_config[aggName].alt_axis`.
93
93
  */
94
94
  export function resolveAltAxis(
95
95
  aggName: string,
@@ -97,3 +97,23 @@ export function resolveAltAxis(
97
97
  ): boolean {
98
98
  return cfg?.[aggName]?.alt_axis === true;
99
99
  }
100
+
101
+ /**
102
+ * Resolve the interpolation mode for this aggregate.
103
+ */
104
+ export function resolveInterpolate(
105
+ aggName: string,
106
+ chartType: ChartType,
107
+ cfg: Record<string, ColumnChartConfig> | undefined,
108
+ ): InterpolateMode {
109
+ if (chartType !== "line" && chartType !== "area") {
110
+ return "skip";
111
+ }
112
+
113
+ const mode = cfg?.[aggName]?.interpolate;
114
+ if (mode === undefined || chartType === "area") {
115
+ return "solid";
116
+ }
117
+
118
+ return mode;
119
+ }
@@ -16,6 +16,7 @@ import type { ZoomConfig } from "../../interaction/zoom-controller";
16
16
  import { CategoricalYChart } from "../common/categorical-y-chart";
17
17
  import { type PlotRect } from "../../layout/plot-layout";
18
18
  import { type AxisDomain } from "../../axis/numeric-axis";
19
+ import type { CategoricalDomain } from "../../axis/categorical-axis";
19
20
  import {
20
21
  buildSeriesPipeline,
21
22
  readBarRecord,
@@ -41,6 +42,9 @@ import { resolvePalette } from "../../theme/palette";
41
42
  import { LineGlyph } from "./glyphs/draw-lines";
42
43
  import { ScatterGlyph } from "./glyphs/draw-scatter";
43
44
  import { AreaGlyph } from "./glyphs/draw-areas";
45
+ import { createQuadCornerBuffer } from "../../webgl/instanced-attrs";
46
+ import { compileProgram } from "../../webgl/program-cache";
47
+ import { expandDomainInPlace } from "../common/expand-domain";
44
48
  import barVert from "../../shaders/bar.vert.glsl";
45
49
  import barFrag from "../../shaders/bar.frag.glsl";
46
50
 
@@ -145,6 +149,22 @@ export class SeriesChart extends CategoricalYChart {
145
149
  _primaryValueLabel = "";
146
150
  _altValueLabel = "";
147
151
 
152
+ /**
153
+ * Per-side value-axis mode. `"category"` fires when every
154
+ * aggregate on that side is post-aggregation `string`-typed
155
+ * (all-or-nothing rule, evaluated independently for primary and
156
+ * alt). When set, `_bars[].y0`/`y1` carry dictionary slot indices
157
+ * instead of numeric values, and the chrome overlay paints a
158
+ * categorical axis on that side.
159
+ *
160
+ * Read by `series-render.ts` to construct the `BarCategoryAxis`
161
+ * descriptor for the value-axis sides.
162
+ */
163
+ _leftValueAxisMode: "numeric" | "category" = "numeric";
164
+ _rightValueAxisMode: "numeric" | "category" | null = null;
165
+ _leftValueCategoryDomain: CategoricalDomain | null = null;
166
+ _rightValueCategoryDomain: CategoricalDomain | null = null;
167
+
148
168
  /**
149
169
  * (seriesId * 1e9 + catIdx) → bar-record index in `_bars`. Built once
150
170
  * per pipeline run for area-strip lookups; rebuilt on hidden-toggle
@@ -430,34 +450,33 @@ export class SeriesChart extends CategoricalYChart {
430
450
  }
431
451
 
432
452
  if (!this._program) {
433
- this._program = glManager.shaders.getOrCreate(
453
+ const compiled = compileProgram<
454
+ { program: WebGLProgram } & CachedLocations
455
+ >(
456
+ glManager,
434
457
  "bar",
435
458
  barVert,
436
459
  barFrag,
460
+ [
461
+ "u_proj_left",
462
+ "u_proj_right",
463
+ "u_hover_series",
464
+ "u_horizontal",
465
+ ],
466
+ [
467
+ "a_corner",
468
+ "a_x_center",
469
+ "a_half_width",
470
+ "a_y0",
471
+ "a_y1",
472
+ "a_color",
473
+ "a_series_id",
474
+ "a_axis",
475
+ ],
437
476
  );
438
- const p = this._program;
439
- this._locations = {
440
- u_proj_left: gl.getUniformLocation(p, "u_proj_left"),
441
- u_proj_right: gl.getUniformLocation(p, "u_proj_right"),
442
- u_hover_series: gl.getUniformLocation(p, "u_hover_series"),
443
- u_horizontal: gl.getUniformLocation(p, "u_horizontal"),
444
- a_corner: gl.getAttribLocation(p, "a_corner"),
445
- a_x_center: gl.getAttribLocation(p, "a_x_center"),
446
- a_half_width: gl.getAttribLocation(p, "a_half_width"),
447
- a_y0: gl.getAttribLocation(p, "a_y0"),
448
- a_y1: gl.getAttribLocation(p, "a_y1"),
449
- a_color: gl.getAttribLocation(p, "a_color"),
450
- a_series_id: gl.getAttribLocation(p, "a_series_id"),
451
- a_axis: gl.getAttribLocation(p, "a_axis"),
452
- };
453
-
454
- this._cornerBuffer = gl.createBuffer()!;
455
- gl.bindBuffer(gl.ARRAY_BUFFER, this._cornerBuffer);
456
- gl.bufferData(
457
- gl.ARRAY_BUFFER,
458
- new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]),
459
- gl.STATIC_DRAW,
460
- );
477
+ this._program = compiled.program;
478
+ this._locations = compiled;
479
+ this._cornerBuffer = createQuadCornerBuffer(gl);
461
480
  }
462
481
 
463
482
  const result = buildSeriesPipeline({
@@ -483,57 +502,29 @@ export class SeriesChart extends CategoricalYChart {
483
502
  scratchNegStack: this._negStackScratch,
484
503
  });
485
504
 
486
- // `domain_mode: "expand"` post-build union. Mutate the pipeline
487
- // result struct in place so every downstream assignment below
488
- // (`_leftDomain`, `_rightDomain`, `_numericCategoryDomain`,
489
- // `_categoryOrigin`) automatically picks up the grown extent.
505
+ // `domain_mode: "expand"` post-build union. Each call mutates
506
+ // the pipeline result in place so the downstream assignments
507
+ // below (`_leftDomain`, `_rightDomain`, `_numericCategoryDomain`,
508
+ // `_categoryOrigin`) automatically pick up the grown extent.
490
509
  // `"fit"` (or a fresh reset) leaves the result untouched and
491
510
  // clears the accumulators so the next toggle starts fresh.
492
511
  if (this._pluginConfig.domain_mode === "expand") {
493
- if (this._expandedLeftDomain) {
494
- result.leftDomain.min = Math.min(
495
- this._expandedLeftDomain.min,
496
- result.leftDomain.min,
497
- );
498
- result.leftDomain.max = Math.max(
499
- this._expandedLeftDomain.max,
500
- result.leftDomain.max,
501
- );
502
- }
503
-
504
- this._expandedLeftDomain = { ...result.leftDomain };
505
-
512
+ this._expandedLeftDomain = expandDomainInPlace(
513
+ this._expandedLeftDomain,
514
+ result.leftDomain,
515
+ );
506
516
  if (result.rightDomain) {
507
- if (this._expandedRightDomain) {
508
- result.rightDomain.min = Math.min(
509
- this._expandedRightDomain.min,
510
- result.rightDomain.min,
511
- );
512
- result.rightDomain.max = Math.max(
513
- this._expandedRightDomain.max,
514
- result.rightDomain.max,
515
- );
516
- }
517
-
518
- this._expandedRightDomain = { ...result.rightDomain };
517
+ this._expandedRightDomain = expandDomainInPlace(
518
+ this._expandedRightDomain,
519
+ result.rightDomain,
520
+ );
519
521
  }
520
522
 
521
523
  if (result.numericCategoryDomain) {
522
- if (this._expandedCategoryDomain) {
523
- result.numericCategoryDomain.min = Math.min(
524
- this._expandedCategoryDomain.min,
525
- result.numericCategoryDomain.min,
526
- );
527
- result.numericCategoryDomain.max = Math.max(
528
- this._expandedCategoryDomain.max,
529
- result.numericCategoryDomain.max,
530
- );
531
- }
532
-
533
- this._expandedCategoryDomain = {
534
- min: result.numericCategoryDomain.min,
535
- max: result.numericCategoryDomain.max,
536
- };
524
+ this._expandedCategoryDomain = expandDomainInPlace(
525
+ this._expandedCategoryDomain,
526
+ result.numericCategoryDomain,
527
+ );
537
528
  }
538
529
  } else {
539
530
  this._expandedLeftDomain = null;
@@ -617,6 +608,10 @@ export class SeriesChart extends CategoricalYChart {
617
608
  this._leftDomain = result.leftDomain;
618
609
  this._rightDomain = result.rightDomain;
619
610
  this._hasRightAxis = result.hasRightAxis;
611
+ this._leftValueAxisMode = result.leftValueAxisMode;
612
+ this._rightValueAxisMode = result.rightValueAxisMode;
613
+ this._leftValueCategoryDomain = result.leftValueCategoryDomain;
614
+ this._rightValueCategoryDomain = result.rightValueCategoryDomain;
620
615
 
621
616
  // Resolve the palette eagerly. Both `uploadBarInstances` (color
622
617
  // attribute) and `rebuildGlyphBuffers` (per-series RGB capture)