@perspective-dev/viewer-charts 4.5.2 → 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 +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 +8 -2
- package/dist/esm/charts/series/glyphs/draw-bars.d.ts +8 -1
- package/dist/esm/charts/series/glyphs/draw-lines.d.ts +8 -2
- package/dist/esm/charts/series/glyphs/draw-scatter.d.ts +8 -2
- package/dist/esm/charts/series/series-build.d.ts +11 -0
- package/dist/esm/charts/series/series-interact.d.ts +13 -2
- package/dist/esm/charts/series/series-render.d.ts +17 -1
- package/dist/esm/charts/series/series.d.ts +104 -10
- 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 +40 -2
- package/dist/esm/render/scheduler.d.ts +11 -8
- package/dist/esm/transport/protocol.d.ts +39 -2
- package/dist/esm/transport/renderer-transport.d.ts +75 -3
- 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 +21 -1
- package/src/ts/charts/series/glyphs/draw-bars.ts +30 -9
- package/src/ts/charts/series/glyphs/draw-lines.ts +21 -1
- package/src/ts/charts/series/glyphs/draw-scatter.ts +68 -3
- package/src/ts/charts/series/series-build.ts +21 -3
- package/src/ts/charts/series/series-interact.ts +210 -78
- package/src/ts/charts/series/series-render.ts +779 -49
- package/src/ts/charts/series/series.ts +209 -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/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 +99 -17
- package/src/ts/render/scheduler.ts +39 -13
- package/src/ts/transport/protocol.ts +42 -0
- package/src/ts/transport/renderer-transport.ts +198 -9
- package/src/ts/webgl/instanced-attrs.ts +15 -1
- package/src/ts/worker/dispatch.ts +4 -1
- package/src/ts/worker/renderer.worker.ts +35 -3
|
@@ -14,17 +14,26 @@ 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";
|
|
20
21
|
import type { PlotRect } from "../../layout/plot-layout";
|
|
21
22
|
import { PlotLayout } from "../../layout/plot-layout";
|
|
22
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
clearAndSetupFrame,
|
|
25
|
+
renderInPlotFrame,
|
|
26
|
+
withScissor,
|
|
27
|
+
} from "../../webgl/plot-frame";
|
|
23
28
|
import { renderCanvasTooltip } from "../../interaction/tooltip-controller";
|
|
24
29
|
import { drawBars, BAR_TYPE_BAR_VAL as BAR_TYPE_BAR } from "./glyphs/draw-bars";
|
|
25
30
|
import { getHoveredBar } from "./series-interact";
|
|
26
31
|
import { computeNiceTicks } from "../../layout/ticks";
|
|
27
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
renderOuterXAxis,
|
|
34
|
+
renderOuterYAxis,
|
|
35
|
+
type AxisDomain,
|
|
36
|
+
} from "../../axis/numeric-axis";
|
|
28
37
|
import {
|
|
29
38
|
renderBarAxesChrome,
|
|
30
39
|
renderBarGridlines,
|
|
@@ -34,8 +43,19 @@ import {
|
|
|
34
43
|
import {
|
|
35
44
|
measureCategoricalAxisHeight,
|
|
36
45
|
measureCategoricalAxisWidth,
|
|
46
|
+
renderCategoricalXTicks,
|
|
47
|
+
renderCategoricalYTicks,
|
|
37
48
|
type CategoricalDomain,
|
|
38
49
|
} from "../../axis/categorical-axis";
|
|
50
|
+
import {
|
|
51
|
+
bottomRowLayouts,
|
|
52
|
+
buildFacetGrid,
|
|
53
|
+
leftColumnLayouts,
|
|
54
|
+
type FacetGrid,
|
|
55
|
+
} from "../../layout/facet-grid";
|
|
56
|
+
import { drawFacetTitle } from "../../axis/facet-chrome";
|
|
57
|
+
import { getScaledContext, initCanvas } from "../../axis/canvas";
|
|
58
|
+
import { drawGridlinesX, drawGridlinesY } from "../../axis/axis-primitives";
|
|
39
59
|
import { buildBarTooltipLines } from "./series-interact";
|
|
40
60
|
|
|
41
61
|
/**
|
|
@@ -77,6 +97,12 @@ function ensureBarInstanceScratch(n: number): BarInstanceScratch {
|
|
|
77
97
|
|
|
78
98
|
/**
|
|
79
99
|
* Upload bar instance buffers from the columnar `_bars` storage.
|
|
100
|
+
*
|
|
101
|
+
* Overlay mode emits instances in `_bars` order. Facet-grid mode
|
|
102
|
+
* (`_facetActive`) emits them split-major via a counting sort so each
|
|
103
|
+
* facet's instances form a contiguous range — recorded on
|
|
104
|
+
* `chart._facetBarRanges` and drawn per facet with an instance-offset
|
|
105
|
+
* `drawBars` call.
|
|
80
106
|
*/
|
|
81
107
|
export function uploadBarInstances(
|
|
82
108
|
chart: SeriesChart,
|
|
@@ -84,8 +110,13 @@ export function uploadBarInstances(
|
|
|
84
110
|
): void {
|
|
85
111
|
const bars = chart._bars;
|
|
86
112
|
const total = bars.count;
|
|
113
|
+
const P = chart._splitPrefixes.length;
|
|
114
|
+
const faceted = chart._facetActive && P > 0;
|
|
87
115
|
let n = 0;
|
|
88
116
|
|
|
117
|
+
chart._facetBarRanges = null;
|
|
118
|
+
chart._facetBarAggRanges = null;
|
|
119
|
+
chart._barAggRanges = null;
|
|
89
120
|
if (total > 0) {
|
|
90
121
|
const scratch = ensureBarInstanceScratch(total);
|
|
91
122
|
if (
|
|
@@ -108,6 +139,61 @@ export function uploadBarInstances(
|
|
|
108
139
|
const by0 = bars.y0;
|
|
109
140
|
const by1 = bars.y1;
|
|
110
141
|
const ax = bars.axis;
|
|
142
|
+
|
|
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;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
counts[bucketOf(sid[i])]++;
|
|
166
|
+
}
|
|
167
|
+
|
|
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];
|
|
175
|
+
}
|
|
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
|
+
|
|
111
197
|
for (let i = 0; i < total; i++) {
|
|
112
198
|
if (ct[i] !== BAR_TYPE_BAR) {
|
|
113
199
|
continue;
|
|
@@ -118,19 +204,25 @@ export function uploadBarInstances(
|
|
|
118
204
|
continue;
|
|
119
205
|
}
|
|
120
206
|
|
|
121
|
-
|
|
122
|
-
scratch.
|
|
123
|
-
scratch.
|
|
124
|
-
scratch.
|
|
125
|
-
scratch.
|
|
126
|
-
scratch.
|
|
207
|
+
const w = writeAt(seriesId);
|
|
208
|
+
scratch.xCenters[w] = xC[i] - xOrigin;
|
|
209
|
+
scratch.halfWidths[w] = hw[i];
|
|
210
|
+
scratch.y0s[w] = by0[i];
|
|
211
|
+
scratch.y1s[w] = by1[i];
|
|
212
|
+
scratch.seriesIds[w] = seriesId;
|
|
213
|
+
scratch.axes[w] = ax[i];
|
|
127
214
|
const color = series[seriesId].color;
|
|
128
|
-
scratch.colors[
|
|
129
|
-
scratch.colors[
|
|
130
|
-
scratch.colors[
|
|
131
|
-
indices[
|
|
215
|
+
scratch.colors[w * 3] = color[0];
|
|
216
|
+
scratch.colors[w * 3 + 1] = color[1];
|
|
217
|
+
scratch.colors[w * 3 + 2] = color[2];
|
|
218
|
+
indices[w] = i;
|
|
132
219
|
n++;
|
|
133
220
|
}
|
|
221
|
+
} else if (faceted) {
|
|
222
|
+
chart._facetBarRanges = Array.from({ length: P }, () => ({
|
|
223
|
+
start: 0,
|
|
224
|
+
count: 0,
|
|
225
|
+
}));
|
|
134
226
|
}
|
|
135
227
|
|
|
136
228
|
chart._uploadedBars = n;
|
|
@@ -386,6 +478,28 @@ export function renderBarFrame(
|
|
|
386
478
|
}
|
|
387
479
|
}
|
|
388
480
|
|
|
481
|
+
// Facet-grid branch — one sub-plot per split group. Keys off the
|
|
482
|
+
// build-time `_facetActive` stamp (NOT the live `_facetConfig`) so
|
|
483
|
+
// this frame's render mode always matches the stack geometry the
|
|
484
|
+
// current `_bars` were built with.
|
|
485
|
+
if (chart._facetActive && chart._splitPrefixes.length > 1) {
|
|
486
|
+
renderFacetedBarFrame(chart, glManager, {
|
|
487
|
+
horizontal,
|
|
488
|
+
numericCat,
|
|
489
|
+
cssWidth,
|
|
490
|
+
cssHeight,
|
|
491
|
+
visCatMin,
|
|
492
|
+
visCatMax,
|
|
493
|
+
visValMin,
|
|
494
|
+
visValMax,
|
|
495
|
+
visRightMin,
|
|
496
|
+
visRightMax,
|
|
497
|
+
});
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
chart._facetGrid = null;
|
|
502
|
+
|
|
389
503
|
const hasLegend = chart._series.length > 1;
|
|
390
504
|
const hasCatLabel = chart._groupBy.length > 0;
|
|
391
505
|
|
|
@@ -559,52 +673,415 @@ export function renderBarFrame(
|
|
|
559
673
|
);
|
|
560
674
|
}
|
|
561
675
|
|
|
676
|
+
const hovered = chart._series.length > 1 ? getHoveredBar(chart) : null;
|
|
562
677
|
renderInPlotFrame(gl, layout, glManager.dpr, () => {
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
678
|
+
drawGlyphRuns(
|
|
679
|
+
chart,
|
|
680
|
+
gl,
|
|
681
|
+
glManager,
|
|
682
|
+
projLeft,
|
|
683
|
+
projRight,
|
|
684
|
+
theme.areaOpacity,
|
|
685
|
+
horizontal,
|
|
686
|
+
hovered ? hovered.seriesId : -1,
|
|
687
|
+
);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
chart._lastXDomain = catDomain;
|
|
691
|
+
chart._lastYDomain = valueDomain;
|
|
692
|
+
chart._lastYTicks = leftValueTicks;
|
|
693
|
+
chart._lastAltYDomain = altValueDomain;
|
|
694
|
+
chart._lastAltYTicks = rightValueTicks;
|
|
695
|
+
chart._lastCatTicks = numericCat
|
|
696
|
+
? computeNiceTicks(visCatMin, visCatMax, 6)
|
|
697
|
+
: null;
|
|
698
|
+
// Deferred past the GPU fence (see `_defer2D`) so the chrome canvas
|
|
699
|
+
// doesn't present ahead of the GL glyphs on resize. Reads the
|
|
700
|
+
// `_last*` frame state set above.
|
|
701
|
+
chart._defer2D(() => renderBarChromeOverlay(chart));
|
|
702
|
+
}
|
|
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
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Domain window computed by `renderBarFrame`'s shared prologue (zoom
|
|
824
|
+
* window + auto-fit + `include_zero`), handed to the faceted branch.
|
|
825
|
+
*/
|
|
826
|
+
interface FacetedFrameCtx {
|
|
827
|
+
horizontal: boolean;
|
|
828
|
+
numericCat: boolean;
|
|
829
|
+
cssWidth: number;
|
|
830
|
+
cssHeight: number;
|
|
831
|
+
visCatMin: number;
|
|
832
|
+
visCatMax: number;
|
|
833
|
+
visValMin: number;
|
|
834
|
+
visValMax: number;
|
|
835
|
+
visRightMin: number;
|
|
836
|
+
visRightMax: number;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Faceted frame — one sub-plot per split group, laid out by
|
|
841
|
+
* `buildFacetGrid`. Geometry contract with the build pipeline
|
|
842
|
+
* (`facetSplits`): every split's records share the same band-slot
|
|
843
|
+
* coordinates and per-split stack baselines, so a facet is exactly
|
|
844
|
+
* "the single-plot render restricted to one split" — same domains,
|
|
845
|
+
* same projection math, different `PlotLayout` + instance subset.
|
|
846
|
+
*
|
|
847
|
+
* Axis policy: the value axis is shared (one outer band, one domain
|
|
848
|
+
* for every facet); the category axis paints per-cell when
|
|
849
|
+
* categorical (the outer painters are numeric-only — same compromise
|
|
850
|
+
* as the cartesian faceted path) and shared-outer when numeric. Zoom
|
|
851
|
+
* is shared: `syncFacetZoomLayouts` keeps the single controller's
|
|
852
|
+
* layout on cell 0, and every facet renders the same visible window.
|
|
853
|
+
*
|
|
854
|
+
* Known limitation: the grid reserves no outer band for a secondary
|
|
855
|
+
* value axis, so dual-axis series project correctly inside each facet
|
|
856
|
+
* but the right-axis chrome is suppressed (`_lastAltYDomain = null`).
|
|
857
|
+
*/
|
|
858
|
+
function renderFacetedBarFrame(
|
|
859
|
+
chart: SeriesChart,
|
|
860
|
+
glManager: WebGLContextManager,
|
|
861
|
+
ctx: FacetedFrameCtx,
|
|
862
|
+
): void {
|
|
863
|
+
const gl = glManager.gl;
|
|
864
|
+
const dpr = glManager.dpr;
|
|
865
|
+
const theme = chart._resolveTheme();
|
|
866
|
+
const {
|
|
867
|
+
horizontal,
|
|
868
|
+
numericCat,
|
|
869
|
+
cssWidth,
|
|
870
|
+
cssHeight,
|
|
871
|
+
visCatMin,
|
|
872
|
+
visCatMax,
|
|
873
|
+
visValMin,
|
|
874
|
+
visValMax,
|
|
875
|
+
visRightMin,
|
|
876
|
+
visRightMax,
|
|
877
|
+
} = ctx;
|
|
878
|
+
|
|
879
|
+
const hasCatLabel = chart._groupBy.length > 0;
|
|
880
|
+
const valueCatDomain = chart._leftValueCategoryDomain;
|
|
881
|
+
const valueCatActive =
|
|
882
|
+
chart._leftValueAxisMode === "category" &&
|
|
883
|
+
valueCatDomain !== null &&
|
|
884
|
+
valueCatDomain.numRows > 0;
|
|
885
|
+
|
|
886
|
+
// Facets absorb the split dimension and color follows the
|
|
887
|
+
// aggregate, so only multiple aggregates warrant a legend.
|
|
888
|
+
const hasLegend = chart._aggregates.length > 1;
|
|
889
|
+
|
|
890
|
+
// `buildFacetGrid` axis modes are named in CANVAS orientation
|
|
891
|
+
// (x = bottom band, y = left band); map the logical category /
|
|
892
|
+
// value sides onto them per chart orientation.
|
|
893
|
+
const catAxisMode = numericCat ? ("outer" as const) : ("cell" as const);
|
|
894
|
+
const valAxisMode = valueCatActive ? ("cell" as const) : ("outer" as const);
|
|
895
|
+
|
|
896
|
+
const grid: FacetGrid = buildFacetGrid(chart._splitPrefixes, {
|
|
897
|
+
cssWidth,
|
|
898
|
+
cssHeight,
|
|
899
|
+
xAxis: horizontal ? valAxisMode : catAxisMode,
|
|
900
|
+
yAxis: horizontal ? catAxisMode : valAxisMode,
|
|
901
|
+
hasLegend,
|
|
902
|
+
hasXLabel: horizontal ? true : hasCatLabel,
|
|
903
|
+
hasYLabel: horizontal ? hasCatLabel : true,
|
|
904
|
+
gap: chart._facetConfig.facet_padding,
|
|
905
|
+
});
|
|
906
|
+
chart._facetGrid = grid;
|
|
907
|
+
chart._lastLayout = grid.cells[0]?.layout ?? null;
|
|
908
|
+
if (grid.cells.length === 0 || !chart._lastLayout) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
chart.syncFacetZoomLayouts(grid.cells);
|
|
913
|
+
|
|
914
|
+
const leftValueTicks = computeNiceTicks(visValMin, visValMax, 6);
|
|
915
|
+
const catTicks = numericCat
|
|
916
|
+
? computeNiceTicks(visCatMin, visCatMax, 6)
|
|
917
|
+
: null;
|
|
918
|
+
|
|
919
|
+
const sampleLayout = grid.cells[0].layout;
|
|
920
|
+
const gridlineCanvas = chart._gridlineCanvas;
|
|
921
|
+
if (gridlineCanvas) {
|
|
922
|
+
// Deferred FIRST so the destructive resize/clear runs before
|
|
923
|
+
// the per-cell gridline closures below (FIFO flush order).
|
|
924
|
+
chart._defer2D(() => initCanvas(gridlineCanvas, sampleLayout, dpr));
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const requireZero = chart._pluginConfig.include_zero;
|
|
928
|
+
const hovered = chart._series.length > 1 ? getHoveredBar(chart) : null;
|
|
929
|
+
const hasRight =
|
|
930
|
+
chart._hasRightAxis && chart._rightDomain !== null && !horizontal;
|
|
931
|
+
|
|
932
|
+
clearAndSetupFrame(gl);
|
|
933
|
+
for (let p = 0; p < grid.cells.length; p++) {
|
|
934
|
+
const cell = grid.cells[p];
|
|
935
|
+
const layout = cell.layout;
|
|
936
|
+
|
|
937
|
+
// Same projection args as the single-plot path, on the cell's
|
|
938
|
+
// layout. Seeds the cell's padded domain, which the deferred
|
|
939
|
+
// gridline closure, chrome overlay, and hover hit-test read.
|
|
940
|
+
const projLeft = horizontal
|
|
941
|
+
? layout.buildProjectionMatrix(
|
|
942
|
+
visValMin,
|
|
943
|
+
visValMax,
|
|
944
|
+
visCatMax,
|
|
945
|
+
visCatMin,
|
|
946
|
+
"x",
|
|
947
|
+
requireZero,
|
|
948
|
+
undefined,
|
|
949
|
+
0,
|
|
950
|
+
chart._categoryOrigin,
|
|
951
|
+
)
|
|
952
|
+
: layout.buildProjectionMatrix(
|
|
953
|
+
visCatMin,
|
|
954
|
+
visCatMax,
|
|
955
|
+
visValMin,
|
|
956
|
+
visValMax,
|
|
957
|
+
"y",
|
|
958
|
+
requireZero,
|
|
959
|
+
undefined,
|
|
960
|
+
chart._categoryOrigin,
|
|
961
|
+
0,
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
let projRight = projLeft;
|
|
965
|
+
if (hasRight) {
|
|
966
|
+
const savedPadXMin = layout.paddedXMin;
|
|
967
|
+
const savedPadXMax = layout.paddedXMax;
|
|
968
|
+
const savedPadYMin = layout.paddedYMin;
|
|
969
|
+
const savedPadYMax = layout.paddedYMax;
|
|
970
|
+
projRight = layout.buildProjectionMatrix(
|
|
971
|
+
visCatMin,
|
|
972
|
+
visCatMax,
|
|
973
|
+
visRightMin,
|
|
974
|
+
visRightMax,
|
|
975
|
+
"y",
|
|
976
|
+
requireZero,
|
|
977
|
+
undefined,
|
|
978
|
+
chart._categoryOrigin,
|
|
979
|
+
0,
|
|
575
980
|
);
|
|
981
|
+
layout.paddedXMin = savedPadXMin;
|
|
982
|
+
layout.paddedXMax = savedPadXMax;
|
|
983
|
+
layout.paddedYMin = savedPadYMin;
|
|
984
|
+
layout.paddedYMax = savedPadYMax;
|
|
576
985
|
}
|
|
577
986
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
987
|
+
if (gridlineCanvas) {
|
|
988
|
+
chart._defer2D(() =>
|
|
989
|
+
renderBarGridlinesCell(
|
|
990
|
+
gridlineCanvas,
|
|
991
|
+
layout,
|
|
992
|
+
leftValueTicks,
|
|
993
|
+
theme,
|
|
994
|
+
dpr,
|
|
995
|
+
horizontal,
|
|
996
|
+
),
|
|
997
|
+
);
|
|
998
|
+
}
|
|
586
999
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
chart._glyphs.scatter.draw(
|
|
1000
|
+
withScissor(gl, layout, dpr, () => {
|
|
1001
|
+
drawGlyphRuns(
|
|
590
1002
|
chart,
|
|
591
1003
|
gl,
|
|
592
1004
|
glManager,
|
|
593
1005
|
projLeft,
|
|
594
1006
|
projRight,
|
|
1007
|
+
theme.areaOpacity,
|
|
1008
|
+
horizontal,
|
|
1009
|
+
hovered && hovered.splitIdx === p ? hovered.seriesId : -1,
|
|
1010
|
+
p,
|
|
595
1011
|
);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
598
1014
|
|
|
599
|
-
chart._lastXDomain =
|
|
600
|
-
|
|
1015
|
+
chart._lastXDomain = {
|
|
1016
|
+
levels: chart._rowPaths,
|
|
1017
|
+
numRows: chart._numCategories,
|
|
1018
|
+
levelLabels: chart._groupBy.slice(),
|
|
1019
|
+
};
|
|
1020
|
+
chart._lastYDomain = {
|
|
1021
|
+
min: visValMin,
|
|
1022
|
+
max: visValMax,
|
|
1023
|
+
label: chart._primaryValueLabel,
|
|
1024
|
+
};
|
|
601
1025
|
chart._lastYTicks = leftValueTicks;
|
|
602
|
-
chart._lastAltYDomain =
|
|
603
|
-
chart._lastAltYTicks =
|
|
604
|
-
chart._lastCatTicks =
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
1026
|
+
chart._lastAltYDomain = null;
|
|
1027
|
+
chart._lastAltYTicks = null;
|
|
1028
|
+
chart._lastCatTicks = catTicks;
|
|
1029
|
+
chart._defer2D(() => renderBarChromeOverlay(chart));
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Per-cell value-axis gridlines for the faceted frame. Non-destructive
|
|
1034
|
+
* counterpart to {@link renderBarGridlines} — the shared gridline
|
|
1035
|
+
* canvas is `initCanvas`'d once per frame, then each facet paints into
|
|
1036
|
+
* it via `getScaledContext`.
|
|
1037
|
+
*/
|
|
1038
|
+
function renderBarGridlinesCell(
|
|
1039
|
+
canvas: NonNullable<SeriesChart["_gridlineCanvas"]>,
|
|
1040
|
+
layout: PlotLayout,
|
|
1041
|
+
valueTicks: number[],
|
|
1042
|
+
theme: ReturnType<SeriesChart["_resolveTheme"]>,
|
|
1043
|
+
dpr: number,
|
|
1044
|
+
horizontal: boolean,
|
|
1045
|
+
): void {
|
|
1046
|
+
const ctx = getScaledContext(canvas, dpr);
|
|
1047
|
+
if (!ctx) {
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
ctx.strokeStyle = theme.gridlineColor;
|
|
1052
|
+
ctx.lineWidth = 1;
|
|
1053
|
+
if (horizontal) {
|
|
1054
|
+
drawGridlinesX(
|
|
1055
|
+
ctx,
|
|
1056
|
+
layout.plotRect,
|
|
1057
|
+
valueTicks,
|
|
1058
|
+
(v) => layout.dataToPixel(v, 0).px,
|
|
1059
|
+
);
|
|
1060
|
+
} else {
|
|
1061
|
+
drawGridlinesY(
|
|
1062
|
+
ctx,
|
|
1063
|
+
layout.plotRect,
|
|
1064
|
+
valueTicks,
|
|
1065
|
+
(v) => layout.dataToPixel(0, v).py,
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Resolve the `PlotLayout` a record renders in: the record's facet
|
|
1072
|
+
* cell in faceted frames, the single plot layout otherwise. Tooltip /
|
|
1073
|
+
* pin positioning must map through this so faceted anchors land in the
|
|
1074
|
+
* record's own cell.
|
|
1075
|
+
*/
|
|
1076
|
+
export function layoutForRecord(
|
|
1077
|
+
chart: SeriesChart,
|
|
1078
|
+
b: { splitIdx: number },
|
|
1079
|
+
): PlotLayout | null {
|
|
1080
|
+
if (chart._facetGrid) {
|
|
1081
|
+
return chart._facetGrid.cells[b.splitIdx]?.layout ?? null;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
return chart._lastLayout;
|
|
608
1085
|
}
|
|
609
1086
|
|
|
610
1087
|
/**
|
|
@@ -620,6 +1097,11 @@ export function renderBarChromeOverlay(chart: SeriesChart): void {
|
|
|
620
1097
|
return;
|
|
621
1098
|
}
|
|
622
1099
|
|
|
1100
|
+
if (chart._facetGrid) {
|
|
1101
|
+
renderFacetedBarChromeOverlay(chart);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
623
1105
|
const theme = chart._resolveTheme();
|
|
624
1106
|
let catAxis: BarCategoryAxis;
|
|
625
1107
|
if (
|
|
@@ -708,6 +1190,248 @@ export function renderBarChromeOverlay(chart: SeriesChart): void {
|
|
|
708
1190
|
}
|
|
709
1191
|
}
|
|
710
1192
|
|
|
1193
|
+
/**
|
|
1194
|
+
* Chrome overlay for the faceted frame. The canvas is `initCanvas`'d
|
|
1195
|
+
* once, then every painter goes through `getScaledContext` — per-cell
|
|
1196
|
+
* axis frames, per-cell categorical ticks, shared outer numeric
|
|
1197
|
+
* bands, facet titles, the aggregate legend, and the tooltip (mapped
|
|
1198
|
+
* through the hovered record's own cell).
|
|
1199
|
+
*/
|
|
1200
|
+
function renderFacetedBarChromeOverlay(chart: SeriesChart): void {
|
|
1201
|
+
const grid = chart._facetGrid!;
|
|
1202
|
+
const canvas = chart._chromeCanvas!;
|
|
1203
|
+
const theme = chart._resolveTheme();
|
|
1204
|
+
const dpr = chart._glManager?.dpr ?? 1;
|
|
1205
|
+
const horizontal = chart._isHorizontal;
|
|
1206
|
+
const hasCatLabel = chart._groupBy.length > 0;
|
|
1207
|
+
const numericCat =
|
|
1208
|
+
chart._categoryAxisMode === "numeric" &&
|
|
1209
|
+
chart._numericCategoryDomain !== null &&
|
|
1210
|
+
chart._lastCatTicks !== null;
|
|
1211
|
+
|
|
1212
|
+
if (!initCanvas(canvas, chart._lastLayout!, dpr)) {
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const catDomain = chart._lastXDomain;
|
|
1217
|
+
const valueDomain = chart._lastYDomain!;
|
|
1218
|
+
const valueTicks = chart._lastYTicks!;
|
|
1219
|
+
const valueCatDomain = chart._leftValueCategoryDomain;
|
|
1220
|
+
const valueCatActive =
|
|
1221
|
+
chart._leftValueAxisMode === "category" &&
|
|
1222
|
+
valueCatDomain !== null &&
|
|
1223
|
+
valueCatDomain.numRows > 0;
|
|
1224
|
+
|
|
1225
|
+
const primarySeries = chart._series.find((s) => s.axis === 0);
|
|
1226
|
+
const valueFmt = chart.getColumnFormatter(
|
|
1227
|
+
primarySeries?.aggName ?? null,
|
|
1228
|
+
"tick",
|
|
1229
|
+
);
|
|
1230
|
+
const catFmt = chart.getColumnFormatter(chart._groupBy[0], "tick");
|
|
1231
|
+
|
|
1232
|
+
for (const cell of grid.cells) {
|
|
1233
|
+
const layout = cell.layout;
|
|
1234
|
+
const plot = layout.plotRect;
|
|
1235
|
+
const ctx = getScaledContext(canvas, dpr);
|
|
1236
|
+
if (!ctx) {
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
ctx.strokeStyle = theme.axisLineColor;
|
|
1241
|
+
ctx.lineWidth = 1;
|
|
1242
|
+
ctx.beginPath();
|
|
1243
|
+
ctx.moveTo(plot.x, plot.y);
|
|
1244
|
+
ctx.lineTo(plot.x, plot.y + plot.height);
|
|
1245
|
+
ctx.lineTo(plot.x + plot.width, plot.y + plot.height);
|
|
1246
|
+
ctx.stroke();
|
|
1247
|
+
|
|
1248
|
+
// Categorical sides paint per cell (the outer band painters
|
|
1249
|
+
// are numeric-only); numeric sides are painted once below
|
|
1250
|
+
// into the shared outer bands.
|
|
1251
|
+
if (!horizontal) {
|
|
1252
|
+
if (!numericCat && catDomain) {
|
|
1253
|
+
renderCategoricalXTicks(ctx, layout, catDomain, theme);
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
if (valueCatActive) {
|
|
1257
|
+
renderCategoricalYTicks(ctx, layout, valueCatDomain!, theme);
|
|
1258
|
+
}
|
|
1259
|
+
} else {
|
|
1260
|
+
if (!numericCat && catDomain) {
|
|
1261
|
+
renderCategoricalYTicks(ctx, layout, catDomain, theme);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
if (valueCatActive) {
|
|
1265
|
+
renderCategoricalXTicks(ctx, layout, valueCatDomain!, theme);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if (cell.titleRect) {
|
|
1270
|
+
drawFacetTitle(canvas, cell.label, cell.titleRect, theme, dpr);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
const numericCatAxisDomain: AxisDomain | null = numericCat
|
|
1275
|
+
? {
|
|
1276
|
+
min: chart._numericCategoryDomain!.min,
|
|
1277
|
+
max: chart._numericCategoryDomain!.max,
|
|
1278
|
+
isDate: chart._numericCategoryDomain!.isDate,
|
|
1279
|
+
label: chart._numericCategoryDomain!.label,
|
|
1280
|
+
}
|
|
1281
|
+
: null;
|
|
1282
|
+
|
|
1283
|
+
if (!horizontal) {
|
|
1284
|
+
if (numericCatAxisDomain && grid.outerXAxisRect) {
|
|
1285
|
+
renderOuterXAxis(
|
|
1286
|
+
canvas,
|
|
1287
|
+
grid.outerXAxisRect,
|
|
1288
|
+
numericCatAxisDomain,
|
|
1289
|
+
chart._lastCatTicks!,
|
|
1290
|
+
bottomRowLayouts(grid),
|
|
1291
|
+
theme,
|
|
1292
|
+
hasCatLabel,
|
|
1293
|
+
dpr,
|
|
1294
|
+
catFmt,
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
if (!valueCatActive && grid.outerYAxisRect) {
|
|
1299
|
+
renderOuterYAxis(
|
|
1300
|
+
canvas,
|
|
1301
|
+
grid.outerYAxisRect,
|
|
1302
|
+
valueDomain,
|
|
1303
|
+
valueTicks,
|
|
1304
|
+
leftColumnLayouts(grid),
|
|
1305
|
+
theme,
|
|
1306
|
+
true,
|
|
1307
|
+
dpr,
|
|
1308
|
+
valueFmt,
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
} else {
|
|
1312
|
+
if (numericCatAxisDomain && grid.outerYAxisRect) {
|
|
1313
|
+
renderOuterYAxis(
|
|
1314
|
+
canvas,
|
|
1315
|
+
grid.outerYAxisRect,
|
|
1316
|
+
numericCatAxisDomain,
|
|
1317
|
+
chart._lastCatTicks!,
|
|
1318
|
+
leftColumnLayouts(grid),
|
|
1319
|
+
theme,
|
|
1320
|
+
hasCatLabel,
|
|
1321
|
+
dpr,
|
|
1322
|
+
catFmt,
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (!valueCatActive && grid.outerXAxisRect) {
|
|
1327
|
+
renderOuterXAxis(
|
|
1328
|
+
canvas,
|
|
1329
|
+
grid.outerXAxisRect,
|
|
1330
|
+
valueDomain,
|
|
1331
|
+
valueTicks,
|
|
1332
|
+
bottomRowLayouts(grid),
|
|
1333
|
+
theme,
|
|
1334
|
+
true,
|
|
1335
|
+
dpr,
|
|
1336
|
+
valueFmt,
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
renderFacetedBarLegend(chart, grid);
|
|
1342
|
+
|
|
1343
|
+
if (getHoveredBar(chart)) {
|
|
1344
|
+
renderBarTooltipCanvas(chart);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Aggregate-level legend for the faceted frame, painted into the
|
|
1350
|
+
* grid's shared right gutter. One entry per aggregate (facets absorb
|
|
1351
|
+
* the split dimension; every split of an aggregate shares its color —
|
|
1352
|
+
* see `ensurePalette`). A legend toggle targets the aggregate's full
|
|
1353
|
+
* seriesId set, so hiding "Sales" hides it in every facet at once; an
|
|
1354
|
+
* entry reads as hidden only when ALL of its series are hidden.
|
|
1355
|
+
*/
|
|
1356
|
+
function renderFacetedBarLegend(chart: SeriesChart, grid: FacetGrid): void {
|
|
1357
|
+
chart._legendRects = [];
|
|
1358
|
+
if (!chart._chromeCanvas || !grid.legendRect) {
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const M = chart._aggregates.length;
|
|
1363
|
+
if (M <= 1) {
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const ctx = chart._chromeCanvas.getContext("2d") as Context2D | null;
|
|
1368
|
+
if (!ctx) {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
ctx.save();
|
|
1373
|
+
|
|
1374
|
+
const theme = chart._resolveTheme();
|
|
1375
|
+
const swatchSize = 10;
|
|
1376
|
+
const lineHeight = 18;
|
|
1377
|
+
const x = grid.legendRect.x + 12;
|
|
1378
|
+
let y = grid.legendRect.y + 10;
|
|
1379
|
+
|
|
1380
|
+
ctx.font = `11px ${theme.fontFamily}`;
|
|
1381
|
+
ctx.textAlign = "left";
|
|
1382
|
+
ctx.textBaseline = "middle";
|
|
1383
|
+
|
|
1384
|
+
for (let k = 0; k < M; k++) {
|
|
1385
|
+
const seriesIds: number[] = [];
|
|
1386
|
+
let color: [number, number, number] = [0.5, 0.5, 0.5];
|
|
1387
|
+
for (const s of chart._series) {
|
|
1388
|
+
if (s.aggIdx === k) {
|
|
1389
|
+
seriesIds.push(s.seriesId);
|
|
1390
|
+
color = s.color;
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
const label = chart._aggregates[k];
|
|
1395
|
+
const hidden = seriesIds.every((sid) => chart._hiddenSeries.has(sid));
|
|
1396
|
+
const r = Math.round(color[0] * 255);
|
|
1397
|
+
const g = Math.round(color[1] * 255);
|
|
1398
|
+
const b = Math.round(color[2] * 255);
|
|
1399
|
+
|
|
1400
|
+
ctx.globalAlpha = hidden ? 0.3 : 1.0;
|
|
1401
|
+
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
|
1402
|
+
ctx.fillRect(x, y - swatchSize / 2, swatchSize, swatchSize);
|
|
1403
|
+
|
|
1404
|
+
ctx.fillStyle = theme.legendText;
|
|
1405
|
+
ctx.fillText(label, x + swatchSize + 6, y);
|
|
1406
|
+
|
|
1407
|
+
const textW = ctx.measureText(label).width;
|
|
1408
|
+
if (hidden) {
|
|
1409
|
+
ctx.strokeStyle = theme.legendText;
|
|
1410
|
+
ctx.lineWidth = 1;
|
|
1411
|
+
ctx.beginPath();
|
|
1412
|
+
ctx.moveTo(x + swatchSize + 6, y);
|
|
1413
|
+
ctx.lineTo(x + swatchSize + 6 + textW, y);
|
|
1414
|
+
ctx.stroke();
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
ctx.globalAlpha = 1.0;
|
|
1418
|
+
|
|
1419
|
+
chart._legendRects.push({
|
|
1420
|
+
seriesIds,
|
|
1421
|
+
rect: {
|
|
1422
|
+
x: x - 2,
|
|
1423
|
+
y: y - lineHeight / 2,
|
|
1424
|
+
width: swatchSize + 6 + textW + 4,
|
|
1425
|
+
height: lineHeight,
|
|
1426
|
+
},
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
y += lineHeight;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
ctx.restore();
|
|
1433
|
+
}
|
|
1434
|
+
|
|
711
1435
|
/**
|
|
712
1436
|
* Cached parallel array of measured legend text widths. The legend
|
|
713
1437
|
* renderer reads from this each frame instead of re-running
|
|
@@ -807,7 +1531,7 @@ function renderBarLegend(chart: SeriesChart): void {
|
|
|
807
1531
|
width: swatchSize + 6 + textW + 4,
|
|
808
1532
|
height: lineHeight,
|
|
809
1533
|
};
|
|
810
|
-
chart._legendRects.push({
|
|
1534
|
+
chart._legendRects.push({ seriesIds: [s.seriesId], rect });
|
|
811
1535
|
|
|
812
1536
|
y += lineHeight;
|
|
813
1537
|
}
|
|
@@ -825,7 +1549,12 @@ function renderBarTooltipCanvas(chart: SeriesChart): void {
|
|
|
825
1549
|
return;
|
|
826
1550
|
}
|
|
827
1551
|
|
|
828
|
-
|
|
1552
|
+
// Faceted frames anchor in the record's own cell; single-plot
|
|
1553
|
+
// frames fall through to `_lastLayout`.
|
|
1554
|
+
const layout = layoutForRecord(chart, b);
|
|
1555
|
+
if (!layout) {
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
829
1558
|
|
|
830
1559
|
// Bar glyphs anchor the tooltip at the midpoint of the bar body so
|
|
831
1560
|
// it reads against a solid swatch. Line / scatter / area glyphs
|
|
@@ -842,7 +1571,7 @@ function renderBarTooltipCanvas(chart: SeriesChart): void {
|
|
|
842
1571
|
? chart._isHorizontal
|
|
843
1572
|
? layout.dataToPixel(anchorV, b.xCenter)
|
|
844
1573
|
: layout.dataToPixel(b.xCenter, anchorV)
|
|
845
|
-
: rightAxisDataToPixel(chart, b.xCenter, anchorV);
|
|
1574
|
+
: rightAxisDataToPixel(chart, b.xCenter, anchorV, layout);
|
|
846
1575
|
|
|
847
1576
|
const lines = buildBarTooltipLines(chart, b);
|
|
848
1577
|
const theme = chart._resolveTheme();
|
|
@@ -860,8 +1589,9 @@ export function rightAxisDataToPixel(
|
|
|
860
1589
|
chart: SeriesChart,
|
|
861
1590
|
x: number,
|
|
862
1591
|
y: number,
|
|
1592
|
+
layoutOverride?: PlotLayout,
|
|
863
1593
|
): { px: number; py: number } {
|
|
864
|
-
const layout = chart._lastLayout!;
|
|
1594
|
+
const layout = layoutOverride ?? chart._lastLayout!;
|
|
865
1595
|
const { x: px, y: py, width, height } = layout.plotRect;
|
|
866
1596
|
const tx =
|
|
867
1597
|
(x - layout.paddedXMin) / (layout.paddedXMax - layout.paddedXMin);
|