@perspective-dev/viewer-charts 4.5.1 → 5.0.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 +3 -3
- package/dist/esm/charts/candlestick/candlestick.d.ts +8 -10
- package/dist/esm/charts/chart-base.d.ts +58 -4
- package/dist/esm/charts/chart.d.ts +45 -9
- package/dist/esm/charts/series/glyphs/draw-areas.d.ts +3 -2
- package/dist/esm/charts/series/glyphs/draw-bars.d.ts +8 -1
- package/dist/esm/charts/series/glyphs/draw-lines.d.ts +3 -2
- package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +5 -2
- package/dist/esm/charts/series/series-build.d.ts +11 -0
- package/dist/esm/charts/series/series-interact.d.ts +10 -0
- package/dist/esm/charts/series/series-render.d.ts +17 -1
- package/dist/esm/charts/series/series.d.ts +53 -10
- package/dist/esm/config.d.ts +19 -0
- package/dist/esm/data/view-reader.d.ts +10 -1
- package/dist/esm/event-detail.d.ts +7 -0
- package/dist/esm/layout/ticks.d.ts +5 -0
- package/dist/esm/perspective-viewer-charts.js +2 -2
- package/dist/esm/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/plugin/charts.d.ts +6 -3
- package/dist/esm/plugin/plugin.d.ts +46 -4
- package/dist/esm/render/scheduler.d.ts +34 -8
- package/dist/esm/transport/protocol.d.ts +31 -2
- package/dist/esm/transport/renderer-transport.d.ts +74 -3
- package/dist/esm/webgl/context-manager.d.ts +100 -117
- package/dist/esm/webgl/context-pool.d.ts +40 -0
- package/dist/esm/webgl/gl-context.d.ts +54 -0
- package/dist/esm/webgl/instanced-attrs.d.ts +7 -1
- package/dist/esm/worker/renderer.worker.d.ts +12 -0
- package/package.json +1 -1
- package/src/ts/charts/candlestick/candlestick-render.ts +4 -1
- package/src/ts/charts/candlestick/candlestick.ts +12 -17
- package/src/ts/charts/cartesian/cartesian-render.ts +80 -63
- package/src/ts/charts/chart-base.ts +85 -5
- package/src/ts/charts/chart.ts +47 -9
- package/src/ts/charts/common/category-axis-resolver.ts +5 -1
- package/src/ts/charts/common/tree-data.ts +61 -16
- package/src/ts/charts/heatmap/heatmap-render.ts +6 -2
- package/src/ts/charts/series/glyphs/draw-areas.ts +10 -1
- package/src/ts/charts/series/glyphs/draw-bars.ts +30 -9
- package/src/ts/charts/series/glyphs/draw-lines.ts +10 -1
- package/src/ts/charts/series/glyphs/draw-scatter.ts +56 -3
- package/src/ts/charts/series/series-build.ts +21 -3
- package/src/ts/charts/series/series-interact.ts +86 -13
- package/src/ts/charts/series/series-render.ts +660 -17
- package/src/ts/charts/series/series.ts +96 -26
- package/src/ts/charts/sunburst/sunburst-interact.ts +2 -3
- package/src/ts/charts/sunburst/sunburst-render.ts +3 -1
- package/src/ts/charts/treemap/treemap-interact.ts +2 -5
- package/src/ts/charts/treemap/treemap-render.ts +3 -1
- package/src/ts/charts/treemap/treemap.ts +6 -1
- package/src/ts/config.ts +21 -1
- package/src/ts/data/view-reader.ts +22 -2
- package/src/ts/event-detail.ts +8 -0
- package/src/ts/index.ts +13 -7
- package/src/ts/interaction/raw-event-forwarder.ts +7 -0
- package/src/ts/interaction/zoom-controller.ts +8 -0
- package/src/ts/layout/ticks.ts +9 -0
- package/src/ts/plugin/charts.ts +20 -4
- package/src/ts/plugin/plugin.ts +157 -23
- package/src/ts/render/scheduler.ts +164 -55
- package/src/ts/transport/protocol.ts +33 -0
- package/src/ts/transport/renderer-transport.ts +185 -9
- package/src/ts/webgl/context-manager.ts +195 -189
- package/src/ts/webgl/context-pool.ts +78 -0
- package/src/ts/webgl/gl-context.ts +191 -0
- package/src/ts/webgl/instanced-attrs.ts +15 -1
- package/src/ts/worker/dispatch.ts +4 -1
- package/src/ts/worker/renderer.worker.ts +94 -22
- package/src/ts/worker/session-host.ts +29 -0
|
@@ -19,12 +19,20 @@ import {
|
|
|
19
19
|
} from "./series";
|
|
20
20
|
import type { PlotRect } from "../../layout/plot-layout";
|
|
21
21
|
import { PlotLayout } from "../../layout/plot-layout";
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
clearAndSetupFrame,
|
|
24
|
+
renderInPlotFrame,
|
|
25
|
+
withScissor,
|
|
26
|
+
} from "../../webgl/plot-frame";
|
|
23
27
|
import { renderCanvasTooltip } from "../../interaction/tooltip-controller";
|
|
24
28
|
import { drawBars, BAR_TYPE_BAR_VAL as BAR_TYPE_BAR } from "./glyphs/draw-bars";
|
|
25
29
|
import { getHoveredBar } from "./series-interact";
|
|
26
30
|
import { computeNiceTicks } from "../../layout/ticks";
|
|
27
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
renderOuterXAxis,
|
|
33
|
+
renderOuterYAxis,
|
|
34
|
+
type AxisDomain,
|
|
35
|
+
} from "../../axis/numeric-axis";
|
|
28
36
|
import {
|
|
29
37
|
renderBarAxesChrome,
|
|
30
38
|
renderBarGridlines,
|
|
@@ -34,8 +42,19 @@ import {
|
|
|
34
42
|
import {
|
|
35
43
|
measureCategoricalAxisHeight,
|
|
36
44
|
measureCategoricalAxisWidth,
|
|
45
|
+
renderCategoricalXTicks,
|
|
46
|
+
renderCategoricalYTicks,
|
|
37
47
|
type CategoricalDomain,
|
|
38
48
|
} from "../../axis/categorical-axis";
|
|
49
|
+
import {
|
|
50
|
+
bottomRowLayouts,
|
|
51
|
+
buildFacetGrid,
|
|
52
|
+
leftColumnLayouts,
|
|
53
|
+
type FacetGrid,
|
|
54
|
+
} from "../../layout/facet-grid";
|
|
55
|
+
import { drawFacetTitle } from "../../axis/facet-chrome";
|
|
56
|
+
import { getScaledContext, initCanvas } from "../../axis/canvas";
|
|
57
|
+
import { drawGridlinesX, drawGridlinesY } from "../../axis/axis-primitives";
|
|
39
58
|
import { buildBarTooltipLines } from "./series-interact";
|
|
40
59
|
|
|
41
60
|
/**
|
|
@@ -77,6 +96,12 @@ function ensureBarInstanceScratch(n: number): BarInstanceScratch {
|
|
|
77
96
|
|
|
78
97
|
/**
|
|
79
98
|
* Upload bar instance buffers from the columnar `_bars` storage.
|
|
99
|
+
*
|
|
100
|
+
* Overlay mode emits instances in `_bars` order. Facet-grid mode
|
|
101
|
+
* (`_facetActive`) emits them split-major via a counting sort so each
|
|
102
|
+
* facet's instances form a contiguous range — recorded on
|
|
103
|
+
* `chart._facetBarRanges` and drawn per facet with an instance-offset
|
|
104
|
+
* `drawBars` call.
|
|
80
105
|
*/
|
|
81
106
|
export function uploadBarInstances(
|
|
82
107
|
chart: SeriesChart,
|
|
@@ -84,8 +109,11 @@ export function uploadBarInstances(
|
|
|
84
109
|
): void {
|
|
85
110
|
const bars = chart._bars;
|
|
86
111
|
const total = bars.count;
|
|
112
|
+
const P = chart._splitPrefixes.length;
|
|
113
|
+
const faceted = chart._facetActive && P > 0;
|
|
87
114
|
let n = 0;
|
|
88
115
|
|
|
116
|
+
chart._facetBarRanges = null;
|
|
89
117
|
if (total > 0) {
|
|
90
118
|
const scratch = ensureBarInstanceScratch(total);
|
|
91
119
|
if (
|
|
@@ -108,6 +136,34 @@ export function uploadBarInstances(
|
|
|
108
136
|
const by0 = bars.y0;
|
|
109
137
|
const by1 = bars.y1;
|
|
110
138
|
const ax = bars.axis;
|
|
139
|
+
|
|
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]++;
|
|
152
|
+
}
|
|
153
|
+
|
|
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
|
+
}
|
|
162
|
+
|
|
163
|
+
chart._facetBarRanges = ranges;
|
|
164
|
+
writeAt = (seriesId: number) => cursors[seriesId % P]++;
|
|
165
|
+
}
|
|
166
|
+
|
|
111
167
|
for (let i = 0; i < total; i++) {
|
|
112
168
|
if (ct[i] !== BAR_TYPE_BAR) {
|
|
113
169
|
continue;
|
|
@@ -118,19 +174,25 @@ export function uploadBarInstances(
|
|
|
118
174
|
continue;
|
|
119
175
|
}
|
|
120
176
|
|
|
121
|
-
|
|
122
|
-
scratch.
|
|
123
|
-
scratch.
|
|
124
|
-
scratch.
|
|
125
|
-
scratch.
|
|
126
|
-
scratch.
|
|
177
|
+
const w = writeAt ? writeAt(seriesId) : n;
|
|
178
|
+
scratch.xCenters[w] = xC[i] - xOrigin;
|
|
179
|
+
scratch.halfWidths[w] = hw[i];
|
|
180
|
+
scratch.y0s[w] = by0[i];
|
|
181
|
+
scratch.y1s[w] = by1[i];
|
|
182
|
+
scratch.seriesIds[w] = seriesId;
|
|
183
|
+
scratch.axes[w] = ax[i];
|
|
127
184
|
const color = series[seriesId].color;
|
|
128
|
-
scratch.colors[
|
|
129
|
-
scratch.colors[
|
|
130
|
-
scratch.colors[
|
|
131
|
-
indices[
|
|
185
|
+
scratch.colors[w * 3] = color[0];
|
|
186
|
+
scratch.colors[w * 3 + 1] = color[1];
|
|
187
|
+
scratch.colors[w * 3 + 2] = color[2];
|
|
188
|
+
indices[w] = i;
|
|
132
189
|
n++;
|
|
133
190
|
}
|
|
191
|
+
} else if (faceted) {
|
|
192
|
+
chart._facetBarRanges = Array.from({ length: P }, () => ({
|
|
193
|
+
start: 0,
|
|
194
|
+
count: 0,
|
|
195
|
+
}));
|
|
134
196
|
}
|
|
135
197
|
|
|
136
198
|
chart._uploadedBars = n;
|
|
@@ -386,6 +448,28 @@ export function renderBarFrame(
|
|
|
386
448
|
}
|
|
387
449
|
}
|
|
388
450
|
|
|
451
|
+
// Facet-grid branch — one sub-plot per split group. Keys off the
|
|
452
|
+
// build-time `_facetActive` stamp (NOT the live `_facetConfig`) so
|
|
453
|
+
// this frame's render mode always matches the stack geometry the
|
|
454
|
+
// current `_bars` were built with.
|
|
455
|
+
if (chart._facetActive && chart._splitPrefixes.length > 1) {
|
|
456
|
+
renderFacetedBarFrame(chart, glManager, {
|
|
457
|
+
horizontal,
|
|
458
|
+
numericCat,
|
|
459
|
+
cssWidth,
|
|
460
|
+
cssHeight,
|
|
461
|
+
visCatMin,
|
|
462
|
+
visCatMax,
|
|
463
|
+
visValMin,
|
|
464
|
+
visValMax,
|
|
465
|
+
visRightMin,
|
|
466
|
+
visRightMax,
|
|
467
|
+
});
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
chart._facetGrid = null;
|
|
472
|
+
|
|
389
473
|
const hasLegend = chart._series.length > 1;
|
|
390
474
|
const hasCatLabel = chart._groupBy.length > 0;
|
|
391
475
|
|
|
@@ -604,7 +688,313 @@ export function renderBarFrame(
|
|
|
604
688
|
chart._lastCatTicks = numericCat
|
|
605
689
|
? computeNiceTicks(visCatMin, visCatMax, 6)
|
|
606
690
|
: null;
|
|
607
|
-
|
|
691
|
+
// Deferred past the GPU fence (see `_defer2D`) so the chrome canvas
|
|
692
|
+
// doesn't present ahead of the GL glyphs on resize. Reads the
|
|
693
|
+
// `_last*` frame state set above.
|
|
694
|
+
chart._defer2D(() => renderBarChromeOverlay(chart));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Domain window computed by `renderBarFrame`'s shared prologue (zoom
|
|
699
|
+
* window + auto-fit + `include_zero`), handed to the faceted branch.
|
|
700
|
+
*/
|
|
701
|
+
interface FacetedFrameCtx {
|
|
702
|
+
horizontal: boolean;
|
|
703
|
+
numericCat: boolean;
|
|
704
|
+
cssWidth: number;
|
|
705
|
+
cssHeight: number;
|
|
706
|
+
visCatMin: number;
|
|
707
|
+
visCatMax: number;
|
|
708
|
+
visValMin: number;
|
|
709
|
+
visValMax: number;
|
|
710
|
+
visRightMin: number;
|
|
711
|
+
visRightMax: number;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Faceted frame — one sub-plot per split group, laid out by
|
|
716
|
+
* `buildFacetGrid`. Geometry contract with the build pipeline
|
|
717
|
+
* (`facetSplits`): every split's records share the same band-slot
|
|
718
|
+
* coordinates and per-split stack baselines, so a facet is exactly
|
|
719
|
+
* "the single-plot render restricted to one split" — same domains,
|
|
720
|
+
* same projection math, different `PlotLayout` + instance subset.
|
|
721
|
+
*
|
|
722
|
+
* Axis policy: the value axis is shared (one outer band, one domain
|
|
723
|
+
* for every facet); the category axis paints per-cell when
|
|
724
|
+
* categorical (the outer painters are numeric-only — same compromise
|
|
725
|
+
* as the cartesian faceted path) and shared-outer when numeric. Zoom
|
|
726
|
+
* is shared: `syncFacetZoomLayouts` keeps the single controller's
|
|
727
|
+
* layout on cell 0, and every facet renders the same visible window.
|
|
728
|
+
*
|
|
729
|
+
* Known limitation: the grid reserves no outer band for a secondary
|
|
730
|
+
* value axis, so dual-axis series project correctly inside each facet
|
|
731
|
+
* but the right-axis chrome is suppressed (`_lastAltYDomain = null`).
|
|
732
|
+
*/
|
|
733
|
+
function renderFacetedBarFrame(
|
|
734
|
+
chart: SeriesChart,
|
|
735
|
+
glManager: WebGLContextManager,
|
|
736
|
+
ctx: FacetedFrameCtx,
|
|
737
|
+
): void {
|
|
738
|
+
const gl = glManager.gl;
|
|
739
|
+
const dpr = glManager.dpr;
|
|
740
|
+
const theme = chart._resolveTheme();
|
|
741
|
+
const {
|
|
742
|
+
horizontal,
|
|
743
|
+
numericCat,
|
|
744
|
+
cssWidth,
|
|
745
|
+
cssHeight,
|
|
746
|
+
visCatMin,
|
|
747
|
+
visCatMax,
|
|
748
|
+
visValMin,
|
|
749
|
+
visValMax,
|
|
750
|
+
visRightMin,
|
|
751
|
+
visRightMax,
|
|
752
|
+
} = ctx;
|
|
753
|
+
|
|
754
|
+
const hasCatLabel = chart._groupBy.length > 0;
|
|
755
|
+
const valueCatDomain = chart._leftValueCategoryDomain;
|
|
756
|
+
const valueCatActive =
|
|
757
|
+
chart._leftValueAxisMode === "category" &&
|
|
758
|
+
valueCatDomain !== null &&
|
|
759
|
+
valueCatDomain.numRows > 0;
|
|
760
|
+
|
|
761
|
+
// Facets absorb the split dimension and color follows the
|
|
762
|
+
// aggregate, so only multiple aggregates warrant a legend.
|
|
763
|
+
const hasLegend = chart._aggregates.length > 1;
|
|
764
|
+
|
|
765
|
+
// `buildFacetGrid` axis modes are named in CANVAS orientation
|
|
766
|
+
// (x = bottom band, y = left band); map the logical category /
|
|
767
|
+
// value sides onto them per chart orientation.
|
|
768
|
+
const catAxisMode = numericCat ? ("outer" as const) : ("cell" as const);
|
|
769
|
+
const valAxisMode = valueCatActive ? ("cell" as const) : ("outer" as const);
|
|
770
|
+
|
|
771
|
+
const grid: FacetGrid = buildFacetGrid(chart._splitPrefixes, {
|
|
772
|
+
cssWidth,
|
|
773
|
+
cssHeight,
|
|
774
|
+
xAxis: horizontal ? valAxisMode : catAxisMode,
|
|
775
|
+
yAxis: horizontal ? catAxisMode : valAxisMode,
|
|
776
|
+
hasLegend,
|
|
777
|
+
hasXLabel: horizontal ? true : hasCatLabel,
|
|
778
|
+
hasYLabel: horizontal ? hasCatLabel : true,
|
|
779
|
+
gap: chart._facetConfig.facet_padding,
|
|
780
|
+
});
|
|
781
|
+
chart._facetGrid = grid;
|
|
782
|
+
chart._lastLayout = grid.cells[0]?.layout ?? null;
|
|
783
|
+
if (grid.cells.length === 0 || !chart._lastLayout) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
chart.syncFacetZoomLayouts(grid.cells);
|
|
788
|
+
|
|
789
|
+
const leftValueTicks = computeNiceTicks(visValMin, visValMax, 6);
|
|
790
|
+
const catTicks = numericCat
|
|
791
|
+
? computeNiceTicks(visCatMin, visCatMax, 6)
|
|
792
|
+
: null;
|
|
793
|
+
|
|
794
|
+
const sampleLayout = grid.cells[0].layout;
|
|
795
|
+
const gridlineCanvas = chart._gridlineCanvas;
|
|
796
|
+
if (gridlineCanvas) {
|
|
797
|
+
// Deferred FIRST so the destructive resize/clear runs before
|
|
798
|
+
// the per-cell gridline closures below (FIFO flush order).
|
|
799
|
+
chart._defer2D(() => initCanvas(gridlineCanvas, sampleLayout, dpr));
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const requireZero = chart._pluginConfig.include_zero;
|
|
803
|
+
const hovered = chart._series.length > 1 ? getHoveredBar(chart) : null;
|
|
804
|
+
const hasRight =
|
|
805
|
+
chart._hasRightAxis && chart._rightDomain !== null && !horizontal;
|
|
806
|
+
|
|
807
|
+
clearAndSetupFrame(gl);
|
|
808
|
+
for (let p = 0; p < grid.cells.length; p++) {
|
|
809
|
+
const cell = grid.cells[p];
|
|
810
|
+
const layout = cell.layout;
|
|
811
|
+
|
|
812
|
+
// Same projection args as the single-plot path, on the cell's
|
|
813
|
+
// layout. Seeds the cell's padded domain, which the deferred
|
|
814
|
+
// gridline closure, chrome overlay, and hover hit-test read.
|
|
815
|
+
const projLeft = horizontal
|
|
816
|
+
? layout.buildProjectionMatrix(
|
|
817
|
+
visValMin,
|
|
818
|
+
visValMax,
|
|
819
|
+
visCatMax,
|
|
820
|
+
visCatMin,
|
|
821
|
+
"x",
|
|
822
|
+
requireZero,
|
|
823
|
+
undefined,
|
|
824
|
+
0,
|
|
825
|
+
chart._categoryOrigin,
|
|
826
|
+
)
|
|
827
|
+
: layout.buildProjectionMatrix(
|
|
828
|
+
visCatMin,
|
|
829
|
+
visCatMax,
|
|
830
|
+
visValMin,
|
|
831
|
+
visValMax,
|
|
832
|
+
"y",
|
|
833
|
+
requireZero,
|
|
834
|
+
undefined,
|
|
835
|
+
chart._categoryOrigin,
|
|
836
|
+
0,
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
let projRight = projLeft;
|
|
840
|
+
if (hasRight) {
|
|
841
|
+
const savedPadXMin = layout.paddedXMin;
|
|
842
|
+
const savedPadXMax = layout.paddedXMax;
|
|
843
|
+
const savedPadYMin = layout.paddedYMin;
|
|
844
|
+
const savedPadYMax = layout.paddedYMax;
|
|
845
|
+
projRight = layout.buildProjectionMatrix(
|
|
846
|
+
visCatMin,
|
|
847
|
+
visCatMax,
|
|
848
|
+
visRightMin,
|
|
849
|
+
visRightMax,
|
|
850
|
+
"y",
|
|
851
|
+
requireZero,
|
|
852
|
+
undefined,
|
|
853
|
+
chart._categoryOrigin,
|
|
854
|
+
0,
|
|
855
|
+
);
|
|
856
|
+
layout.paddedXMin = savedPadXMin;
|
|
857
|
+
layout.paddedXMax = savedPadXMax;
|
|
858
|
+
layout.paddedYMin = savedPadYMin;
|
|
859
|
+
layout.paddedYMax = savedPadYMax;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
if (gridlineCanvas) {
|
|
863
|
+
chart._defer2D(() =>
|
|
864
|
+
renderBarGridlinesCell(
|
|
865
|
+
gridlineCanvas,
|
|
866
|
+
layout,
|
|
867
|
+
leftValueTicks,
|
|
868
|
+
theme,
|
|
869
|
+
dpr,
|
|
870
|
+
horizontal,
|
|
871
|
+
),
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
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(
|
|
901
|
+
chart,
|
|
902
|
+
gl,
|
|
903
|
+
glManager,
|
|
904
|
+
chart._facetBarRanges?.[p] ?? { start: 0, count: 0 },
|
|
905
|
+
);
|
|
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
|
+
});
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
chart._lastXDomain = {
|
|
929
|
+
levels: chart._rowPaths,
|
|
930
|
+
numRows: chart._numCategories,
|
|
931
|
+
levelLabels: chart._groupBy.slice(),
|
|
932
|
+
};
|
|
933
|
+
chart._lastYDomain = {
|
|
934
|
+
min: visValMin,
|
|
935
|
+
max: visValMax,
|
|
936
|
+
label: chart._primaryValueLabel,
|
|
937
|
+
};
|
|
938
|
+
chart._lastYTicks = leftValueTicks;
|
|
939
|
+
chart._lastAltYDomain = null;
|
|
940
|
+
chart._lastAltYTicks = null;
|
|
941
|
+
chart._lastCatTicks = catTicks;
|
|
942
|
+
chart._defer2D(() => renderBarChromeOverlay(chart));
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Per-cell value-axis gridlines for the faceted frame. Non-destructive
|
|
947
|
+
* counterpart to {@link renderBarGridlines} — the shared gridline
|
|
948
|
+
* canvas is `initCanvas`'d once per frame, then each facet paints into
|
|
949
|
+
* it via `getScaledContext`.
|
|
950
|
+
*/
|
|
951
|
+
function renderBarGridlinesCell(
|
|
952
|
+
canvas: NonNullable<SeriesChart["_gridlineCanvas"]>,
|
|
953
|
+
layout: PlotLayout,
|
|
954
|
+
valueTicks: number[],
|
|
955
|
+
theme: ReturnType<SeriesChart["_resolveTheme"]>,
|
|
956
|
+
dpr: number,
|
|
957
|
+
horizontal: boolean,
|
|
958
|
+
): void {
|
|
959
|
+
const ctx = getScaledContext(canvas, dpr);
|
|
960
|
+
if (!ctx) {
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
ctx.strokeStyle = theme.gridlineColor;
|
|
965
|
+
ctx.lineWidth = 1;
|
|
966
|
+
if (horizontal) {
|
|
967
|
+
drawGridlinesX(
|
|
968
|
+
ctx,
|
|
969
|
+
layout.plotRect,
|
|
970
|
+
valueTicks,
|
|
971
|
+
(v) => layout.dataToPixel(v, 0).px,
|
|
972
|
+
);
|
|
973
|
+
} else {
|
|
974
|
+
drawGridlinesY(
|
|
975
|
+
ctx,
|
|
976
|
+
layout.plotRect,
|
|
977
|
+
valueTicks,
|
|
978
|
+
(v) => layout.dataToPixel(0, v).py,
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Resolve the `PlotLayout` a record renders in: the record's facet
|
|
985
|
+
* cell in faceted frames, the single plot layout otherwise. Tooltip /
|
|
986
|
+
* pin positioning must map through this so faceted anchors land in the
|
|
987
|
+
* record's own cell.
|
|
988
|
+
*/
|
|
989
|
+
export function layoutForRecord(
|
|
990
|
+
chart: SeriesChart,
|
|
991
|
+
b: { splitIdx: number },
|
|
992
|
+
): PlotLayout | null {
|
|
993
|
+
if (chart._facetGrid) {
|
|
994
|
+
return chart._facetGrid.cells[b.splitIdx]?.layout ?? null;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return chart._lastLayout;
|
|
608
998
|
}
|
|
609
999
|
|
|
610
1000
|
/**
|
|
@@ -620,6 +1010,11 @@ export function renderBarChromeOverlay(chart: SeriesChart): void {
|
|
|
620
1010
|
return;
|
|
621
1011
|
}
|
|
622
1012
|
|
|
1013
|
+
if (chart._facetGrid) {
|
|
1014
|
+
renderFacetedBarChromeOverlay(chart);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
623
1018
|
const theme = chart._resolveTheme();
|
|
624
1019
|
let catAxis: BarCategoryAxis;
|
|
625
1020
|
if (
|
|
@@ -708,6 +1103,248 @@ export function renderBarChromeOverlay(chart: SeriesChart): void {
|
|
|
708
1103
|
}
|
|
709
1104
|
}
|
|
710
1105
|
|
|
1106
|
+
/**
|
|
1107
|
+
* Chrome overlay for the faceted frame. The canvas is `initCanvas`'d
|
|
1108
|
+
* once, then every painter goes through `getScaledContext` — per-cell
|
|
1109
|
+
* axis frames, per-cell categorical ticks, shared outer numeric
|
|
1110
|
+
* bands, facet titles, the aggregate legend, and the tooltip (mapped
|
|
1111
|
+
* through the hovered record's own cell).
|
|
1112
|
+
*/
|
|
1113
|
+
function renderFacetedBarChromeOverlay(chart: SeriesChart): void {
|
|
1114
|
+
const grid = chart._facetGrid!;
|
|
1115
|
+
const canvas = chart._chromeCanvas!;
|
|
1116
|
+
const theme = chart._resolveTheme();
|
|
1117
|
+
const dpr = chart._glManager?.dpr ?? 1;
|
|
1118
|
+
const horizontal = chart._isHorizontal;
|
|
1119
|
+
const hasCatLabel = chart._groupBy.length > 0;
|
|
1120
|
+
const numericCat =
|
|
1121
|
+
chart._categoryAxisMode === "numeric" &&
|
|
1122
|
+
chart._numericCategoryDomain !== null &&
|
|
1123
|
+
chart._lastCatTicks !== null;
|
|
1124
|
+
|
|
1125
|
+
if (!initCanvas(canvas, chart._lastLayout!, dpr)) {
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
const catDomain = chart._lastXDomain;
|
|
1130
|
+
const valueDomain = chart._lastYDomain!;
|
|
1131
|
+
const valueTicks = chart._lastYTicks!;
|
|
1132
|
+
const valueCatDomain = chart._leftValueCategoryDomain;
|
|
1133
|
+
const valueCatActive =
|
|
1134
|
+
chart._leftValueAxisMode === "category" &&
|
|
1135
|
+
valueCatDomain !== null &&
|
|
1136
|
+
valueCatDomain.numRows > 0;
|
|
1137
|
+
|
|
1138
|
+
const primarySeries = chart._series.find((s) => s.axis === 0);
|
|
1139
|
+
const valueFmt = chart.getColumnFormatter(
|
|
1140
|
+
primarySeries?.aggName ?? null,
|
|
1141
|
+
"tick",
|
|
1142
|
+
);
|
|
1143
|
+
const catFmt = chart.getColumnFormatter(chart._groupBy[0], "tick");
|
|
1144
|
+
|
|
1145
|
+
for (const cell of grid.cells) {
|
|
1146
|
+
const layout = cell.layout;
|
|
1147
|
+
const plot = layout.plotRect;
|
|
1148
|
+
const ctx = getScaledContext(canvas, dpr);
|
|
1149
|
+
if (!ctx) {
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
ctx.strokeStyle = theme.axisLineColor;
|
|
1154
|
+
ctx.lineWidth = 1;
|
|
1155
|
+
ctx.beginPath();
|
|
1156
|
+
ctx.moveTo(plot.x, plot.y);
|
|
1157
|
+
ctx.lineTo(plot.x, plot.y + plot.height);
|
|
1158
|
+
ctx.lineTo(plot.x + plot.width, plot.y + plot.height);
|
|
1159
|
+
ctx.stroke();
|
|
1160
|
+
|
|
1161
|
+
// Categorical sides paint per cell (the outer band painters
|
|
1162
|
+
// are numeric-only); numeric sides are painted once below
|
|
1163
|
+
// into the shared outer bands.
|
|
1164
|
+
if (!horizontal) {
|
|
1165
|
+
if (!numericCat && catDomain) {
|
|
1166
|
+
renderCategoricalXTicks(ctx, layout, catDomain, theme);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
if (valueCatActive) {
|
|
1170
|
+
renderCategoricalYTicks(ctx, layout, valueCatDomain!, theme);
|
|
1171
|
+
}
|
|
1172
|
+
} else {
|
|
1173
|
+
if (!numericCat && catDomain) {
|
|
1174
|
+
renderCategoricalYTicks(ctx, layout, catDomain, theme);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (valueCatActive) {
|
|
1178
|
+
renderCategoricalXTicks(ctx, layout, valueCatDomain!, theme);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (cell.titleRect) {
|
|
1183
|
+
drawFacetTitle(canvas, cell.label, cell.titleRect, theme, dpr);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const numericCatAxisDomain: AxisDomain | null = numericCat
|
|
1188
|
+
? {
|
|
1189
|
+
min: chart._numericCategoryDomain!.min,
|
|
1190
|
+
max: chart._numericCategoryDomain!.max,
|
|
1191
|
+
isDate: chart._numericCategoryDomain!.isDate,
|
|
1192
|
+
label: chart._numericCategoryDomain!.label,
|
|
1193
|
+
}
|
|
1194
|
+
: null;
|
|
1195
|
+
|
|
1196
|
+
if (!horizontal) {
|
|
1197
|
+
if (numericCatAxisDomain && grid.outerXAxisRect) {
|
|
1198
|
+
renderOuterXAxis(
|
|
1199
|
+
canvas,
|
|
1200
|
+
grid.outerXAxisRect,
|
|
1201
|
+
numericCatAxisDomain,
|
|
1202
|
+
chart._lastCatTicks!,
|
|
1203
|
+
bottomRowLayouts(grid),
|
|
1204
|
+
theme,
|
|
1205
|
+
hasCatLabel,
|
|
1206
|
+
dpr,
|
|
1207
|
+
catFmt,
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
if (!valueCatActive && grid.outerYAxisRect) {
|
|
1212
|
+
renderOuterYAxis(
|
|
1213
|
+
canvas,
|
|
1214
|
+
grid.outerYAxisRect,
|
|
1215
|
+
valueDomain,
|
|
1216
|
+
valueTicks,
|
|
1217
|
+
leftColumnLayouts(grid),
|
|
1218
|
+
theme,
|
|
1219
|
+
true,
|
|
1220
|
+
dpr,
|
|
1221
|
+
valueFmt,
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
} else {
|
|
1225
|
+
if (numericCatAxisDomain && grid.outerYAxisRect) {
|
|
1226
|
+
renderOuterYAxis(
|
|
1227
|
+
canvas,
|
|
1228
|
+
grid.outerYAxisRect,
|
|
1229
|
+
numericCatAxisDomain,
|
|
1230
|
+
chart._lastCatTicks!,
|
|
1231
|
+
leftColumnLayouts(grid),
|
|
1232
|
+
theme,
|
|
1233
|
+
hasCatLabel,
|
|
1234
|
+
dpr,
|
|
1235
|
+
catFmt,
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
if (!valueCatActive && grid.outerXAxisRect) {
|
|
1240
|
+
renderOuterXAxis(
|
|
1241
|
+
canvas,
|
|
1242
|
+
grid.outerXAxisRect,
|
|
1243
|
+
valueDomain,
|
|
1244
|
+
valueTicks,
|
|
1245
|
+
bottomRowLayouts(grid),
|
|
1246
|
+
theme,
|
|
1247
|
+
true,
|
|
1248
|
+
dpr,
|
|
1249
|
+
valueFmt,
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
renderFacetedBarLegend(chart, grid);
|
|
1255
|
+
|
|
1256
|
+
if (getHoveredBar(chart)) {
|
|
1257
|
+
renderBarTooltipCanvas(chart);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Aggregate-level legend for the faceted frame, painted into the
|
|
1263
|
+
* grid's shared right gutter. One entry per aggregate (facets absorb
|
|
1264
|
+
* the split dimension; every split of an aggregate shares its color —
|
|
1265
|
+
* see `ensurePalette`). A legend toggle targets the aggregate's full
|
|
1266
|
+
* seriesId set, so hiding "Sales" hides it in every facet at once; an
|
|
1267
|
+
* entry reads as hidden only when ALL of its series are hidden.
|
|
1268
|
+
*/
|
|
1269
|
+
function renderFacetedBarLegend(chart: SeriesChart, grid: FacetGrid): void {
|
|
1270
|
+
chart._legendRects = [];
|
|
1271
|
+
if (!chart._chromeCanvas || !grid.legendRect) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const M = chart._aggregates.length;
|
|
1276
|
+
if (M <= 1) {
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
const ctx = chart._chromeCanvas.getContext("2d") as Context2D | null;
|
|
1281
|
+
if (!ctx) {
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
ctx.save();
|
|
1286
|
+
|
|
1287
|
+
const theme = chart._resolveTheme();
|
|
1288
|
+
const swatchSize = 10;
|
|
1289
|
+
const lineHeight = 18;
|
|
1290
|
+
const x = grid.legendRect.x + 12;
|
|
1291
|
+
let y = grid.legendRect.y + 10;
|
|
1292
|
+
|
|
1293
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
1294
|
+
ctx.textAlign = "left";
|
|
1295
|
+
ctx.textBaseline = "middle";
|
|
1296
|
+
|
|
1297
|
+
for (let k = 0; k < M; k++) {
|
|
1298
|
+
const seriesIds: number[] = [];
|
|
1299
|
+
let color: [number, number, number] = [0.5, 0.5, 0.5];
|
|
1300
|
+
for (const s of chart._series) {
|
|
1301
|
+
if (s.aggIdx === k) {
|
|
1302
|
+
seriesIds.push(s.seriesId);
|
|
1303
|
+
color = s.color;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const label = chart._aggregates[k];
|
|
1308
|
+
const hidden = seriesIds.every((sid) => chart._hiddenSeries.has(sid));
|
|
1309
|
+
const r = Math.round(color[0] * 255);
|
|
1310
|
+
const g = Math.round(color[1] * 255);
|
|
1311
|
+
const b = Math.round(color[2] * 255);
|
|
1312
|
+
|
|
1313
|
+
ctx.globalAlpha = hidden ? 0.3 : 1.0;
|
|
1314
|
+
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
|
1315
|
+
ctx.fillRect(x, y - swatchSize / 2, swatchSize, swatchSize);
|
|
1316
|
+
|
|
1317
|
+
ctx.fillStyle = theme.legendText;
|
|
1318
|
+
ctx.fillText(label, x + swatchSize + 6, y);
|
|
1319
|
+
|
|
1320
|
+
const textW = ctx.measureText(label).width;
|
|
1321
|
+
if (hidden) {
|
|
1322
|
+
ctx.strokeStyle = theme.legendText;
|
|
1323
|
+
ctx.lineWidth = 1;
|
|
1324
|
+
ctx.beginPath();
|
|
1325
|
+
ctx.moveTo(x + swatchSize + 6, y);
|
|
1326
|
+
ctx.lineTo(x + swatchSize + 6 + textW, y);
|
|
1327
|
+
ctx.stroke();
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
ctx.globalAlpha = 1.0;
|
|
1331
|
+
|
|
1332
|
+
chart._legendRects.push({
|
|
1333
|
+
seriesIds,
|
|
1334
|
+
rect: {
|
|
1335
|
+
x: x - 2,
|
|
1336
|
+
y: y - lineHeight / 2,
|
|
1337
|
+
width: swatchSize + 6 + textW + 4,
|
|
1338
|
+
height: lineHeight,
|
|
1339
|
+
},
|
|
1340
|
+
});
|
|
1341
|
+
|
|
1342
|
+
y += lineHeight;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
ctx.restore();
|
|
1346
|
+
}
|
|
1347
|
+
|
|
711
1348
|
/**
|
|
712
1349
|
* Cached parallel array of measured legend text widths. The legend
|
|
713
1350
|
* renderer reads from this each frame instead of re-running
|
|
@@ -807,7 +1444,7 @@ function renderBarLegend(chart: SeriesChart): void {
|
|
|
807
1444
|
width: swatchSize + 6 + textW + 4,
|
|
808
1445
|
height: lineHeight,
|
|
809
1446
|
};
|
|
810
|
-
chart._legendRects.push({
|
|
1447
|
+
chart._legendRects.push({ seriesIds: [s.seriesId], rect });
|
|
811
1448
|
|
|
812
1449
|
y += lineHeight;
|
|
813
1450
|
}
|
|
@@ -825,7 +1462,12 @@ function renderBarTooltipCanvas(chart: SeriesChart): void {
|
|
|
825
1462
|
return;
|
|
826
1463
|
}
|
|
827
1464
|
|
|
828
|
-
|
|
1465
|
+
// Faceted frames anchor in the record's own cell; single-plot
|
|
1466
|
+
// frames fall through to `_lastLayout`.
|
|
1467
|
+
const layout = layoutForRecord(chart, b);
|
|
1468
|
+
if (!layout) {
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
829
1471
|
|
|
830
1472
|
// Bar glyphs anchor the tooltip at the midpoint of the bar body so
|
|
831
1473
|
// it reads against a solid swatch. Line / scatter / area glyphs
|
|
@@ -842,7 +1484,7 @@ function renderBarTooltipCanvas(chart: SeriesChart): void {
|
|
|
842
1484
|
? chart._isHorizontal
|
|
843
1485
|
? layout.dataToPixel(anchorV, b.xCenter)
|
|
844
1486
|
: layout.dataToPixel(b.xCenter, anchorV)
|
|
845
|
-
: rightAxisDataToPixel(chart, b.xCenter, anchorV);
|
|
1487
|
+
: rightAxisDataToPixel(chart, b.xCenter, anchorV, layout);
|
|
846
1488
|
|
|
847
1489
|
const lines = buildBarTooltipLines(chart, b);
|
|
848
1490
|
const theme = chart._resolveTheme();
|
|
@@ -860,8 +1502,9 @@ export function rightAxisDataToPixel(
|
|
|
860
1502
|
chart: SeriesChart,
|
|
861
1503
|
x: number,
|
|
862
1504
|
y: number,
|
|
1505
|
+
layoutOverride?: PlotLayout,
|
|
863
1506
|
): { px: number; py: number } {
|
|
864
|
-
const layout = chart._lastLayout!;
|
|
1507
|
+
const layout = layoutOverride ?? chart._lastLayout!;
|
|
865
1508
|
const { x: px, y: py, width, height } = layout.plotRect;
|
|
866
1509
|
const tx =
|
|
867
1510
|
(x - layout.paddedXMin) / (layout.paddedXMax - layout.paddedXMin);
|