@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.
- package/dist/cdn/perspective-viewer-charts.js +2 -2
- package/dist/cdn/perspective-viewer-charts.js.map +2 -2
- package/dist/esm/charts/series/glyphs/draw-areas.d.ts +7 -2
- package/dist/esm/charts/series/glyphs/draw-lines.d.ts +7 -2
- package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +7 -4
- package/dist/esm/charts/series/series-interact.d.ts +3 -2
- package/dist/esm/charts/series/series.d.ts +51 -0
- package/dist/esm/perspective-viewer-charts.js +2 -2
- package/dist/esm/perspective-viewer-charts.js.map +2 -2
- package/dist/esm/plugin/plugin.d.ts +9 -0
- package/dist/esm/transport/protocol.d.ts +8 -0
- package/dist/esm/transport/renderer-transport.d.ts +1 -0
- package/package.json +1 -1
- package/src/ts/charts/series/glyphs/draw-areas.ts +12 -1
- package/src/ts/charts/series/glyphs/draw-lines.ts +12 -1
- package/src/ts/charts/series/glyphs/draw-scatter.ts +17 -5
- package/src/ts/charts/series/series-interact.ts +137 -78
- package/src/ts/charts/series/series-render.ts +189 -102
- package/src/ts/charts/series/series.ts +113 -0
- package/src/ts/plugin/plugin.ts +13 -1
- package/src/ts/transport/protocol.ts +9 -0
- package/src/ts/transport/renderer-transport.ts +13 -0
- package/src/ts/worker/renderer.worker.ts +3 -0
|
@@ -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
|
-
//
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
164
|
-
|
|
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
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/src/ts/plugin/plugin.ts
CHANGED
|
@@ -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(
|
|
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();
|