@perspective-dev/viewer-charts 5.0.0 → 5.1.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.
@@ -14,6 +14,7 @@ import type { Context2D } from "../canvas-types";
14
14
  import type { WebGLContextManager } from "../../webgl/context-manager";
15
15
  import {
16
16
  ensurePalette,
17
+ type GlyphRun,
17
18
  type SeriesChart,
18
19
  type SeriesAutoFitCache,
19
20
  } from "./series";
@@ -114,6 +115,8 @@ export function uploadBarInstances(
114
115
  let n = 0;
115
116
 
116
117
  chart._facetBarRanges = null;
118
+ chart._facetBarAggRanges = null;
119
+ chart._barAggRanges = null;
117
120
  if (total > 0) {
118
121
  const scratch = ensureBarInstanceScratch(total);
119
122
  if (
@@ -137,33 +140,60 @@ export function uploadBarInstances(
137
140
  const by1 = bars.y1;
138
141
  const ax = bars.axis;
139
142
 
140
- // Facet mode: counting sort by splitIdx (`seriesId % P`). Pass
141
- // 1 counts eligible instances per split; prefix sums become the
142
- // per-split write cursors AND the published ranges.
143
- let writeAt: ((seriesId: number) => number) | null = null;
144
- if (faceted) {
145
- const counts = new Array<number>(P).fill(0);
146
- for (let i = 0; i < total; i++) {
147
- if (ct[i] !== BAR_TYPE_BAR || hidden.has(sid[i])) {
148
- continue;
149
- }
150
-
151
- counts[sid[i] % P]++;
143
+ // Counting sort so contiguous instance ranges exist for every
144
+ // draw grouping. Overlay: key = aggIdx, publishing
145
+ // `_barAggRanges` — a glyph run `[aggStart, aggEnd]` is one
146
+ // contiguous slice, and within-call overlap Z follows `columns`
147
+ // declaration order. Faceted: key = splitIdx-major then aggIdx,
148
+ // publishing both the per-split `_facetBarRanges` (facet
149
+ // dispatch) and the per-(split, agg) `_facetBarAggRanges` (run
150
+ // slices within a facet). Pass 1 counts eligible instances per
151
+ // bucket; prefix sums become the write cursors AND the ranges.
152
+ const M = Math.max(1, chart._aggregates.length);
153
+ const bucketOf = faceted
154
+ ? (seriesId: number) =>
155
+ (seriesId % P) * M + Math.floor(seriesId / P)
156
+ : (seriesId: number) =>
157
+ P > 0 ? Math.floor(seriesId / P) : seriesId;
158
+ const numBuckets = faceted ? P * M : M;
159
+ const counts = new Array<number>(numBuckets).fill(0);
160
+ for (let i = 0; i < total; i++) {
161
+ if (ct[i] !== BAR_TYPE_BAR || hidden.has(sid[i])) {
162
+ continue;
152
163
  }
153
164
 
154
- const ranges: { start: number; count: number }[] = [];
155
- const cursors = new Int32Array(P);
156
- let acc = 0;
157
- for (let p = 0; p < P; p++) {
158
- ranges.push({ start: acc, count: counts[p] });
159
- cursors[p] = acc;
160
- acc += counts[p];
161
- }
165
+ counts[bucketOf(sid[i])]++;
166
+ }
162
167
 
163
- chart._facetBarRanges = ranges;
164
- writeAt = (seriesId: number) => cursors[seriesId % P]++;
168
+ const ranges: { start: number; count: number }[] = [];
169
+ const cursors = new Int32Array(numBuckets);
170
+ let acc = 0;
171
+ for (let b = 0; b < numBuckets; b++) {
172
+ ranges.push({ start: acc, count: counts[b] });
173
+ cursors[b] = acc;
174
+ acc += counts[b];
165
175
  }
166
176
 
177
+ if (faceted) {
178
+ chart._barAggRanges = null;
179
+ chart._facetBarAggRanges = Array.from({ length: P }, (_, p) =>
180
+ ranges.slice(p * M, (p + 1) * M),
181
+ );
182
+ chart._facetBarRanges = Array.from({ length: P }, (_, p) => {
183
+ const first = ranges[p * M];
184
+ const last = ranges[(p + 1) * M - 1];
185
+ return {
186
+ start: first.start,
187
+ count: last.start + last.count - first.start,
188
+ };
189
+ });
190
+ } else {
191
+ chart._barAggRanges = ranges;
192
+ chart._facetBarAggRanges = null;
193
+ }
194
+
195
+ const writeAt = (seriesId: number) => cursors[bucketOf(seriesId)]++;
196
+
167
197
  for (let i = 0; i < total; i++) {
168
198
  if (ct[i] !== BAR_TYPE_BAR) {
169
199
  continue;
@@ -174,7 +204,7 @@ export function uploadBarInstances(
174
204
  continue;
175
205
  }
176
206
 
177
- const w = writeAt ? writeAt(seriesId) : n;
207
+ const w = writeAt(seriesId);
178
208
  scratch.xCenters[w] = xC[i] - xOrigin;
179
209
  scratch.halfWidths[w] = hw[i];
180
210
  scratch.y0s[w] = by0[i];
@@ -643,41 +673,18 @@ export function renderBarFrame(
643
673
  );
644
674
  }
645
675
 
676
+ const hovered = chart._series.length > 1 ? getHoveredBar(chart) : null;
646
677
  renderInPlotFrame(gl, layout, glManager.dpr, () => {
647
- // Paint order: areas behind bars (so bar borders stay crisp),
648
- // bars above, lines above those, scatter points on top. X Bar
649
- // only paints bars — the other glyphs bake in vertical geometry
650
- // and aren't supported for horizontal orientation.
651
- if (!horizontal) {
652
- chart._glyphs.areas.draw(
653
- chart,
654
- gl,
655
- glManager,
656
- projLeft,
657
- projRight,
658
- theme.areaOpacity,
659
- );
660
- }
661
-
662
- gl.useProgram(chart._program!);
663
- const loc = chart._locations!;
664
- gl.uniformMatrix4fv(loc.u_proj_left, false, projLeft);
665
- gl.uniformMatrix4fv(loc.u_proj_right, false, projRight);
666
- gl.uniform1f(loc.u_horizontal, horizontal ? 1.0 : 0.0);
667
- const hovered = chart._series.length > 1 ? getHoveredBar(chart) : null;
668
- gl.uniform1f(loc.u_hover_series, hovered ? hovered.seriesId : -1);
669
- drawBars(chart, gl, glManager);
670
-
671
- if (!horizontal) {
672
- chart._glyphs.lines.draw(chart, gl, glManager, projLeft, projRight);
673
- chart._glyphs.scatter.draw(
674
- chart,
675
- gl,
676
- glManager,
677
- projLeft,
678
- projRight,
679
- );
680
- }
678
+ drawGlyphRuns(
679
+ chart,
680
+ gl,
681
+ glManager,
682
+ projLeft,
683
+ projRight,
684
+ theme.areaOpacity,
685
+ horizontal,
686
+ hovered ? hovered.seriesId : -1,
687
+ );
681
688
  });
682
689
 
683
690
  chart._lastXDomain = catDomain;
@@ -694,6 +701,124 @@ export function renderBarFrame(
694
701
  chart._defer2D(() => renderBarChromeOverlay(chart));
695
702
  }
696
703
 
704
+ /**
705
+ * Paint every glyph in `columns` declaration Z-order — one pass per
706
+ * {@link SeriesChart._glyphRuns} entry, ascending `aggIdx`, later
707
+ * columns on top; splits within an aggregate paint in `splitIdx`
708
+ * order. A homogeneous chart is a single run and takes exactly the
709
+ * legacy one-pass path (no per-series filtering, single instanced bar
710
+ * draw). X Bar (horizontal) paints bars only — the other glyphs bake
711
+ * vertical geometry.
712
+ *
713
+ * The caller wraps this in its plot clip (`renderInPlotFrame` /
714
+ * `withScissor`); `splitFilter` is the facet index in faceted frames.
715
+ */
716
+ function drawGlyphRuns(
717
+ chart: SeriesChart,
718
+ gl: WebGL2RenderingContext | WebGLRenderingContext,
719
+ glManager: WebGLContextManager,
720
+ projLeft: Float32Array,
721
+ projRight: Float32Array,
722
+ areaOpacity: number,
723
+ horizontal: boolean,
724
+ hoveredSeriesId: number,
725
+ splitFilter?: number,
726
+ ): void {
727
+ const runs = chart._glyphRuns;
728
+ const single = runs.length <= 1;
729
+ for (const run of runs) {
730
+ const aggRange = single
731
+ ? undefined
732
+ : { start: run.aggStart, end: run.aggEnd };
733
+ switch (run.chartType) {
734
+ case "area":
735
+ if (!horizontal) {
736
+ chart._glyphs.areas.draw(
737
+ chart,
738
+ gl,
739
+ glManager,
740
+ projLeft,
741
+ projRight,
742
+ areaOpacity,
743
+ splitFilter,
744
+ aggRange,
745
+ );
746
+ }
747
+
748
+ break;
749
+ case "bar": {
750
+ gl.useProgram(chart._program!);
751
+ const loc = chart._locations!;
752
+ gl.uniformMatrix4fv(loc.u_proj_left, false, projLeft);
753
+ gl.uniformMatrix4fv(loc.u_proj_right, false, projRight);
754
+ gl.uniform1f(loc.u_horizontal, horizontal ? 1.0 : 0.0);
755
+ gl.uniform1f(loc.u_hover_series, hoveredSeriesId);
756
+ drawBars(
757
+ chart,
758
+ gl,
759
+ glManager,
760
+ barRunRange(chart, run, splitFilter),
761
+ );
762
+ break;
763
+ }
764
+
765
+ case "line":
766
+ if (!horizontal) {
767
+ chart._glyphs.lines.draw(
768
+ chart,
769
+ gl,
770
+ glManager,
771
+ projLeft,
772
+ projRight,
773
+ splitFilter,
774
+ aggRange,
775
+ );
776
+ }
777
+
778
+ break;
779
+ case "scatter":
780
+ if (!horizontal) {
781
+ chart._glyphs.scatter.draw(
782
+ chart,
783
+ gl,
784
+ glManager,
785
+ projLeft,
786
+ projRight,
787
+ splitFilter,
788
+ aggRange,
789
+ );
790
+ }
791
+
792
+ break;
793
+ }
794
+ }
795
+ }
796
+
797
+ /**
798
+ * The contiguous uploaded-instance slice for a bar run — per-aggregate
799
+ * ranges are adjacent by construction (`uploadBarInstances` counting
800
+ * sort), so the run `[aggStart, aggEnd]` spans from the first range's
801
+ * start through the last range's end. Faceted frames slice within the
802
+ * facet's split-major block.
803
+ */
804
+ function barRunRange(
805
+ chart: SeriesChart,
806
+ run: GlyphRun,
807
+ splitFilter?: number,
808
+ ): { start: number; count: number } | undefined {
809
+ const table =
810
+ splitFilter !== undefined
811
+ ? chart._facetBarAggRanges?.[splitFilter]
812
+ : chart._barAggRanges;
813
+ if (!table) {
814
+ return splitFilter !== undefined ? { start: 0, count: 0 } : undefined;
815
+ }
816
+
817
+ const first = table[run.aggStart];
818
+ const last = table[run.aggEnd];
819
+ return { start: first.start, count: last.start + last.count - first.start };
820
+ }
821
+
697
822
  /**
698
823
  * Domain window computed by `renderBarFrame`'s shared prologue (zoom
699
824
  * window + auto-fit + `include_zero`), handed to the faceted branch.
@@ -873,55 +998,17 @@ function renderFacetedBarFrame(
873
998
  }
874
999
 
875
1000
  withScissor(gl, layout, dpr, () => {
876
- // Same paint order as the single-plot path: areas behind
877
- // bars, lines above, scatter on top. X Bar draws bars only
878
- // (the other glyphs bake vertical geometry).
879
- if (!horizontal) {
880
- chart._glyphs.areas.draw(
881
- chart,
882
- gl,
883
- glManager,
884
- projLeft,
885
- projRight,
886
- theme.areaOpacity,
887
- p,
888
- );
889
- }
890
-
891
- gl.useProgram(chart._program!);
892
- const loc = chart._locations!;
893
- gl.uniformMatrix4fv(loc.u_proj_left, false, projLeft);
894
- gl.uniformMatrix4fv(loc.u_proj_right, false, projRight);
895
- gl.uniform1f(loc.u_horizontal, horizontal ? 1.0 : 0.0);
896
- gl.uniform1f(
897
- loc.u_hover_series,
898
- hovered && hovered.splitIdx === p ? hovered.seriesId : -1,
899
- );
900
- drawBars(
1001
+ drawGlyphRuns(
901
1002
  chart,
902
1003
  gl,
903
1004
  glManager,
904
- chart._facetBarRanges?.[p] ?? { start: 0, count: 0 },
1005
+ projLeft,
1006
+ projRight,
1007
+ theme.areaOpacity,
1008
+ horizontal,
1009
+ hovered && hovered.splitIdx === p ? hovered.seriesId : -1,
1010
+ p,
905
1011
  );
906
-
907
- if (!horizontal) {
908
- chart._glyphs.lines.draw(
909
- chart,
910
- gl,
911
- glManager,
912
- projLeft,
913
- projRight,
914
- p,
915
- );
916
- chart._glyphs.scatter.draw(
917
- chart,
918
- gl,
919
- glManager,
920
- projLeft,
921
- projRight,
922
- p,
923
- );
924
- }
925
1012
  });
926
1013
  }
927
1014
 
@@ -24,6 +24,7 @@ import {
24
24
  type SeriesChartRecord,
25
25
  type NumericCategoryDomain,
26
26
  type SeriesInfo,
27
+ type SeriesPipelineResult,
27
28
  type BarColumns,
28
29
  emptyBarColumns,
29
30
  } from "./series-build";
@@ -66,6 +67,16 @@ export interface SeriesAutoFitCache {
66
67
  hasRight: boolean;
67
68
  }
68
69
 
70
+ /**
71
+ * One paint pass: the contiguous span of aggregates (`columns` indices,
72
+ * inclusive) sharing a glyph type. See {@link SeriesChart._glyphRuns}.
73
+ */
74
+ export interface GlyphRun {
75
+ chartType: SeriesInfo["chartType"];
76
+ aggStart: number;
77
+ aggEnd: number;
78
+ }
79
+
69
80
  export interface CachedLocations {
70
81
  u_proj_left: WebGLUniformLocation | null;
71
82
  u_proj_right: WebGLUniformLocation | null;
@@ -153,6 +164,32 @@ export class SeriesChart extends CategoricalYChart {
153
164
  */
154
165
  _facetBarRanges: { start: number; count: number }[] | null = null;
155
166
 
167
+ /**
168
+ * Paint order of the current build: consecutive aggregates sharing
169
+ * a glyph type, merged into runs, in `columns` declaration order
170
+ * (`aggIdx` ascending — later columns paint on top). The frame
171
+ * draws one pass per run; a homogeneous chart is a single run and
172
+ * degenerates to the legacy one-pass-per-type path. `aggEnd` is
173
+ * inclusive.
174
+ */
175
+ _glyphRuns: GlyphRun[] = [];
176
+
177
+ /**
178
+ * Per-aggregate contiguous instance ranges in the uploaded bar
179
+ * buffers, indexed by `aggIdx` — the overlay-mode counterpart of
180
+ * `_facetBarRanges`, populated by `uploadBarInstances`' aggregate
181
+ * counting sort. `null` until first upload.
182
+ */
183
+ _barAggRanges: { start: number; count: number }[] | null = null;
184
+
185
+ /**
186
+ * Faceted per-(split, aggregate) instance ranges,
187
+ * `[splitIdx][aggIdx]` — instances are emitted split-major then
188
+ * aggregate-ordered, so a facet's glyph-run slice is contiguous.
189
+ * `null` in overlay mode.
190
+ */
191
+ _facetBarAggRanges: { start: number; count: number }[][] | null = null;
192
+
156
193
  /**
157
194
  * Columnar bar/area record storage. Indexed by bar slot in
158
195
  * `[0, _bars.count)`. Replaces the legacy `SeriesChartRecord[]` to
@@ -249,6 +286,20 @@ export class SeriesChart extends CategoricalYChart {
249
286
  _expandedLeftDomain: { min: number; max: number } | null = null;
250
287
  _expandedRightDomain: { min: number; max: number } | null = null;
251
288
 
289
+ /**
290
+ * The axis partition (per-aggregate axis side) the expand
291
+ * accumulators were accumulated under. An accumulator is only
292
+ * meaningful for the partition that produced it: when an aggregate
293
+ * moves sides (`columns_config.alt_axis` pin via a config-only
294
+ * `update()`, or a data-driven `auto_alt_y_axis` flip), the side it
295
+ * LEFT would otherwise retain its extent forever — the departed
296
+ * column's range never leaves the primary axis. A signature
297
+ * mismatch resets BOTH accumulators before the union. Keyed per
298
+ * AGGREGATE (not per series) so split-group growth on a streaming
299
+ * update never spuriously resets.
300
+ */
301
+ _expandedAxisSig: string | null = null;
302
+
252
303
  /**
253
304
  * Numeric category-axis state. Populated only when `group_by` has
254
305
  * exactly one level and that level is `date | datetime | integer |
@@ -564,6 +615,13 @@ export class SeriesChart extends CategoricalYChart {
564
615
  // `"fit"` (or a fresh reset) leaves the result untouched and
565
616
  // clears the accumulators so the next toggle starts fresh.
566
617
  if (this._pluginConfig.domain_mode === "expand") {
618
+ const axisSig = expandAxisSignature(result);
619
+ if (this._expandedAxisSig !== axisSig) {
620
+ this._expandedLeftDomain = null;
621
+ this._expandedRightDomain = null;
622
+ this._expandedAxisSig = axisSig;
623
+ }
624
+
567
625
  this._expandedLeftDomain = expandDomainInPlace(
568
626
  this._expandedLeftDomain,
569
627
  result.leftDomain,
@@ -578,6 +636,7 @@ export class SeriesChart extends CategoricalYChart {
578
636
  } else {
579
637
  this._expandedLeftDomain = null;
580
638
  this._expandedRightDomain = null;
639
+ this._expandedAxisSig = null;
581
640
  }
582
641
 
583
642
  this._aggregates = result.aggregates;
@@ -639,6 +698,25 @@ export class SeriesChart extends CategoricalYChart {
639
698
  this._primaryValueLabel = uniqueAggLabels(result.series, 0);
640
699
  this._altValueLabel = uniqueAggLabels(result.series, 1);
641
700
 
701
+ // Paint-order runs: every series of an aggregate shares one
702
+ // glyph type (`resolveChartType` is per-aggName), so sampling
703
+ // the aggregate's first series suffices. MUST be computed
704
+ // before `uploadBarInstances` below — the aggregate counting
705
+ // sort publishes ranges the run passes draw from.
706
+ this._glyphRuns.length = 0;
707
+ {
708
+ const P = Math.max(1, result.splitPrefixes.length);
709
+ for (let k = 0; k < result.aggregates.length; k++) {
710
+ const chartType = result.series[k * P].chartType;
711
+ const last = this._glyphRuns[this._glyphRuns.length - 1];
712
+ if (last && last.chartType === chartType) {
713
+ last.aggEnd = k;
714
+ } else {
715
+ this._glyphRuns.push({ chartType, aggStart: k, aggEnd: k });
716
+ }
717
+ }
718
+ }
719
+
642
720
  // Pre-build the area-strip lookup index (seriesId * 1e9 + catIdx
643
721
  // → bar slot). Legacy code rebuilt this every frame inside
644
722
  // `drawAreas`. The index is derived purely from `_bars` and is
@@ -657,6 +735,17 @@ export class SeriesChart extends CategoricalYChart {
657
735
  this._paletteCacheKey = null;
658
736
  this._catExtentsHidden = null;
659
737
  this._lastUploadedColors = null;
738
+
739
+ // Hover records index into `_bars` / `_series` and MUST NOT
740
+ // survive a build that replaces them: bar-column capacity is
741
+ // reused across builds, so a stale `_hoveredBarIdx` reads a
742
+ // leftover `seriesId` past the new series set (and a retained
743
+ // `_hoveredSample` carries one verbatim), crashing the tooltip
744
+ // pass of the next present when the pointer rests over a glyph
745
+ // through a host `restore`. The next mousemove re-hit-tests
746
+ // against the new data.
747
+ this._hoveredBarIdx = -1;
748
+ this._hoveredSample = null;
660
749
  this._sampleValid = result.sampleValid;
661
750
  this._leftDomain = result.leftDomain;
662
751
  this._rightDomain = result.rightDomain;
@@ -691,6 +780,7 @@ export class SeriesChart extends CategoricalYChart {
691
780
  override resetExpandedDomain(): void {
692
781
  this._expandedLeftDomain = null;
693
782
  this._expandedRightDomain = null;
783
+ this._expandedAxisSig = null;
694
784
  }
695
785
 
696
786
  protected destroyInternal(): void {
@@ -709,6 +799,9 @@ export class SeriesChart extends CategoricalYChart {
709
799
  this._facetActive = false;
710
800
  this._facetGrid = null;
711
801
  this._facetBarRanges = null;
802
+ this._facetBarAggRanges = null;
803
+ this._barAggRanges = null;
804
+ this._glyphRuns.length = 0;
712
805
  this._bars = emptyBarColumns();
713
806
  this._series = [];
714
807
  this._barSeries = [];
@@ -724,6 +817,8 @@ export class SeriesChart extends CategoricalYChart {
724
817
  this._rowPaths = [];
725
818
  this._numCategories = 0;
726
819
  this._hiddenSeries.clear();
820
+ this._hoveredBarIdx = -1;
821
+ this._hoveredSample = null;
727
822
  }
728
823
  }
729
824
 
@@ -770,6 +865,24 @@ function uniqueAggLabels(series: SeriesInfo[], axis: 0 | 1): string {
770
865
  return ordered.join(", ");
771
866
  }
772
867
 
868
+ /**
869
+ * Per-aggregate axis-partition signature for the `domain_mode:
870
+ * "expand"` accumulators. Invariant: every series of an aggregate
871
+ * shares one axis side (both axis-override loops in the build pipeline
872
+ * assign by `aggIdx`), so sampling the aggregate's first series
873
+ * suffices. NUL-joined — aggregate names may contain any printable
874
+ * separator.
875
+ */
876
+ function expandAxisSignature(result: SeriesPipelineResult): string {
877
+ const P = Math.max(1, result.splitPrefixes.length);
878
+ const parts: string[] = [];
879
+ for (let k = 0; k < result.aggregates.length; k++) {
880
+ parts.push(`${result.aggregates[k]}:${result.series[k * P].axis}`);
881
+ }
882
+
883
+ return parts.join("\u0000");
884
+ }
885
+
773
886
  /**
774
887
  * Resolve the per-series palette and stamp it onto `_series[i].color`.
775
888
  * Cached on `_paletteCache` keyed by reference identity of the theme
@@ -171,6 +171,16 @@ export class HTMLPerspectiveViewerWebGLPluginElement
171
171
  */
172
172
  private _pluginConfigStore: PluginConfig | null = null;
173
173
 
174
+ /**
175
+ * Per-column config (`alt_axis`, `chart_type`, formats, …), held on
176
+ * the element for the same reason as `_pluginConfigStore`: the host
177
+ * calls `restore()` BEFORE the first draw builds the renderer, so
178
+ * forwarding only to a live renderer silently drops the initial
179
+ * `columns_config` — `_buildRenderer` ships it in the `InitMsg`
180
+ * instead.
181
+ */
182
+ private _columnsConfig: Record<string, any> = {};
183
+
174
184
  private get _pluginConfig(): PluginConfig {
175
185
  if (!this._pluginConfigStore) {
176
186
  this._pluginConfigStore = this._effectiveDefaults();
@@ -396,6 +406,7 @@ export class HTMLPerspectiveViewerWebGLPluginElement
396
406
  zoom_mode: this._pluginConfig.facet_zoom_mode,
397
407
  },
398
408
  pluginConfig: this._pluginConfig,
409
+ columnsConfig: this._columnsConfig,
399
410
  defaultChartType: this._chartType.default_chart_type,
400
411
  renderBlitMode: BLIT_MODE,
401
412
  });
@@ -693,8 +704,9 @@ export class HTMLPerspectiveViewerWebGLPluginElement
693
704
  ...config,
694
705
  };
695
706
 
707
+ this._columnsConfig = columns_config ?? {};
696
708
  this._renderer?.setPluginConfig(this._pluginConfig);
697
- this._renderer?.setColumnsConfig(columns_config ?? {});
709
+ this._renderer?.setColumnsConfig(this._columnsConfig);
698
710
  }
699
711
 
700
712
  delete() {
@@ -162,6 +162,15 @@ export interface InitMsg {
162
162
  * a `setPluginConfig` control msg.
163
163
  */
164
164
  pluginConfig: PluginConfig;
165
+
166
+ /**
167
+ * Initial per-column config (`alt_axis`, `chart_type`, formats, …).
168
+ * Seeds the chart impl before the first `loadAndRender` — the host
169
+ * calls `plugin.restore` before the renderer exists, so without
170
+ * this the initial `columns_config` never reaches the worker.
171
+ * Later changes arrive as `setColumnsConfig` control msgs.
172
+ */
173
+ columnsConfig?: Record<string, any>;
165
174
  defaultChartType?: string;
166
175
 
167
176
  /**
@@ -258,6 +258,7 @@ export class RendererTransport {
258
258
  chrome: HTMLCanvasElement;
259
259
  facetConfig: FacetConfig;
260
260
  pluginConfig: PluginConfig;
261
+ columnsConfig?: Record<string, any>;
261
262
  defaultChartType?: string;
262
263
  renderBlitMode: "blit" | "direct";
263
264
  }): Promise<void> {
@@ -339,6 +340,7 @@ export class RendererTransport {
339
340
  tableName: this._tableName,
340
341
  facetConfig: opts.facetConfig,
341
342
  pluginConfig: opts.pluginConfig,
343
+ columnsConfig: opts.columnsConfig,
342
344
  defaultChartType: opts.defaultChartType,
343
345
  themeVars,
344
346
  fontFaces,
@@ -577,6 +579,17 @@ export class RendererTransport {
577
579
  return Promise.resolve();
578
580
  }
579
581
 
582
+ const dpr = window.devicePixelRatio || 1;
583
+ const last = this._lastPostedSize;
584
+ if (
585
+ last &&
586
+ Math.abs(last.cssWidth - cssWidth) <= 0.5 &&
587
+ Math.abs(last.cssHeight - cssHeight) <= 0.5 &&
588
+ last.dpr === dpr
589
+ ) {
590
+ return Promise.resolve();
591
+ }
592
+
580
593
  this._holdPresent = true;
581
594
  return this._postResize(cssWidth, cssHeight).then(
582
595
  () => () => this._presentStaged(),
@@ -204,6 +204,9 @@ export class WorkerRenderer {
204
204
 
205
205
  this.chartImpl.setFacetConfig?.(msg.facetConfig);
206
206
  this.chartImpl.setPluginConfig?.(msg.pluginConfig);
207
+ if (msg.columnsConfig) {
208
+ this.chartImpl.setColumnsConfig?.(msg.columnsConfig);
209
+ }
207
210
 
208
211
  if (this.chartImpl.setZoomController) {
209
212
  this.zoomController = new ZoomController();