@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
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { ColumnDataMap } from "../../data/view-reader";
|
|
14
14
|
import type { WebGLContextManager } from "../../webgl/context-manager";
|
|
15
15
|
import type { ZoomConfig } from "../../interaction/zoom-controller";
|
|
16
|
+
import type { FacetGrid } from "../../layout/facet-grid";
|
|
16
17
|
import { CategoricalYChart } from "../common/categorical-y-chart";
|
|
17
18
|
import { type PlotRect } from "../../layout/plot-layout";
|
|
18
19
|
import { type AxisDomain } from "../../axis/numeric-axis";
|
|
@@ -23,6 +24,7 @@ import {
|
|
|
23
24
|
type SeriesChartRecord,
|
|
24
25
|
type NumericCategoryDomain,
|
|
25
26
|
type SeriesInfo,
|
|
27
|
+
type SeriesPipelineResult,
|
|
26
28
|
type BarColumns,
|
|
27
29
|
emptyBarColumns,
|
|
28
30
|
} from "./series-build";
|
|
@@ -65,6 +67,16 @@ export interface SeriesAutoFitCache {
|
|
|
65
67
|
hasRight: boolean;
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
/**
|
|
71
|
+
* One paint pass: the contiguous span of aggregates (`columns` indices,
|
|
72
|
+
* inclusive) sharing a glyph type. See {@link SeriesChart._glyphRuns}.
|
|
73
|
+
*/
|
|
74
|
+
export interface GlyphRun {
|
|
75
|
+
chartType: SeriesInfo["chartType"];
|
|
76
|
+
aggStart: number;
|
|
77
|
+
aggEnd: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
68
80
|
export interface CachedLocations {
|
|
69
81
|
u_proj_left: WebGLUniformLocation | null;
|
|
70
82
|
u_proj_right: WebGLUniformLocation | null;
|
|
@@ -123,6 +135,61 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
123
135
|
_splitPrefixes: string[] = [];
|
|
124
136
|
_series: SeriesInfo[] = [];
|
|
125
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Whether the CURRENT build's geometry was produced for facet-grid
|
|
140
|
+
* mode (`facet_mode: "grid"` with real splits). The render / hover
|
|
141
|
+
* paths branch on this — never on the live `_facetConfig` — so a
|
|
142
|
+
* `setPluginConfig` that flips `facet_mode` between a build and a
|
|
143
|
+
* redraw can't pair grid-mode rendering with overlay-built stack
|
|
144
|
+
* geometry (or vice versa). The host always follows a
|
|
145
|
+
* `plugin.restore` with an `update` → `loadAndRender`, so the flag
|
|
146
|
+
* catches up one build later.
|
|
147
|
+
*/
|
|
148
|
+
_facetActive = false;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Facet grid of the most recent faceted frame — one cell per
|
|
152
|
+
* split group. `null` in overlay / no-split frames. Read by the
|
|
153
|
+
* chrome overlay, hover hit-test, and tooltip pin to resolve the
|
|
154
|
+
* per-facet `PlotLayout`.
|
|
155
|
+
*/
|
|
156
|
+
_facetGrid: FacetGrid | null = null;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Per-split contiguous instance ranges in the uploaded bar
|
|
160
|
+
* buffers, indexed by `splitIdx`. Populated by
|
|
161
|
+
* `uploadBarInstances` when `_facetActive` (instances are emitted
|
|
162
|
+
* split-major); `null` in overlay mode. Each facet's draw binds
|
|
163
|
+
* the instance attributes at `start` and draws `count` instances.
|
|
164
|
+
*/
|
|
165
|
+
_facetBarRanges: { start: number; count: number }[] | null = null;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Paint order of the current build: consecutive aggregates sharing
|
|
169
|
+
* a glyph type, merged into runs, in `columns` declaration order
|
|
170
|
+
* (`aggIdx` ascending — later columns paint on top). The frame
|
|
171
|
+
* draws one pass per run; a homogeneous chart is a single run and
|
|
172
|
+
* degenerates to the legacy one-pass-per-type path. `aggEnd` is
|
|
173
|
+
* inclusive.
|
|
174
|
+
*/
|
|
175
|
+
_glyphRuns: GlyphRun[] = [];
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Per-aggregate contiguous instance ranges in the uploaded bar
|
|
179
|
+
* buffers, indexed by `aggIdx` — the overlay-mode counterpart of
|
|
180
|
+
* `_facetBarRanges`, populated by `uploadBarInstances`' aggregate
|
|
181
|
+
* counting sort. `null` until first upload.
|
|
182
|
+
*/
|
|
183
|
+
_barAggRanges: { start: number; count: number }[] | null = null;
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Faceted per-(split, aggregate) instance ranges,
|
|
187
|
+
* `[splitIdx][aggIdx]` — instances are emitted split-major then
|
|
188
|
+
* aggregate-ordered, so a facet's glyph-run slice is contiguous.
|
|
189
|
+
* `null` in overlay mode.
|
|
190
|
+
*/
|
|
191
|
+
_facetBarAggRanges: { start: number; count: number }[][] | null = null;
|
|
192
|
+
|
|
126
193
|
/**
|
|
127
194
|
* Columnar bar/area record storage. Indexed by bar slot in
|
|
128
195
|
* `[0, _bars.count)`. Replaces the legacy `SeriesChartRecord[]` to
|
|
@@ -205,8 +272,12 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
205
272
|
|
|
206
273
|
/**
|
|
207
274
|
* `domain_mode: "expand"` accumulators. Hold the running union of
|
|
208
|
-
* every prior build's
|
|
209
|
-
*
|
|
275
|
+
* every prior build's VALUE-axis extents (primary + alt) for as
|
|
276
|
+
* long as the option is active. The category axis deliberately has
|
|
277
|
+
* NO accumulator: `domain_mode` scopes to the value axis only on
|
|
278
|
+
* band charts (Y for the Y-family, X for X Bar) — a numeric or
|
|
279
|
+
* datetime `group_by` axis always fits the current data, so a
|
|
280
|
+
* streaming chart's time axis releases departed categories.
|
|
210
281
|
* Cleared in `resetExpandedDomain` — wired from the worker's
|
|
211
282
|
* `resetAllZooms` and from view-config mutations on the base
|
|
212
283
|
* class. `null` whenever the option is `"fit"` or the accumulator
|
|
@@ -214,7 +285,20 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
214
285
|
*/
|
|
215
286
|
_expandedLeftDomain: { min: number; max: number } | null = null;
|
|
216
287
|
_expandedRightDomain: { min: number; max: number } | null = null;
|
|
217
|
-
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The axis partition (per-aggregate axis side) the expand
|
|
291
|
+
* accumulators were accumulated under. An accumulator is only
|
|
292
|
+
* meaningful for the partition that produced it: when an aggregate
|
|
293
|
+
* moves sides (`columns_config.alt_axis` pin via a config-only
|
|
294
|
+
* `update()`, or a data-driven `auto_alt_y_axis` flip), the side it
|
|
295
|
+
* LEFT would otherwise retain its extent forever — the departed
|
|
296
|
+
* column's range never leaves the primary axis. A signature
|
|
297
|
+
* mismatch resets BOTH accumulators before the union. Keyed per
|
|
298
|
+
* AGGREGATE (not per series) so split-group growth on a streaming
|
|
299
|
+
* update never spuriously resets.
|
|
300
|
+
*/
|
|
301
|
+
_expandedAxisSig: string | null = null;
|
|
218
302
|
|
|
219
303
|
/**
|
|
220
304
|
* Numeric category-axis state. Populated only when `group_by` has
|
|
@@ -296,7 +380,13 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
296
380
|
*/
|
|
297
381
|
_visibleBarIndices: Int32Array = new Int32Array(0);
|
|
298
382
|
|
|
299
|
-
|
|
383
|
+
/**
|
|
384
|
+
* Hit-test rects for legend entries. Overlay mode: one entry per
|
|
385
|
+
* series (`seriesIds.length === 1`). Facet-grid mode: one entry
|
|
386
|
+
* per AGGREGATE, carrying every split's seriesId — a legend
|
|
387
|
+
* toggle hides/shows the aggregate across all facets at once.
|
|
388
|
+
*/
|
|
389
|
+
_legendRects: { seriesIds: number[]; rect: PlotRect }[] = [];
|
|
300
390
|
|
|
301
391
|
/**
|
|
302
392
|
* Cached legend layout — recomputed only on series-set / palette /
|
|
@@ -362,7 +452,14 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
362
452
|
this._hoveredBarIdx = -1;
|
|
363
453
|
this._hoveredSample = null;
|
|
364
454
|
if (this._glManager) {
|
|
365
|
-
|
|
455
|
+
// Route through the scheduler (not a direct
|
|
456
|
+
// `renderBarFrame`) so the frame is serialized
|
|
457
|
+
// against sibling charts sharing a pooled GL
|
|
458
|
+
// context in blit mode — a direct render would
|
|
459
|
+
// paint into the shared canvas outside the
|
|
460
|
+
// present cycle and leak into another plugin's
|
|
461
|
+
// frame.
|
|
462
|
+
this.requestRender(this._glManager);
|
|
366
463
|
}
|
|
367
464
|
}
|
|
368
465
|
},
|
|
@@ -479,6 +576,9 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
479
576
|
this._cornerBuffer = createQuadCornerBuffer(gl);
|
|
480
577
|
}
|
|
481
578
|
|
|
579
|
+
const facetSplits =
|
|
580
|
+
this._splitBy.length > 0 && this._facetConfig.facet_mode === "grid";
|
|
581
|
+
|
|
482
582
|
const result = buildSeriesPipeline({
|
|
483
583
|
columns,
|
|
484
584
|
numRows: endRow,
|
|
@@ -497,43 +597,55 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
497
597
|
bandInnerFrac: this._pluginConfig.band_inner_frac,
|
|
498
598
|
barInnerPad: this._pluginConfig.bar_inner_pad,
|
|
499
599
|
includeZero: this._pluginConfig.include_zero,
|
|
600
|
+
facetSplits,
|
|
500
601
|
scratchBars: this._bars,
|
|
501
602
|
scratchPosStack: this._posStackScratch,
|
|
502
603
|
scratchNegStack: this._negStackScratch,
|
|
503
604
|
});
|
|
504
605
|
|
|
505
|
-
// `domain_mode: "expand"` post-build union
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
//
|
|
606
|
+
// `domain_mode: "expand"` post-build union — VALUE axes only
|
|
607
|
+
// (`leftDomain`/`rightDomain` are the logical value domains,
|
|
608
|
+
// orientation-agnostic, so this is Y for the Y-family and X for
|
|
609
|
+
// X Bar). The category axis is NEVER expanded: it identifies
|
|
610
|
+
// the data, and pinning a numeric/datetime `group_by` axis to
|
|
611
|
+
// departed categories froze streaming charts on their full
|
|
612
|
+
// history. Each call mutates the pipeline result in place so
|
|
613
|
+
// the downstream assignments below (`_leftDomain`,
|
|
614
|
+
// `_rightDomain`) automatically pick up the grown extent.
|
|
509
615
|
// `"fit"` (or a fresh reset) leaves the result untouched and
|
|
510
616
|
// clears the accumulators so the next toggle starts fresh.
|
|
511
617
|
if (this._pluginConfig.domain_mode === "expand") {
|
|
618
|
+
const axisSig = expandAxisSignature(result);
|
|
619
|
+
if (this._expandedAxisSig !== axisSig) {
|
|
620
|
+
this._expandedLeftDomain = null;
|
|
621
|
+
this._expandedRightDomain = null;
|
|
622
|
+
this._expandedAxisSig = axisSig;
|
|
623
|
+
}
|
|
624
|
+
|
|
512
625
|
this._expandedLeftDomain = expandDomainInPlace(
|
|
513
626
|
this._expandedLeftDomain,
|
|
514
627
|
result.leftDomain,
|
|
515
628
|
);
|
|
629
|
+
|
|
516
630
|
if (result.rightDomain) {
|
|
517
631
|
this._expandedRightDomain = expandDomainInPlace(
|
|
518
632
|
this._expandedRightDomain,
|
|
519
633
|
result.rightDomain,
|
|
520
634
|
);
|
|
521
635
|
}
|
|
522
|
-
|
|
523
|
-
if (result.numericCategoryDomain) {
|
|
524
|
-
this._expandedCategoryDomain = expandDomainInPlace(
|
|
525
|
-
this._expandedCategoryDomain,
|
|
526
|
-
result.numericCategoryDomain,
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
636
|
} else {
|
|
530
637
|
this._expandedLeftDomain = null;
|
|
531
638
|
this._expandedRightDomain = null;
|
|
532
|
-
this.
|
|
639
|
+
this._expandedAxisSig = null;
|
|
533
640
|
}
|
|
534
641
|
|
|
535
642
|
this._aggregates = result.aggregates;
|
|
536
643
|
this._splitPrefixes = result.splitPrefixes;
|
|
644
|
+
|
|
645
|
+
// Stamp AFTER the build so render always branches on the mode
|
|
646
|
+
// the geometry was actually built for. A single "" prefix means
|
|
647
|
+
// no real splits — facet mode degenerates to the single plot.
|
|
648
|
+
this._facetActive = facetSplits && result.splitPrefixes.length > 1;
|
|
537
649
|
this._rowPaths = result.rowPaths;
|
|
538
650
|
this._numCategories = result.numCategories;
|
|
539
651
|
this._rowOffset = result.rowOffset;
|
|
@@ -586,6 +698,25 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
586
698
|
this._primaryValueLabel = uniqueAggLabels(result.series, 0);
|
|
587
699
|
this._altValueLabel = uniqueAggLabels(result.series, 1);
|
|
588
700
|
|
|
701
|
+
// Paint-order runs: every series of an aggregate shares one
|
|
702
|
+
// glyph type (`resolveChartType` is per-aggName), so sampling
|
|
703
|
+
// the aggregate's first series suffices. MUST be computed
|
|
704
|
+
// before `uploadBarInstances` below — the aggregate counting
|
|
705
|
+
// sort publishes ranges the run passes draw from.
|
|
706
|
+
this._glyphRuns.length = 0;
|
|
707
|
+
{
|
|
708
|
+
const P = Math.max(1, result.splitPrefixes.length);
|
|
709
|
+
for (let k = 0; k < result.aggregates.length; k++) {
|
|
710
|
+
const chartType = result.series[k * P].chartType;
|
|
711
|
+
const last = this._glyphRuns[this._glyphRuns.length - 1];
|
|
712
|
+
if (last && last.chartType === chartType) {
|
|
713
|
+
last.aggEnd = k;
|
|
714
|
+
} else {
|
|
715
|
+
this._glyphRuns.push({ chartType, aggStart: k, aggEnd: k });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
589
720
|
// Pre-build the area-strip lookup index (seriesId * 1e9 + catIdx
|
|
590
721
|
// → bar slot). Legacy code rebuilt this every frame inside
|
|
591
722
|
// `drawAreas`. The index is derived purely from `_bars` and is
|
|
@@ -604,6 +735,17 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
604
735
|
this._paletteCacheKey = null;
|
|
605
736
|
this._catExtentsHidden = null;
|
|
606
737
|
this._lastUploadedColors = null;
|
|
738
|
+
|
|
739
|
+
// Hover records index into `_bars` / `_series` and MUST NOT
|
|
740
|
+
// survive a build that replaces them: bar-column capacity is
|
|
741
|
+
// reused across builds, so a stale `_hoveredBarIdx` reads a
|
|
742
|
+
// leftover `seriesId` past the new series set (and a retained
|
|
743
|
+
// `_hoveredSample` carries one verbatim), crashing the tooltip
|
|
744
|
+
// pass of the next present when the pointer rests over a glyph
|
|
745
|
+
// through a host `restore`. The next mousemove re-hit-tests
|
|
746
|
+
// against the new data.
|
|
747
|
+
this._hoveredBarIdx = -1;
|
|
748
|
+
this._hoveredSample = null;
|
|
607
749
|
this._sampleValid = result.sampleValid;
|
|
608
750
|
this._leftDomain = result.leftDomain;
|
|
609
751
|
this._rightDomain = result.rightDomain;
|
|
@@ -638,7 +780,7 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
638
780
|
override resetExpandedDomain(): void {
|
|
639
781
|
this._expandedLeftDomain = null;
|
|
640
782
|
this._expandedRightDomain = null;
|
|
641
|
-
this.
|
|
783
|
+
this._expandedAxisSig = null;
|
|
642
784
|
}
|
|
643
785
|
|
|
644
786
|
protected destroyInternal(): void {
|
|
@@ -654,6 +796,12 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
654
796
|
this._program = null;
|
|
655
797
|
this._locations = null;
|
|
656
798
|
this._cornerBuffer = null;
|
|
799
|
+
this._facetActive = false;
|
|
800
|
+
this._facetGrid = null;
|
|
801
|
+
this._facetBarRanges = null;
|
|
802
|
+
this._facetBarAggRanges = null;
|
|
803
|
+
this._barAggRanges = null;
|
|
804
|
+
this._glyphRuns.length = 0;
|
|
657
805
|
this._bars = emptyBarColumns();
|
|
658
806
|
this._series = [];
|
|
659
807
|
this._barSeries = [];
|
|
@@ -669,6 +817,8 @@ export class SeriesChart extends CategoricalYChart {
|
|
|
669
817
|
this._rowPaths = [];
|
|
670
818
|
this._numCategories = 0;
|
|
671
819
|
this._hiddenSeries.clear();
|
|
820
|
+
this._hoveredBarIdx = -1;
|
|
821
|
+
this._hoveredSample = null;
|
|
672
822
|
}
|
|
673
823
|
}
|
|
674
824
|
|
|
@@ -715,12 +865,37 @@ function uniqueAggLabels(series: SeriesInfo[], axis: 0 | 1): string {
|
|
|
715
865
|
return ordered.join(", ");
|
|
716
866
|
}
|
|
717
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Per-aggregate axis-partition signature for the `domain_mode:
|
|
870
|
+
* "expand"` accumulators. Invariant: every series of an aggregate
|
|
871
|
+
* shares one axis side (both axis-override loops in the build pipeline
|
|
872
|
+
* assign by `aggIdx`), so sampling the aggregate's first series
|
|
873
|
+
* suffices. NUL-joined — aggregate names may contain any printable
|
|
874
|
+
* separator.
|
|
875
|
+
*/
|
|
876
|
+
function expandAxisSignature(result: SeriesPipelineResult): string {
|
|
877
|
+
const P = Math.max(1, result.splitPrefixes.length);
|
|
878
|
+
const parts: string[] = [];
|
|
879
|
+
for (let k = 0; k < result.aggregates.length; k++) {
|
|
880
|
+
parts.push(`${result.aggregates[k]}:${result.series[k * P].axis}`);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
return parts.join("\u0000");
|
|
884
|
+
}
|
|
885
|
+
|
|
718
886
|
/**
|
|
719
887
|
* Resolve the per-series palette and stamp it onto `_series[i].color`.
|
|
720
888
|
* Cached on `_paletteCache` keyed by reference identity of the theme
|
|
721
|
-
* inputs +
|
|
722
|
-
* via `invalidateTheme`) or a data load (which clears
|
|
723
|
-
* forces re-resolution.
|
|
889
|
+
* inputs + resolved color count — only `restyle()` (which clears
|
|
890
|
+
* `_paletteCache` via `invalidateTheme`) or a data load (which clears
|
|
891
|
+
* it explicitly) forces re-resolution.
|
|
892
|
+
*
|
|
893
|
+
* Facet-grid mode colors by AGGREGATE, not by (agg × split) series:
|
|
894
|
+
* the facet is the axis of splitting, so aggregate `k` renders the
|
|
895
|
+
* same color in every facet and the legend lists aggregates once.
|
|
896
|
+
* Overlay mode keeps the per-series palette (splits are distinguished
|
|
897
|
+
* by color there). `_facetActive` only changes at build time, which
|
|
898
|
+
* clears the cache, so the stamp mode can't go stale.
|
|
724
899
|
*
|
|
725
900
|
* Returns true when the cache changed (caller invalidates color upload).
|
|
726
901
|
*/
|
|
@@ -728,7 +903,9 @@ export function ensurePalette(chart: SeriesChart): boolean {
|
|
|
728
903
|
const theme = chart._resolveTheme();
|
|
729
904
|
const seriesPalette = theme.seriesPalette;
|
|
730
905
|
const gradientStops = theme.gradientStops;
|
|
731
|
-
const
|
|
906
|
+
const paletteCount = chart._facetActive
|
|
907
|
+
? Math.max(1, chart._aggregates.length)
|
|
908
|
+
: chart._series.length;
|
|
732
909
|
|
|
733
910
|
const key = chart._paletteCacheKey;
|
|
734
911
|
if (
|
|
@@ -736,7 +913,7 @@ export function ensurePalette(chart: SeriesChart): boolean {
|
|
|
736
913
|
key &&
|
|
737
914
|
key.seriesPalette === seriesPalette &&
|
|
738
915
|
key.gradientStops === gradientStops &&
|
|
739
|
-
key.seriesLength ===
|
|
916
|
+
key.seriesLength === paletteCount
|
|
740
917
|
) {
|
|
741
918
|
return false;
|
|
742
919
|
}
|
|
@@ -744,13 +921,19 @@ export function ensurePalette(chart: SeriesChart): boolean {
|
|
|
744
921
|
const palette = resolvePaletteCached(
|
|
745
922
|
seriesPalette,
|
|
746
923
|
gradientStops,
|
|
747
|
-
|
|
924
|
+
paletteCount,
|
|
748
925
|
);
|
|
749
926
|
chart._paletteCache = palette;
|
|
750
|
-
chart._paletteCacheKey = {
|
|
927
|
+
chart._paletteCacheKey = {
|
|
928
|
+
seriesPalette,
|
|
929
|
+
gradientStops,
|
|
930
|
+
seriesLength: paletteCount,
|
|
931
|
+
};
|
|
751
932
|
|
|
752
933
|
for (let i = 0; i < chart._series.length; i++) {
|
|
753
|
-
chart._series[i].color =
|
|
934
|
+
chart._series[i].color = chart._facetActive
|
|
935
|
+
? palette[chart._series[i].aggIdx]
|
|
936
|
+
: palette[i];
|
|
754
937
|
}
|
|
755
938
|
|
|
756
939
|
return true;
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
import type { SunburstChart } from "./sunburst";
|
|
14
14
|
import { NULL_NODE } from "../common/node-store";
|
|
15
15
|
import {
|
|
16
|
-
renderSunburstFrame,
|
|
17
16
|
renderSunburstChromeOverlay,
|
|
18
17
|
facetCenterForNode,
|
|
19
18
|
} from "./sunburst-render";
|
|
@@ -264,7 +263,7 @@ export function handleSunburstClick(
|
|
|
264
263
|
}
|
|
265
264
|
|
|
266
265
|
if (chart._glManager) {
|
|
267
|
-
|
|
266
|
+
chart.requestRender(chart._glManager);
|
|
268
267
|
}
|
|
269
268
|
}
|
|
270
269
|
|
|
@@ -289,7 +288,7 @@ export function handleSunburstClick(
|
|
|
289
288
|
function drillTo(chart: SunburstChart, nodeId: number): void {
|
|
290
289
|
treeDrillTo(chart, nodeId, () => {
|
|
291
290
|
if (chart._glManager) {
|
|
292
|
-
|
|
291
|
+
chart.requestRender(chart._glManager);
|
|
293
292
|
}
|
|
294
293
|
});
|
|
295
294
|
}
|
|
@@ -189,7 +189,9 @@ export function renderSunburstFrame(
|
|
|
189
189
|
drawArcs(chart, gl, glManager, 0, chart._instanceCount);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
// Deferred past the GPU fence (see `_defer2D`) so the chrome canvas
|
|
193
|
+
// doesn't present ahead of the GL arcs on resize.
|
|
194
|
+
chart._defer2D(() => renderSunburstChromeOverlay(chart));
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
/**
|
|
@@ -13,10 +13,7 @@
|
|
|
13
13
|
import type { TreemapChart } from "./treemap";
|
|
14
14
|
import { NULL_NODE } from "../common/node-store";
|
|
15
15
|
import { PADDING_LABEL } from "./treemap-layout";
|
|
16
|
-
import {
|
|
17
|
-
renderTreemapFrame,
|
|
18
|
-
renderTreemapChromeOverlay,
|
|
19
|
-
} from "./treemap-render";
|
|
16
|
+
import { renderTreemapChromeOverlay } from "./treemap-render";
|
|
20
17
|
import {
|
|
21
18
|
buildTreeTooltipLines,
|
|
22
19
|
dismissTreePinnedTooltip,
|
|
@@ -254,7 +251,7 @@ export function handleTreemapDblClick(
|
|
|
254
251
|
function drillTo(chart: TreemapChart, nodeId: number): void {
|
|
255
252
|
treeDrillTo(chart, nodeId, () => {
|
|
256
253
|
if (chart._glManager) {
|
|
257
|
-
|
|
254
|
+
chart.requestRender(chart._glManager);
|
|
258
255
|
}
|
|
259
256
|
});
|
|
260
257
|
}
|
|
@@ -173,7 +173,9 @@ export function renderTreemapFrame(
|
|
|
173
173
|
|
|
174
174
|
gl.drawArrays(gl.TRIANGLES, 0, chart._vertexCount);
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
// Deferred past the GPU fence (see `_defer2D`) so the chrome canvas
|
|
177
|
+
// doesn't present ahead of the GL tiles on resize.
|
|
178
|
+
chart._defer2D(() => renderTreemapChromeOverlay(chart));
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
/**
|
|
@@ -109,7 +109,12 @@ export class TreemapChart extends TreeChartBase {
|
|
|
109
109
|
) {
|
|
110
110
|
this._hoveredNodeId = NULL_NODE;
|
|
111
111
|
if (this._glManager) {
|
|
112
|
-
|
|
112
|
+
// Schedule through the render scheduler rather
|
|
113
|
+
// than painting directly — a direct frame render
|
|
114
|
+
// bypasses the per-context serialization and, in
|
|
115
|
+
// pooled blit mode, corrupts a sibling chart's
|
|
116
|
+
// in-flight shared-canvas frame.
|
|
117
|
+
this.requestRender(this._glManager);
|
|
113
118
|
}
|
|
114
119
|
}
|
|
115
120
|
},
|
|
@@ -50,10 +50,19 @@ export interface TypedArrayWindowOptions {
|
|
|
50
50
|
* `Promise`, the underlying `with_typed_arrays` call awaits it before
|
|
51
51
|
* releasing the backing Arrow buffer. Callers must not retain any
|
|
52
52
|
* `ColumnData` reference past `render`'s resolution.
|
|
53
|
+
*
|
|
54
|
+
* `render` also receives the DELIVERED row count — the shortest
|
|
55
|
+
* column's length, which may be less than the requested `end_row`:
|
|
56
|
+
* the engine clamps the window to the view's size at fetch time, and
|
|
57
|
+
* on a fast-updating table with deletions that size can shrink after
|
|
58
|
+
* the caller's `num_rows()` round-trip. Build pipelines must iterate
|
|
59
|
+
* to THIS count, never the requested one — a typed array indexed past
|
|
60
|
+
* its length returns `undefined`, which no amount of validity-bitmap
|
|
61
|
+
* checking catches (the bitmap read is out-of-bounds too).
|
|
53
62
|
*/
|
|
54
63
|
export async function viewToColumnDataMap(
|
|
55
64
|
view: View,
|
|
56
|
-
render: (data: ColumnDataMap) => void | Promise<void>,
|
|
65
|
+
render: (data: ColumnDataMap, numRows: number) => void | Promise<void>,
|
|
57
66
|
options?: TypedArrayWindowOptions,
|
|
58
67
|
): Promise<void> {
|
|
59
68
|
const result: ColumnDataMap = new Map();
|
|
@@ -101,7 +110,18 @@ export async function viewToColumnDataMap(
|
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
|
|
104
|
-
|
|
113
|
+
let numRows = 0;
|
|
114
|
+
if (result.size > 0) {
|
|
115
|
+
numRows = Infinity;
|
|
116
|
+
for (const col of result.values()) {
|
|
117
|
+
const n = col.indices?.length ?? col.values?.length ?? 0;
|
|
118
|
+
if (n < numRows) {
|
|
119
|
+
numRows = n;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await render(result, numRows);
|
|
105
125
|
},
|
|
106
126
|
);
|
|
107
127
|
}
|
package/src/ts/event-detail.ts
CHANGED
|
@@ -39,6 +39,14 @@ export interface PerspectiveClickDetail {
|
|
|
39
39
|
* `[<col>, "==", <value>]` clauses.
|
|
40
40
|
*/
|
|
41
41
|
config: Partial<ViewConfig>;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The source panel's `slot` name in a multi-panel `<perspective-viewer>`,
|
|
45
|
+
* so the host can attribute the click to the originating panel. `undefined`
|
|
46
|
+
* for a lone, unslotted viewer. Mirrors `PerspectiveSelectDetail.panel` and
|
|
47
|
+
* the datagrid `PerspectiveClickDetail`.
|
|
48
|
+
*/
|
|
49
|
+
panel?: string;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
export { PerspectiveSelectDetail } from "@perspective-dev/viewer/src/ts/extensions.js";
|
package/src/ts/index.ts
CHANGED
|
@@ -23,17 +23,17 @@ export function register(...plugin_names: string[]) {
|
|
|
23
23
|
: CHARTS.map((chart) => chart.name),
|
|
24
24
|
);
|
|
25
25
|
|
|
26
|
+
const already_registered: string[] = [];
|
|
27
|
+
|
|
26
28
|
CHARTS.forEach((chart) => {
|
|
27
29
|
if (plugins.has(chart.name)) {
|
|
28
30
|
const tagName = `perspective-viewer-charts-${chart.tag}`;
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// boundary, and the renderer constructs the impl from
|
|
36
|
-
// its own `CHART_IMPLS` registry.
|
|
32
|
+
if (customElements.get(tagName)) {
|
|
33
|
+
already_registered.push(tagName);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
37
|
customElements.define(
|
|
38
38
|
tagName,
|
|
39
39
|
class extends HTMLPerspectiveViewerWebGLPluginElement {
|
|
@@ -48,6 +48,12 @@ export function register(...plugin_names: string[]) {
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
|
+
|
|
52
|
+
if (already_registered.length > 0) {
|
|
53
|
+
console.warn(
|
|
54
|
+
`viewer-charts plugins already registered (${already_registered.length}); skipping duplicate registration (Perspective was loaded more than once on this page).`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
register();
|
|
@@ -55,6 +55,13 @@ export class RawEventForwarder {
|
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
this._onPointerDown = (e: PointerEvent) => {
|
|
58
|
+
// Only forward primary-button pointerdowns. Right/middle clicks must
|
|
59
|
+
// reach the panel's `contextmenu` handler — capturing the pointer
|
|
60
|
+
// here would otherwise swallow the right-click.
|
|
61
|
+
if (e.button !== 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
const rect = element.getBoundingClientRect();
|
|
59
66
|
const mx = e.clientX - rect.left;
|
|
60
67
|
const my = e.clientY - rect.top;
|
|
@@ -388,6 +388,14 @@ export class ZoomController {
|
|
|
388
388
|
};
|
|
389
389
|
|
|
390
390
|
this._onPointerDown = (e: PointerEvent) => {
|
|
391
|
+
// Only the primary (left) button starts a pan. Right/middle clicks
|
|
392
|
+
// must fall through so the panel's `contextmenu` handler can open
|
|
393
|
+
// the menu — capturing the pointer here would otherwise swallow the
|
|
394
|
+
// right-click.
|
|
395
|
+
if (e.button !== 0) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
391
399
|
const rect = element.getBoundingClientRect();
|
|
392
400
|
const mouseX = e.clientX - rect.left;
|
|
393
401
|
const mouseY = e.clientY - rect.top;
|
package/src/ts/layout/ticks.ts
CHANGED
|
@@ -72,8 +72,17 @@ export function computeNiceTicks(
|
|
|
72
72
|
/**
|
|
73
73
|
* Format a numeric tick value for display.
|
|
74
74
|
* Uses K/M/B suffixes for large numbers, fixed decimals for small.
|
|
75
|
+
*
|
|
76
|
+
* Total over any input: label formatters run inside render passes, so a
|
|
77
|
+
* non-finite value (or `undefined` smuggled in by an upstream
|
|
78
|
+
* out-of-bounds read) must degrade to a placeholder, never throw the
|
|
79
|
+
* frame away.
|
|
75
80
|
*/
|
|
76
81
|
export function formatTickValue(val: number): string {
|
|
82
|
+
if (!Number.isFinite(val)) {
|
|
83
|
+
return "-";
|
|
84
|
+
}
|
|
85
|
+
|
|
77
86
|
const abs = Math.abs(val);
|
|
78
87
|
if (abs === 0) {
|
|
79
88
|
return "0";
|