@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
|
@@ -34,10 +34,22 @@ interface ScatterProgramCache {
|
|
|
34
34
|
* Persistent scatter glyph state — left/right axis position + color
|
|
35
35
|
* buffers built once at data load. Pan/zoom redraws rebind without
|
|
36
36
|
* uploading.
|
|
37
|
+
*
|
|
38
|
+
* `seriesRanges` records each visible series' contiguous run within
|
|
39
|
+
* its axis bucket (`first` is bucket-relative). The fill loop appends
|
|
40
|
+
* series in order, so runs are contiguous by construction; the
|
|
41
|
+
* faceted draw path uses them to dispatch per-facet sub-ranges via
|
|
42
|
+
* `drawArrays(POINTS, first, count)`.
|
|
37
43
|
*/
|
|
38
44
|
interface ScatterBuffers {
|
|
39
45
|
leftCount: number;
|
|
40
46
|
rightCount: number;
|
|
47
|
+
seriesRanges: {
|
|
48
|
+
seriesId: number;
|
|
49
|
+
axis: 0 | 1;
|
|
50
|
+
first: number;
|
|
51
|
+
count: number;
|
|
52
|
+
}[];
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
/**
|
|
@@ -170,6 +182,9 @@ export class ScatterGlyph {
|
|
|
170
182
|
|
|
171
183
|
// Fill left bucket from `[0, leftCount)`, right bucket from
|
|
172
184
|
// `[leftCount, total)` — single typed-array allocation each.
|
|
185
|
+
// Series runs are contiguous within their bucket; record each
|
|
186
|
+
// run's bucket-relative `[first, count)` for the faceted draw.
|
|
187
|
+
const seriesRanges: ScatterBuffers["seriesRanges"] = [];
|
|
173
188
|
let leftWrite = 0;
|
|
174
189
|
let rightWrite = leftCount;
|
|
175
190
|
for (const s of scatterSeries) {
|
|
@@ -177,6 +192,7 @@ export class ScatterGlyph {
|
|
|
177
192
|
continue;
|
|
178
193
|
}
|
|
179
194
|
|
|
195
|
+
const runFirst = s.axis === 1 ? rightWrite - leftCount : leftWrite;
|
|
180
196
|
const r = s.color[0];
|
|
181
197
|
const g = s.color[1];
|
|
182
198
|
const b = s.color[2];
|
|
@@ -204,6 +220,16 @@ export class ScatterGlyph {
|
|
|
204
220
|
leftWrite++;
|
|
205
221
|
}
|
|
206
222
|
}
|
|
223
|
+
|
|
224
|
+
const runEnd = s.axis === 1 ? rightWrite - leftCount : leftWrite;
|
|
225
|
+
if (runEnd > runFirst) {
|
|
226
|
+
seriesRanges.push({
|
|
227
|
+
seriesId: s.seriesId,
|
|
228
|
+
axis: s.axis,
|
|
229
|
+
first: runFirst,
|
|
230
|
+
count: runEnd - runFirst,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
207
233
|
}
|
|
208
234
|
|
|
209
235
|
if (leftCount > 0) {
|
|
@@ -236,12 +262,15 @@ export class ScatterGlyph {
|
|
|
236
262
|
);
|
|
237
263
|
}
|
|
238
264
|
|
|
239
|
-
this._buffers = { leftCount, rightCount };
|
|
265
|
+
this._buffers = { leftCount, rightCount, seriesRanges };
|
|
240
266
|
}
|
|
241
267
|
|
|
242
268
|
/**
|
|
243
269
|
* Bind the persistent left/right buffers and issue up to two draw
|
|
244
|
-
* calls. No per-frame allocations or buffer uploads.
|
|
270
|
+
* calls. No per-frame allocations or buffer uploads. `splitFilter`
|
|
271
|
+
* (faceted frames) and/or `aggRange` (mixed glyph-run frames)
|
|
272
|
+
* instead draw each matching series' contiguous bucket sub-range —
|
|
273
|
+
* one `drawArrays(POINTS, first, count)` per matching series.
|
|
245
274
|
*/
|
|
246
275
|
draw(
|
|
247
276
|
chart: SeriesChart,
|
|
@@ -249,6 +278,8 @@ export class ScatterGlyph {
|
|
|
249
278
|
glManager: WebGLContextManager,
|
|
250
279
|
projLeft: Float32Array,
|
|
251
280
|
projRight: Float32Array,
|
|
281
|
+
splitFilter?: number,
|
|
282
|
+
aggRange?: { start: number; end: number },
|
|
252
283
|
): void {
|
|
253
284
|
const buf = this._buffers;
|
|
254
285
|
const cache = this._program;
|
|
@@ -267,6 +298,39 @@ export class ScatterGlyph {
|
|
|
267
298
|
chart._pluginConfig.point_size_px * dpr,
|
|
268
299
|
);
|
|
269
300
|
|
|
301
|
+
if (splitFilter !== undefined || aggRange !== undefined) {
|
|
302
|
+
for (const r of buf.seriesRanges) {
|
|
303
|
+
if (
|
|
304
|
+
splitFilter !== undefined &&
|
|
305
|
+
chart._series[r.seriesId].splitIdx !== splitFilter
|
|
306
|
+
) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const aggIdx = chart._series[r.seriesId].aggIdx;
|
|
311
|
+
if (
|
|
312
|
+
aggRange !== undefined &&
|
|
313
|
+
(aggIdx < aggRange.start || aggIdx > aggRange.end)
|
|
314
|
+
) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
drawBucket(
|
|
319
|
+
gl,
|
|
320
|
+
cache,
|
|
321
|
+
r.axis === 1 ? cache.posRightBuffer : cache.posLeftBuffer,
|
|
322
|
+
r.axis === 1
|
|
323
|
+
? cache.colorRightBuffer
|
|
324
|
+
: cache.colorLeftBuffer,
|
|
325
|
+
r.count,
|
|
326
|
+
r.axis === 1 ? projRight : projLeft,
|
|
327
|
+
r.first,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
270
334
|
drawBucket(
|
|
271
335
|
gl,
|
|
272
336
|
cache,
|
|
@@ -309,6 +373,7 @@ function drawBucket(
|
|
|
309
373
|
colBuf: WebGLBuffer,
|
|
310
374
|
count: number,
|
|
311
375
|
proj: Float32Array,
|
|
376
|
+
first = 0,
|
|
312
377
|
): void {
|
|
313
378
|
if (count === 0) {
|
|
314
379
|
return;
|
|
@@ -324,5 +389,5 @@ function drawBucket(
|
|
|
324
389
|
gl.enableVertexAttribArray(cache.a_color);
|
|
325
390
|
gl.vertexAttribPointer(cache.a_color, 3, gl.FLOAT, false, 0, 0);
|
|
326
391
|
|
|
327
|
-
gl.drawArrays(gl.POINTS,
|
|
392
|
+
gl.drawArrays(gl.POINTS, first, count);
|
|
328
393
|
}
|
|
@@ -285,6 +285,18 @@ export interface SeriesPipelineInput {
|
|
|
285
285
|
*/
|
|
286
286
|
includeZero: boolean;
|
|
287
287
|
|
|
288
|
+
/**
|
|
289
|
+
* `facet_mode: "grid"` geometry: key the stack ladder by
|
|
290
|
+
* `(catIdx, aggIdx, splitIdx)` instead of `(catIdx, aggIdx)`, so
|
|
291
|
+
* splits never stack on each other — each facet's bars / areas
|
|
292
|
+
* grow from their own zero baseline. Slot geometry (`xCenter` /
|
|
293
|
+
* `halfWidth`) is unchanged: facets separate splits by projection
|
|
294
|
+
* + scissor, not by X offset, so every split's records share the
|
|
295
|
+
* same band-slot coordinates. Value extents still accumulate
|
|
296
|
+
* across ALL splits — the facet grid renders a shared value axis.
|
|
297
|
+
*/
|
|
298
|
+
facetSplits?: boolean;
|
|
299
|
+
|
|
288
300
|
/**
|
|
289
301
|
* Reusable scratch — pipeline writes records into these in place
|
|
290
302
|
* and zero-fills the stack ladder. Pass the previous build's
|
|
@@ -404,6 +416,7 @@ export function buildSeriesPipeline(
|
|
|
404
416
|
bandInnerFrac,
|
|
405
417
|
barInnerPad,
|
|
406
418
|
includeZero,
|
|
419
|
+
facetSplits,
|
|
407
420
|
scratchBars,
|
|
408
421
|
scratchPosStack,
|
|
409
422
|
scratchNegStack,
|
|
@@ -529,9 +542,12 @@ export function buildSeriesPipeline(
|
|
|
529
542
|
const N = numCategories;
|
|
530
543
|
const S = series.length;
|
|
531
544
|
|
|
532
|
-
// Stacking ladder, keyed by (catIdx, aggIdx)
|
|
545
|
+
// Stacking ladder, keyed by (catIdx, aggIdx) — or per-split
|
|
546
|
+
// (catIdx, aggIdx, splitIdx) in facet-grid mode, where each split
|
|
547
|
+
// renders in its own facet and cross-split stacking would lift
|
|
548
|
+
// every facet after the first off its baseline. Reuse chart-owned
|
|
533
549
|
// scratch when sized; else allocate. Active prefix is zero-filled.
|
|
534
|
-
const stackLen = N * M;
|
|
550
|
+
const stackLen = facetSplits ? N * M * P : N * M;
|
|
535
551
|
const posStack = ensureFloat64Scratch(scratchPosStack ?? null, stackLen);
|
|
536
552
|
const negStack = ensureFloat64Scratch(scratchNegStack ?? null, stackLen);
|
|
537
553
|
posStack.fill(0, 0, stackLen);
|
|
@@ -922,7 +938,9 @@ export function buildSeriesPipeline(
|
|
|
922
938
|
}
|
|
923
939
|
}
|
|
924
940
|
|
|
925
|
-
const stackIdx =
|
|
941
|
+
const stackIdx = facetSplits
|
|
942
|
+
? (catI * M + k) * P + p
|
|
943
|
+
: catI * M + k;
|
|
926
944
|
let y0: number;
|
|
927
945
|
let y1: number;
|
|
928
946
|
if (v >= 0) {
|
|
@@ -18,10 +18,10 @@ import {
|
|
|
18
18
|
type SeriesChartRecord,
|
|
19
19
|
} from "./series-build";
|
|
20
20
|
import {
|
|
21
|
-
renderBarFrame,
|
|
22
21
|
uploadBarInstances,
|
|
23
22
|
rebuildGlyphBuffers,
|
|
24
23
|
rightAxisDataToPixel,
|
|
24
|
+
layoutForRecord,
|
|
25
25
|
} from "./series-render";
|
|
26
26
|
|
|
27
27
|
const POINT_HIT_RADIUS_PX = 10;
|
|
@@ -47,9 +47,16 @@ export function getHoveredBar(chart: SeriesChart): SeriesChartRecord | null {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* Handle mouse-move across all glyph types. Tests
|
|
51
|
-
* so top glyphs win
|
|
50
|
+
* Handle mouse-move across all glyph types. Tests glyph runs in reverse
|
|
51
|
+
* paint order so top glyphs win — paint order is the `columns`
|
|
52
|
+
* declaration order (see `_glyphRuns` / `drawGlyphRuns`).
|
|
52
53
|
* Updates `_hoveredBarIdx` or `_hoveredSample` and re-renders on change.
|
|
54
|
+
*
|
|
55
|
+
* Faceted frames first resolve the cell under the cursor: that cell's
|
|
56
|
+
* layout supplies the pixel→data mapping and its index becomes the
|
|
57
|
+
* `splitFilter` — records of other splits share the same band-slot
|
|
58
|
+
* coordinates by construction, so an unfiltered hit-test would match
|
|
59
|
+
* a bar from a different facet.
|
|
53
60
|
*/
|
|
54
61
|
export function handleBarHover(
|
|
55
62
|
chart: SeriesChart,
|
|
@@ -60,7 +67,33 @@ export function handleBarHover(
|
|
|
60
67
|
return;
|
|
61
68
|
}
|
|
62
69
|
|
|
63
|
-
|
|
70
|
+
let layout = chart._lastLayout;
|
|
71
|
+
let splitFilter: number | undefined;
|
|
72
|
+
if (chart._facetGrid) {
|
|
73
|
+
const cells = chart._facetGrid.cells;
|
|
74
|
+
let found = -1;
|
|
75
|
+
for (let i = 0; i < cells.length; i++) {
|
|
76
|
+
const p = cells[i].layout.plotRect;
|
|
77
|
+
if (
|
|
78
|
+
mx >= p.x &&
|
|
79
|
+
mx <= p.x + p.width &&
|
|
80
|
+
my >= p.y &&
|
|
81
|
+
my <= p.y + p.height
|
|
82
|
+
) {
|
|
83
|
+
found = i;
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (found < 0) {
|
|
89
|
+
clearHover(chart);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
layout = cells[found].layout;
|
|
94
|
+
splitFilter = found;
|
|
95
|
+
}
|
|
96
|
+
|
|
64
97
|
const plot = layout.plotRect;
|
|
65
98
|
|
|
66
99
|
if (
|
|
@@ -121,83 +154,131 @@ export function handleBarHover(
|
|
|
121
154
|
let nextBarIdx = -1;
|
|
122
155
|
let nextSample: SeriesChartRecord | null = null;
|
|
123
156
|
|
|
124
|
-
//
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
157
|
+
// Test topmost-first: the REVERSE of the paint order, which is the
|
|
158
|
+
// `columns` declaration order (`_glyphRuns`, ascending `aggIdx` —
|
|
159
|
+
// see `drawGlyphRuns`). A single-run chart passes no `aggRange`,
|
|
160
|
+
// reducing to the legacy whole-type scan.
|
|
161
|
+
const runs = chart._glyphRuns;
|
|
162
|
+
const single = runs.length <= 1;
|
|
163
|
+
for (let r = runs.length - 1; r >= 0; r--) {
|
|
164
|
+
const run = runs[r];
|
|
165
|
+
const aggRange = single
|
|
166
|
+
? undefined
|
|
167
|
+
: { start: run.aggStart, end: run.aggEnd };
|
|
168
|
+
switch (run.chartType) {
|
|
169
|
+
case "scatter":
|
|
170
|
+
case "line":
|
|
171
|
+
nextSample = hitTestPoints(
|
|
172
|
+
chart,
|
|
173
|
+
run.chartType,
|
|
174
|
+
dataX,
|
|
175
|
+
dataYLeft,
|
|
176
|
+
dataYRight,
|
|
177
|
+
pxPerDataX,
|
|
178
|
+
pxPerDataYLeft,
|
|
179
|
+
pxPerDataYRight,
|
|
180
|
+
splitFilter,
|
|
181
|
+
aggRange,
|
|
182
|
+
);
|
|
183
|
+
break;
|
|
184
|
+
case "bar":
|
|
185
|
+
nextBarIdx = hitTestBars(
|
|
186
|
+
chart,
|
|
187
|
+
dataX,
|
|
188
|
+
dataYLeft,
|
|
189
|
+
dataYRight,
|
|
190
|
+
splitFilter,
|
|
191
|
+
aggRange,
|
|
192
|
+
);
|
|
193
|
+
break;
|
|
194
|
+
case "area": {
|
|
195
|
+
const areaHit = hitTestAreas(
|
|
196
|
+
chart,
|
|
197
|
+
dataX,
|
|
198
|
+
dataYLeft,
|
|
199
|
+
dataYRight,
|
|
200
|
+
splitFilter,
|
|
201
|
+
aggRange,
|
|
202
|
+
);
|
|
203
|
+
if (areaHit) {
|
|
204
|
+
if (areaHit.idx >= 0) {
|
|
205
|
+
nextBarIdx = areaHit.idx;
|
|
206
|
+
} else {
|
|
207
|
+
nextSample = areaHit.bar;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
135
210
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
dataYRight,
|
|
144
|
-
pxPerDataX,
|
|
145
|
-
pxPerDataYLeft,
|
|
146
|
-
pxPerDataYRight,
|
|
147
|
-
);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (nextBarIdx >= 0 || nextSample) {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
148
218
|
}
|
|
149
219
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const bars = chart._bars;
|
|
153
|
-
const ct = bars.chartType;
|
|
154
|
-
const sid = bars.seriesId;
|
|
155
|
-
const xC = bars.xCenter;
|
|
156
|
-
const hw = bars.halfWidth;
|
|
157
|
-
const by0 = bars.y0;
|
|
158
|
-
const by1 = bars.y1;
|
|
159
|
-
const ax = bars.axis;
|
|
160
|
-
const hidden = chart._hiddenSeries;
|
|
161
|
-
for (let i = 0; i < bars.count; i++) {
|
|
162
|
-
if (ct[i] !== BAR_TYPE_BAR) {
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
220
|
+
applyHover(chart, nextBarIdx, nextSample);
|
|
221
|
+
}
|
|
165
222
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
223
|
+
/**
|
|
224
|
+
* Rect-intersect hit-test over the bar-typed `_bars` records. Returns
|
|
225
|
+
* the record index, or `-1`.
|
|
226
|
+
*/
|
|
227
|
+
function hitTestBars(
|
|
228
|
+
chart: SeriesChart,
|
|
229
|
+
dataX: number,
|
|
230
|
+
dataYLeft: number,
|
|
231
|
+
dataYRight: number,
|
|
232
|
+
splitFilter?: number,
|
|
233
|
+
aggRange?: { start: number; end: number },
|
|
234
|
+
): number {
|
|
235
|
+
const bars = chart._bars;
|
|
236
|
+
const ct = bars.chartType;
|
|
237
|
+
const sid = bars.seriesId;
|
|
238
|
+
const xC = bars.xCenter;
|
|
239
|
+
const hw = bars.halfWidth;
|
|
240
|
+
const by0 = bars.y0;
|
|
241
|
+
const by1 = bars.y1;
|
|
242
|
+
const ax = bars.axis;
|
|
243
|
+
const hidden = chart._hiddenSeries;
|
|
244
|
+
const P = Math.max(1, chart._splitPrefixes.length);
|
|
245
|
+
for (let i = 0; i < bars.count; i++) {
|
|
246
|
+
if (ct[i] !== BAR_TYPE_BAR) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
169
249
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
250
|
+
if (hidden.has(sid[i])) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (splitFilter !== undefined && sid[i] % P !== splitFilter) {
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (aggRange !== undefined) {
|
|
259
|
+
const aggIdx = Math.floor(sid[i] / P);
|
|
260
|
+
if (aggIdx < aggRange.start || aggIdx > aggRange.end) {
|
|
173
261
|
continue;
|
|
174
262
|
}
|
|
263
|
+
}
|
|
175
264
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const hi = y0 < y1 ? y1 : y0;
|
|
181
|
-
if (dy >= lo && dy <= hi) {
|
|
182
|
-
nextBarIdx = i;
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
265
|
+
const xc = xC[i];
|
|
266
|
+
const halfW = hw[i];
|
|
267
|
+
if (dataX < xc - halfW || dataX > xc + halfW) {
|
|
268
|
+
continue;
|
|
185
269
|
}
|
|
186
|
-
}
|
|
187
270
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
nextSample = areaHit.bar;
|
|
196
|
-
}
|
|
271
|
+
const dy = ax[i] === 0 ? dataYLeft : dataYRight;
|
|
272
|
+
const y0 = by0[i];
|
|
273
|
+
const y1 = by1[i];
|
|
274
|
+
const lo = y0 < y1 ? y0 : y1;
|
|
275
|
+
const hi = y0 < y1 ? y1 : y0;
|
|
276
|
+
if (dy >= lo && dy <= hi) {
|
|
277
|
+
return i;
|
|
197
278
|
}
|
|
198
279
|
}
|
|
199
280
|
|
|
200
|
-
|
|
281
|
+
return -1;
|
|
201
282
|
}
|
|
202
283
|
|
|
203
284
|
function hitTestPoints(
|
|
@@ -209,6 +290,8 @@ function hitTestPoints(
|
|
|
209
290
|
pxPerDataX: number,
|
|
210
291
|
pxPerDataYLeft: number,
|
|
211
292
|
pxPerDataYRight: number,
|
|
293
|
+
splitFilter?: number,
|
|
294
|
+
aggRange?: { start: number; end: number },
|
|
212
295
|
): SeriesChartRecord | null {
|
|
213
296
|
const N = chart._numCategories;
|
|
214
297
|
const S = chart._series.length;
|
|
@@ -233,6 +316,17 @@ function hitTestPoints(
|
|
|
233
316
|
continue;
|
|
234
317
|
}
|
|
235
318
|
|
|
319
|
+
if (splitFilter !== undefined && s.splitIdx !== splitFilter) {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (
|
|
324
|
+
aggRange !== undefined &&
|
|
325
|
+
(s.aggIdx < aggRange.start || s.aggIdx > aggRange.end)
|
|
326
|
+
) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
236
330
|
const dataY = s.axis === 1 ? dataYRight : dataYLeft;
|
|
237
331
|
const pyPerData = s.axis === 1 ? pxPerDataYRight : pxPerDataYLeft;
|
|
238
332
|
|
|
@@ -289,6 +383,8 @@ function hitTestAreas(
|
|
|
289
383
|
dataX: number,
|
|
290
384
|
dataYLeft: number,
|
|
291
385
|
dataYRight: number,
|
|
386
|
+
splitFilter?: number,
|
|
387
|
+
aggRange?: { start: number; end: number },
|
|
292
388
|
): { idx: number; bar: SeriesChartRecord | null } | null {
|
|
293
389
|
// Closest category to the mouse; an area covers every [cat - 0.5, cat + 0.5]
|
|
294
390
|
// slot, so use `round(dataX)` as the candidate index.
|
|
@@ -313,6 +409,7 @@ function hitTestAreas(
|
|
|
313
409
|
const ax = bars.axis;
|
|
314
410
|
const by0 = bars.y0;
|
|
315
411
|
const by1 = bars.y1;
|
|
412
|
+
const P = chart._splitPrefixes.length;
|
|
316
413
|
for (let i = 0; i < bars.count; i++) {
|
|
317
414
|
if (ct[i] !== BAR_TYPE_AREA) {
|
|
318
415
|
continue;
|
|
@@ -326,6 +423,17 @@ function hitTestAreas(
|
|
|
326
423
|
continue;
|
|
327
424
|
}
|
|
328
425
|
|
|
426
|
+
if (splitFilter !== undefined && sid[i] % P !== splitFilter) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (aggRange !== undefined) {
|
|
431
|
+
const aggIdx = Math.floor(sid[i] / Math.max(1, P));
|
|
432
|
+
if (aggIdx < aggRange.start || aggIdx > aggRange.end) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
329
437
|
const dy = ax[i] === 0 ? dataYLeft : dataYRight;
|
|
330
438
|
const y0 = by0[i];
|
|
331
439
|
const y1 = by1[i];
|
|
@@ -346,6 +454,17 @@ function hitTestAreas(
|
|
|
346
454
|
continue;
|
|
347
455
|
}
|
|
348
456
|
|
|
457
|
+
if (splitFilter !== undefined && s.splitIdx !== splitFilter) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (
|
|
462
|
+
aggRange !== undefined &&
|
|
463
|
+
(s.aggIdx < aggRange.start || s.aggIdx > aggRange.end)
|
|
464
|
+
) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
|
|
349
468
|
const idx = cat * S + s.seriesId;
|
|
350
469
|
if (!((valid[idx >> 3] >> (idx & 7)) & 1)) {
|
|
351
470
|
continue;
|
|
@@ -383,7 +502,7 @@ function clearHover(chart: SeriesChart): void {
|
|
|
383
502
|
chart._hoveredBarIdx = -1;
|
|
384
503
|
chart._hoveredSample = null;
|
|
385
504
|
if (chart._glManager) {
|
|
386
|
-
|
|
505
|
+
chart.requestRender(chart._glManager);
|
|
387
506
|
}
|
|
388
507
|
}
|
|
389
508
|
}
|
|
@@ -405,13 +524,17 @@ function applyHover(
|
|
|
405
524
|
chart._hoveredBarIdx = nextBarIdx;
|
|
406
525
|
chart._hoveredSample = nextSample;
|
|
407
526
|
if (chart._glManager) {
|
|
408
|
-
|
|
527
|
+
chart.requestRender(chart._glManager);
|
|
409
528
|
}
|
|
410
529
|
}
|
|
411
530
|
|
|
412
531
|
/**
|
|
413
532
|
* Handle a click on the legend area. Returns true when the click hit a
|
|
414
533
|
* legend entry (the caller should then treat the event as consumed).
|
|
534
|
+
*
|
|
535
|
+
* An entry may own several series (facet-grid mode groups the legend
|
|
536
|
+
* by aggregate): the toggle is all-or-nothing — any visible member
|
|
537
|
+
* hides the whole group, a fully-hidden group shows every member.
|
|
415
538
|
*/
|
|
416
539
|
export function handleBarLegendClick(
|
|
417
540
|
chart: SeriesChart,
|
|
@@ -430,10 +553,15 @@ export function handleBarLegendClick(
|
|
|
430
553
|
my >= r.y &&
|
|
431
554
|
my <= r.y + r.height
|
|
432
555
|
) {
|
|
433
|
-
|
|
434
|
-
chart._hiddenSeries.
|
|
435
|
-
|
|
436
|
-
|
|
556
|
+
const anyVisible = entry.seriesIds.some(
|
|
557
|
+
(sid) => !chart._hiddenSeries.has(sid),
|
|
558
|
+
);
|
|
559
|
+
for (const sid of entry.seriesIds) {
|
|
560
|
+
if (anyVisible) {
|
|
561
|
+
chart._hiddenSeries.add(sid);
|
|
562
|
+
} else {
|
|
563
|
+
chart._hiddenSeries.delete(sid);
|
|
564
|
+
}
|
|
437
565
|
}
|
|
438
566
|
|
|
439
567
|
// Hidden-series change affects which bars contribute to
|
|
@@ -454,7 +582,7 @@ export function handleBarLegendClick(
|
|
|
454
582
|
if (chart._glManager) {
|
|
455
583
|
uploadBarInstances(chart, chart._glManager);
|
|
456
584
|
rebuildGlyphBuffers(chart, chart._glManager);
|
|
457
|
-
|
|
585
|
+
chart.requestRender(chart._glManager);
|
|
458
586
|
}
|
|
459
587
|
|
|
460
588
|
return true;
|
|
@@ -571,7 +699,11 @@ function pinTooltip(chart: SeriesChart, b: SeriesChartRecord): void {
|
|
|
571
699
|
return;
|
|
572
700
|
}
|
|
573
701
|
|
|
574
|
-
|
|
702
|
+
// Faceted frames anchor in the record's own cell.
|
|
703
|
+
const layout = layoutForRecord(chart, b);
|
|
704
|
+
if (!layout) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
575
707
|
|
|
576
708
|
// Anchor at the bar midpoint for bar glyphs (tooltip reads against
|
|
577
709
|
// the body); at the point itself (`y1`) for line / scatter / area.
|
|
@@ -582,7 +714,7 @@ function pinTooltip(chart: SeriesChart, b: SeriesChartRecord): void {
|
|
|
582
714
|
? chart._isHorizontal
|
|
583
715
|
? layout.dataToPixel(anchorV, b.xCenter)
|
|
584
716
|
: layout.dataToPixel(b.xCenter, anchorV)
|
|
585
|
-
: rightAxisDataToPixel(chart, b.xCenter, anchorV);
|
|
717
|
+
: rightAxisDataToPixel(chart, b.xCenter, anchorV, layout);
|
|
586
718
|
|
|
587
719
|
const lines = buildBarTooltipLines(chart, b);
|
|
588
720
|
if (lines.length === 0) {
|
|
@@ -594,7 +726,7 @@ function pinTooltip(chart: SeriesChart, b: SeriesChartRecord): void {
|
|
|
594
726
|
chart._hoveredBarIdx = -1;
|
|
595
727
|
chart._hoveredSample = null;
|
|
596
728
|
if (chart._glManager) {
|
|
597
|
-
|
|
729
|
+
chart.requestRender(chart._glManager);
|
|
598
730
|
}
|
|
599
731
|
}
|
|
600
732
|
|