@perspective-dev/viewer-charts 4.5.0 → 4.5.2
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/LICENSE.md +193 -0
- package/dist/cdn/perspective-viewer-charts.js +2 -2
- package/dist/cdn/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/axis/bar-axis.d.ts +9 -1
- package/dist/esm/axis/categorical-axis.d.ts +0 -2
- package/dist/esm/charts/cartesian/cartesian.d.ts +26 -0
- package/dist/esm/charts/common/category-axis-resolver.d.ts +43 -1
- package/dist/esm/charts/common/expand-domain.d.ts +20 -0
- package/dist/esm/charts/common/tree-chart.d.ts +7 -0
- package/dist/esm/charts/common/tree-chrome.d.ts +23 -1
- package/dist/esm/charts/common/tree-interact.d.ts +46 -0
- package/dist/esm/charts/series/glyphs/draw-lines.d.ts +11 -4
- package/dist/esm/charts/series/series-build.d.ts +38 -2
- package/dist/esm/charts/series/series-render.d.ts +1 -4
- package/dist/esm/charts/series/series-type.d.ts +19 -17
- package/dist/esm/charts/series/series.d.ts +16 -0
- package/dist/esm/charts/sunburst/sunburst-interact.d.ts +1 -1
- package/dist/esm/charts/treemap/treemap-interact.d.ts +1 -6
- package/dist/esm/config.d.ts +19 -0
- package/dist/esm/interaction/host-sink-message.d.ts +10 -28
- package/dist/esm/interaction/raw-event-forwarder.d.ts +6 -7
- package/dist/esm/interaction/zoom-controller.d.ts +31 -20
- package/dist/esm/interaction/zoom-router.d.ts +3 -26
- package/dist/esm/perspective-viewer-charts.js +2 -2
- package/dist/esm/perspective-viewer-charts.js.map +3 -3
- package/dist/esm/plugin/plugin.d.ts +15 -3
- package/dist/esm/render/scheduler.d.ts +23 -0
- package/dist/esm/theme/palette.d.ts +0 -5
- package/dist/esm/transport/protocol.d.ts +2 -7
- package/dist/esm/webgl/context-manager.d.ts +100 -117
- package/dist/esm/webgl/context-pool.d.ts +40 -0
- package/dist/esm/webgl/gl-context.d.ts +54 -0
- package/dist/esm/worker/renderer.worker.d.ts +2 -4
- package/package.json +45 -45
- package/src/ts/axis/bar-axis.ts +74 -45
- package/src/ts/axis/categorical-axis.ts +0 -2
- package/src/ts/charts/candlestick/candlestick-render.ts +10 -7
- package/src/ts/charts/candlestick/candlestick.ts +10 -29
- package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +36 -2
- package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +36 -2
- package/src/ts/charts/cartesian/cartesian-build.ts +143 -9
- package/src/ts/charts/cartesian/cartesian-render.ts +205 -30
- package/src/ts/charts/cartesian/cartesian.ts +43 -4
- package/src/ts/charts/cartesian/glyphs/density.ts +36 -41
- package/src/ts/charts/cartesian/glyphs/lines.ts +13 -15
- package/src/ts/charts/cartesian/glyphs/points.ts +12 -17
- package/src/ts/charts/chart-base.ts +20 -6
- package/src/ts/charts/chart.ts +1 -1
- package/src/ts/charts/common/category-axis-resolver.ts +135 -1
- package/src/ts/charts/common/expand-domain.ts +40 -0
- package/src/ts/charts/common/tree-chart.ts +16 -0
- package/src/ts/charts/common/tree-chrome.ts +86 -1
- package/src/ts/charts/common/tree-interact.ts +209 -0
- package/src/ts/charts/heatmap/heatmap-render.ts +9 -11
- package/src/ts/charts/series/glyphs/draw-areas.ts +30 -1
- package/src/ts/charts/series/glyphs/draw-lines.ts +151 -76
- package/src/ts/charts/series/series-build.ts +394 -21
- package/src/ts/charts/series/series-render.ts +159 -38
- package/src/ts/charts/series/series-type.ts +37 -17
- package/src/ts/charts/series/series.ts +63 -68
- package/src/ts/charts/sunburst/sunburst-interact.ts +18 -162
- package/src/ts/charts/sunburst/sunburst-render.ts +24 -89
- package/src/ts/charts/sunburst/sunburst.ts +1 -15
- package/src/ts/charts/treemap/treemap-interact.ts +22 -189
- package/src/ts/charts/treemap/treemap-render.ts +19 -46
- package/src/ts/charts/treemap/treemap.ts +1 -16
- package/src/ts/config.ts +21 -1
- package/src/ts/interaction/host-sink-message.ts +33 -22
- package/src/ts/interaction/raw-event-forwarder.ts +10 -12
- package/src/ts/interaction/zoom-controller.ts +120 -83
- package/src/ts/interaction/zoom-router.ts +3 -126
- package/src/ts/map/tile-layer.ts +13 -13
- package/src/ts/plugin/plugin.ts +176 -196
- package/src/ts/render/scheduler.ts +125 -42
- package/src/ts/shaders/line-uniform.frag.glsl +2 -1
- package/src/ts/shaders/line-uniform.vert.glsl +19 -0
- package/src/ts/theme/palette.ts +1 -4
- package/src/ts/transport/protocol.ts +3 -8
- package/src/ts/webgl/context-manager.ts +195 -189
- package/src/ts/webgl/context-pool.ts +78 -0
- package/src/ts/webgl/gl-context.ts +191 -0
- package/src/ts/worker/dispatch.ts +0 -1
- package/src/ts/worker/renderer.worker.ts +72 -65
- package/src/ts/worker/session-host.ts +29 -0
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
13
|
import type { ColumnDataMap, ColumnData } from "../../data/view-reader";
|
|
14
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
CategoricalDomain,
|
|
16
|
+
CategoricalLevel,
|
|
17
|
+
} from "../../axis/categorical-axis";
|
|
15
18
|
import { buildGroupRuns } from "../../axis/categorical-axis-core";
|
|
16
19
|
import { formatTickValue, formatDateTickValue } from "../../layout/ticks";
|
|
17
20
|
|
|
@@ -312,3 +315,134 @@ export function resolveCategoryAxis(
|
|
|
312
315
|
|
|
313
316
|
return { rowPaths, numCategories, rowOffset };
|
|
314
317
|
}
|
|
318
|
+
|
|
319
|
+
export interface ValueCategoryColumn {
|
|
320
|
+
/**
|
|
321
|
+
* Source aggregate column name; used only for the axis label fallback.
|
|
322
|
+
*/
|
|
323
|
+
name: string;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Post-aggregation perspective type string from `chart._columnTypes`
|
|
327
|
+
* (`"string"` is what triggers categorical mode).
|
|
328
|
+
*/
|
|
329
|
+
type: string;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The actual `ColumnData` from the view. May be undefined when the
|
|
333
|
+
* caller couldn't resolve the column (treated as all-null).
|
|
334
|
+
*/
|
|
335
|
+
data: ColumnData | undefined;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export interface ValueCategoryDomain {
|
|
339
|
+
/**
|
|
340
|
+
* Single-level `CategoricalDomain` shared across all input columns.
|
|
341
|
+
* `levels[0].labels` is the dictionary in slot order.
|
|
342
|
+
*/
|
|
343
|
+
domain: CategoricalDomain;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Per-column slot-index buffers. Length === `numCategories`.
|
|
347
|
+
* Indexed in the same order as the input `columns` array.
|
|
348
|
+
*/
|
|
349
|
+
perColumnSlots: Int32Array[];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Build a single shared categorical domain across one or more aggregate
|
|
354
|
+
* columns that land on the same axis side (primary or alt). Implements
|
|
355
|
+
* the "all-or-nothing per axis side" rule: returns `null` (= caller stays
|
|
356
|
+
* numeric) when any column is non-string; otherwise returns a single-
|
|
357
|
+
* level domain with the dictionary built in first-seen row order plus
|
|
358
|
+
* per-column slot indices the build pipeline writes into its pixel/slot
|
|
359
|
+
* buffer.
|
|
360
|
+
*
|
|
361
|
+
* Null / invalid rows surface as a `"(null)"` slot that's lazily added
|
|
362
|
+
* to the dictionary on first encounter — no reserved slot 0 when the
|
|
363
|
+
* data has no missing values.
|
|
364
|
+
*/
|
|
365
|
+
export function resolveValueCategoryDomain(
|
|
366
|
+
columns: ValueCategoryColumn[],
|
|
367
|
+
numRows: number,
|
|
368
|
+
rowOffset: number,
|
|
369
|
+
axisLabel: string,
|
|
370
|
+
): ValueCategoryDomain | null {
|
|
371
|
+
if (columns.length === 0) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
for (const c of columns) {
|
|
376
|
+
if (c.type !== "string") {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const numCategories = Math.max(0, numRows - rowOffset);
|
|
382
|
+
const dictionary: string[] = [];
|
|
383
|
+
const seen = new Map<string, number>();
|
|
384
|
+
const perColumnSlots: Int32Array[] = columns.map(
|
|
385
|
+
() => new Int32Array(numCategories),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const slotFor = (s: string): number => {
|
|
389
|
+
let slot = seen.get(s);
|
|
390
|
+
if (slot === undefined) {
|
|
391
|
+
slot = dictionary.length;
|
|
392
|
+
dictionary.push(s);
|
|
393
|
+
seen.set(s, slot);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return slot;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
for (let ci = 0; ci < columns.length; ci++) {
|
|
400
|
+
const col = columns[ci].data;
|
|
401
|
+
const slots = perColumnSlots[ci];
|
|
402
|
+
for (let r = 0; r < numCategories; r++) {
|
|
403
|
+
const rowIdx = r + rowOffset;
|
|
404
|
+
let label: string;
|
|
405
|
+
if (!col) {
|
|
406
|
+
label = "(null)";
|
|
407
|
+
} else {
|
|
408
|
+
const valid = col.valid;
|
|
409
|
+
const isValid = valid
|
|
410
|
+
? !!((valid[rowIdx >> 3] >> (rowIdx & 7)) & 1)
|
|
411
|
+
: true;
|
|
412
|
+
if (!isValid) {
|
|
413
|
+
label = "(null)";
|
|
414
|
+
} else if (col.indices && col.dictionary) {
|
|
415
|
+
label = col.dictionary[col.indices[rowIdx]] ?? "(null)";
|
|
416
|
+
} else if (col.values) {
|
|
417
|
+
const v = col.values[rowIdx];
|
|
418
|
+
label = v == null ? "(null)" : String(v);
|
|
419
|
+
} else {
|
|
420
|
+
label = "(null)";
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
slots[r] = slotFor(label);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
let maxLabelChars = 0;
|
|
429
|
+
for (const s of dictionary) {
|
|
430
|
+
if (s.length > maxLabelChars) {
|
|
431
|
+
maxLabelChars = s.length;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const level: CategoricalLevel = {
|
|
436
|
+
labels: dictionary.slice(),
|
|
437
|
+
runs: [],
|
|
438
|
+
maxLabelChars,
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
const domain: CategoricalDomain = {
|
|
442
|
+
levels: [level],
|
|
443
|
+
numRows: dictionary.length,
|
|
444
|
+
levelLabels: [axisLabel],
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
return { domain, perColumnSlots };
|
|
448
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Numeric extent — used by series + candlestick build pipelines for
|
|
15
|
+
* value / category axis domains.
|
|
16
|
+
*/
|
|
17
|
+
export interface Domain {
|
|
18
|
+
min: number;
|
|
19
|
+
max: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Union `next` (a freshly-computed extent) with `prev` (the prior
|
|
24
|
+
* accumulator) IN PLACE on `next`, then return a fresh copy to store
|
|
25
|
+
* back as the new accumulator. Idempotent when `prev` is null — `next`
|
|
26
|
+
* is left untouched.
|
|
27
|
+
*
|
|
28
|
+
* Used by the `domain_mode: "expand"` mirror-back step in the series /
|
|
29
|
+
* candlestick / cartesian build pipelines: mutating `next` in place
|
|
30
|
+
* means every downstream assignment that reads from the pipeline
|
|
31
|
+
* result struct automatically picks up the grown extent.
|
|
32
|
+
*/
|
|
33
|
+
export function expandDomainInPlace(prev: Domain | null, next: Domain): Domain {
|
|
34
|
+
if (prev) {
|
|
35
|
+
next.min = Math.min(prev.min, next.min);
|
|
36
|
+
next.max = Math.max(prev.max, next.max);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { min: next.min, max: next.max };
|
|
40
|
+
}
|
|
@@ -11,9 +11,25 @@
|
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
13
|
import { AbstractChart } from "../chart-base";
|
|
14
|
+
import type { ColumnDataMap } from "../../data/view-reader";
|
|
14
15
|
import { NodeStore, NULL_NODE } from "./node-store";
|
|
15
16
|
import { LazyTooltip } from "../../interaction/lazy-tooltip";
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Sentinel fallback for the Size slot when the user hasn't picked one:
|
|
20
|
+
* use the first non-metadata column in the incoming view. Tree charts
|
|
21
|
+
* still need *some* numeric-ish column to size geometry.
|
|
22
|
+
*/
|
|
23
|
+
export function firstNonMetadataColumn(columns: ColumnDataMap): string {
|
|
24
|
+
for (const k of columns.keys()) {
|
|
25
|
+
if (!k.startsWith("__")) {
|
|
26
|
+
return k;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
|
|
17
33
|
/**
|
|
18
34
|
* Shared state for hierarchical charts (treemap, sunburst). Holds the
|
|
19
35
|
* tree store + streaming-insert scaffolding + per-row tooltip data
|
|
@@ -10,7 +10,17 @@
|
|
|
10
10
|
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
11
|
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
12
|
|
|
13
|
-
import type { Context2D } from "../canvas-types";
|
|
13
|
+
import type { Canvas2D, Context2D } from "../canvas-types";
|
|
14
|
+
import type { PlotRect } from "../../layout/plot-layout";
|
|
15
|
+
import { PlotLayout } from "../../layout/plot-layout";
|
|
16
|
+
import type { GradientStop } from "../../theme/gradient";
|
|
17
|
+
import type { Vec3 } from "../../theme/palette";
|
|
18
|
+
import type { Theme } from "../../theme/theme";
|
|
19
|
+
import {
|
|
20
|
+
renderCategoricalLegend,
|
|
21
|
+
renderCategoricalLegendAt,
|
|
22
|
+
renderLegend,
|
|
23
|
+
} from "../../axis/legend";
|
|
14
24
|
import type { TreeChartBase } from "./tree-chart";
|
|
15
25
|
import { drawTooltipBox } from "./draw-tooltip-box";
|
|
16
26
|
|
|
@@ -121,3 +131,78 @@ export function renderTreeTooltip(
|
|
|
121
131
|
fontFamily,
|
|
122
132
|
);
|
|
123
133
|
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Paint a color legend (categorical swatches or numeric gradient bar)
|
|
137
|
+
* for a tree chart. Shared by sunburst + treemap; both consult
|
|
138
|
+
* `_colorMode` / `_uniqueColorLabels.size` / `_colorMin..max` the same
|
|
139
|
+
* way.
|
|
140
|
+
*
|
|
141
|
+
* `categoricalRect`, when non-null, is used as the explicit rect for
|
|
142
|
+
* the categorical-swatch variant (sunburst's faceted mode passes
|
|
143
|
+
* `FacetGrid.legendRect` here). Numeric mode always derives from a
|
|
144
|
+
* synthetic single-plot `PlotLayout` to match the legacy per-chart
|
|
145
|
+
* branch — its gradient bar's vertical span doesn't fit the
|
|
146
|
+
* categorical legend's compact rect.
|
|
147
|
+
*
|
|
148
|
+
* Returns silently when the color slot is empty, when categorical mode
|
|
149
|
+
* has only one label, or when numeric mode has a degenerate
|
|
150
|
+
* (`min >= max`) extent.
|
|
151
|
+
*/
|
|
152
|
+
export function renderTreeColorLegend(
|
|
153
|
+
chart: TreeChartBase,
|
|
154
|
+
canvas: Canvas2D,
|
|
155
|
+
palette: Vec3[],
|
|
156
|
+
stops: GradientStop[],
|
|
157
|
+
theme: Theme,
|
|
158
|
+
cssWidth: number,
|
|
159
|
+
cssHeight: number,
|
|
160
|
+
categoricalRect: PlotRect | null = null,
|
|
161
|
+
): void {
|
|
162
|
+
if (chart._colorMode === "series" && chart._uniqueColorLabels.size > 1) {
|
|
163
|
+
if (categoricalRect) {
|
|
164
|
+
renderCategoricalLegendAt(
|
|
165
|
+
canvas,
|
|
166
|
+
categoricalRect,
|
|
167
|
+
chart._uniqueColorLabels,
|
|
168
|
+
palette,
|
|
169
|
+
theme,
|
|
170
|
+
);
|
|
171
|
+
} else {
|
|
172
|
+
renderCategoricalLegend(
|
|
173
|
+
canvas,
|
|
174
|
+
syntheticLegendLayout(cssWidth, cssHeight),
|
|
175
|
+
chart._uniqueColorLabels,
|
|
176
|
+
palette,
|
|
177
|
+
theme,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
} else if (
|
|
181
|
+
chart._colorMode === "numeric" &&
|
|
182
|
+
chart._colorMin < chart._colorMax
|
|
183
|
+
) {
|
|
184
|
+
renderLegend(
|
|
185
|
+
canvas,
|
|
186
|
+
syntheticLegendLayout(cssWidth, cssHeight),
|
|
187
|
+
{
|
|
188
|
+
min: chart._colorMin,
|
|
189
|
+
max: chart._colorMax,
|
|
190
|
+
label: chart._colorName,
|
|
191
|
+
},
|
|
192
|
+
stops,
|
|
193
|
+
theme,
|
|
194
|
+
chart.getColumnFormatter(chart._colorName, "value"),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function syntheticLegendLayout(
|
|
200
|
+
cssWidth: number,
|
|
201
|
+
cssHeight: number,
|
|
202
|
+
): PlotLayout {
|
|
203
|
+
return new PlotLayout(cssWidth, cssHeight, {
|
|
204
|
+
hasXLabel: false,
|
|
205
|
+
hasYLabel: false,
|
|
206
|
+
hasLegend: true,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
2
|
+
// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
|
|
3
|
+
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
|
|
4
|
+
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
|
|
5
|
+
// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
|
|
6
|
+
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
|
|
7
|
+
// ┃ Copyright (c) 2017, the Perspective Authors. ┃
|
|
8
|
+
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
|
|
9
|
+
// ┃ This file is part of the Perspective library, distributed under the terms ┃
|
|
10
|
+
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
|
|
11
|
+
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
|
12
|
+
|
|
13
|
+
import type { TreeChartBase } from "./tree-chart";
|
|
14
|
+
import { NULL_NODE, ancestorNames } from "./node-store";
|
|
15
|
+
import { rebuildBreadcrumbs } from "./tree-data";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Common subset of `TreemapChart` / `SunburstChart` reached by the
|
|
19
|
+
* shared interaction helpers — anything that lives on `TreeChartBase`
|
|
20
|
+
* plus the pinned/hover/facet-drill state the two charts both declare
|
|
21
|
+
* with identical shape but on the subclass (so we type it as an
|
|
22
|
+
* intersection).
|
|
23
|
+
*/
|
|
24
|
+
export type TreeInteractChart = TreeChartBase & {
|
|
25
|
+
_pinnedNodeId: number;
|
|
26
|
+
_hoveredNodeId: number;
|
|
27
|
+
_facetDrillRoots: Map<string, number>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Emit `perspective-click` + `perspective-global-filter selected:true`
|
|
32
|
+
* for a treemap/sunburst node. The path is walked via `ancestorNames`
|
|
33
|
+
* and split into split-by prefix + group-by levels using
|
|
34
|
+
* `_splitBy.length` as the boundary; faceted mode keeps the depth-0
|
|
35
|
+
* ancestor as the split prefix.
|
|
36
|
+
*/
|
|
37
|
+
export async function emitTreeNodeEvent(
|
|
38
|
+
chart: TreeInteractChart,
|
|
39
|
+
nodeId: number,
|
|
40
|
+
kind: "leaf" | "branch",
|
|
41
|
+
): Promise<void> {
|
|
42
|
+
const store = chart._nodeStore;
|
|
43
|
+
const path = ancestorNames(store, nodeId);
|
|
44
|
+
const isFaceted =
|
|
45
|
+
chart._splitBy.length > 0 && chart._facetConfig.facet_mode === "grid";
|
|
46
|
+
const splitByValues: (string | null)[] = isFaceted
|
|
47
|
+
? path.slice(0, chart._splitBy.length)
|
|
48
|
+
: [];
|
|
49
|
+
const groupByValues: (string | null)[] = isFaceted
|
|
50
|
+
? path.slice(
|
|
51
|
+
chart._splitBy.length,
|
|
52
|
+
chart._splitBy.length + chart._groupBy.length,
|
|
53
|
+
)
|
|
54
|
+
: path.slice(0, chart._groupBy.length);
|
|
55
|
+
|
|
56
|
+
const rowIdx = kind === "leaf" ? (store.leafRowIdx[nodeId] ?? null) : null;
|
|
57
|
+
|
|
58
|
+
await chart.emitClickAndSelect({
|
|
59
|
+
rowIdx: rowIdx != null && rowIdx >= 0 ? rowIdx : null,
|
|
60
|
+
columnName: chart._sizeName,
|
|
61
|
+
groupByValues,
|
|
62
|
+
splitByValues,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build tooltip lines for `nodeId`: ancestor name path + aggregate
|
|
68
|
+
* value + (numeric) color value + per-row tooltip columns from
|
|
69
|
+
* `_lazyRows` for leaves. The leaf branch awaits the source-view row
|
|
70
|
+
* fetch; branch nodes have no underlying row so they emit a Children
|
|
71
|
+
* count instead.
|
|
72
|
+
*/
|
|
73
|
+
export async function buildTreeTooltipLines(
|
|
74
|
+
chart: TreeInteractChart,
|
|
75
|
+
nodeId: number,
|
|
76
|
+
): Promise<string[]> {
|
|
77
|
+
const store = chart._nodeStore;
|
|
78
|
+
const lines: string[] = [];
|
|
79
|
+
|
|
80
|
+
const pathNames: string[] = [];
|
|
81
|
+
let p = nodeId;
|
|
82
|
+
while (store.parent[p] !== NULL_NODE) {
|
|
83
|
+
pathNames.push(store.name[p]);
|
|
84
|
+
p = store.parent[p];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pathNames.reverse();
|
|
88
|
+
if (pathNames.length > 0) {
|
|
89
|
+
lines.push(pathNames.join(" › "));
|
|
90
|
+
} else {
|
|
91
|
+
lines.push(store.name[nodeId]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const sizeFmt = chart.getColumnFormatter(chart._sizeName, "value");
|
|
95
|
+
lines.push(`Value: ${sizeFmt(store.value[nodeId])}`);
|
|
96
|
+
|
|
97
|
+
if (chart._colorName && !isNaN(store.colorValue[nodeId])) {
|
|
98
|
+
const colorFmt = chart.getColumnFormatter(chart._colorName, "value");
|
|
99
|
+
lines.push(
|
|
100
|
+
`${chart._colorName}: ${colorFmt(store.colorValue[nodeId])}`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const rowIdx = store.leafRowIdx[nodeId];
|
|
105
|
+
const isLeaf =
|
|
106
|
+
store.firstChild[nodeId] === NULL_NODE && rowIdx !== NULL_NODE;
|
|
107
|
+
|
|
108
|
+
if (isLeaf && chart._lazyRows) {
|
|
109
|
+
const row = await chart._lazyRows.fetchRow(rowIdx);
|
|
110
|
+
for (const [name, value] of row) {
|
|
111
|
+
if (value === null || value === undefined) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (name === chart._colorName && !isNaN(store.colorValue[nodeId])) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (typeof value === "number") {
|
|
120
|
+
lines.push(
|
|
121
|
+
`${name}: ${chart.getColumnFormatter(name, "value")(value)}`,
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
lines.push(`${name}: ${value}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (store.firstChild[nodeId] !== NULL_NODE) {
|
|
130
|
+
lines.push(`Children: ${store.childCount[nodeId]}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return lines;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Pin a tooltip at the chart-supplied anchor. Lines are fetched lazily;
|
|
138
|
+
* the `_pinnedNodeId` check on resolve discards stale results from a
|
|
139
|
+
* prior pin or dismissal.
|
|
140
|
+
*/
|
|
141
|
+
export function showTreePinnedTooltip(
|
|
142
|
+
chart: TreeInteractChart,
|
|
143
|
+
nodeId: number,
|
|
144
|
+
anchor: { cx: number; cy: number },
|
|
145
|
+
renderChromeOverlay: () => void,
|
|
146
|
+
): void {
|
|
147
|
+
chart._tooltip.dismiss();
|
|
148
|
+
chart._pinnedNodeId = nodeId;
|
|
149
|
+
|
|
150
|
+
const cssWidth = chart._glManager?.cssWidth ?? 0;
|
|
151
|
+
const cssHeight = chart._glManager?.cssHeight ?? 0;
|
|
152
|
+
|
|
153
|
+
buildTreeTooltipLines(chart, nodeId).then((lines) => {
|
|
154
|
+
if (chart._pinnedNodeId !== nodeId) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (lines.length === 0) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
chart._tooltip.pin(
|
|
163
|
+
lines,
|
|
164
|
+
{ px: anchor.cx, py: anchor.cy },
|
|
165
|
+
{ cssWidth, cssHeight },
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
chart._hoveredNodeId = NULL_NODE;
|
|
170
|
+
renderChromeOverlay();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function dismissTreePinnedTooltip(chart: TreeInteractChart): void {
|
|
174
|
+
chart._tooltip.dismiss();
|
|
175
|
+
chart._pinnedNodeId = NULL_NODE;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Drill the clicked facet (or the whole chart in non-facet mode).
|
|
180
|
+
* Faceted drill walks up to the facet root (top-level child of
|
|
181
|
+
* `_rootId`), records the new drill node under that facet's label, and
|
|
182
|
+
* re-renders.
|
|
183
|
+
*/
|
|
184
|
+
export function treeDrillTo(
|
|
185
|
+
chart: TreeInteractChart,
|
|
186
|
+
nodeId: number,
|
|
187
|
+
renderFrame: () => void,
|
|
188
|
+
): void {
|
|
189
|
+
const store = chart._nodeStore;
|
|
190
|
+
if (chart._splitBy.length > 0 && chart._facetConfig.facet_mode === "grid") {
|
|
191
|
+
let p = nodeId;
|
|
192
|
+
while (p !== NULL_NODE && store.parent[p] !== chart._rootId) {
|
|
193
|
+
p = store.parent[p];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (p !== NULL_NODE) {
|
|
197
|
+
chart._facetDrillRoots.set(store.name[p], nodeId);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
chart._hoveredNodeId = NULL_NODE;
|
|
201
|
+
renderFrame();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
chart._currentRootId = nodeId;
|
|
206
|
+
rebuildBreadcrumbs(chart, nodeId);
|
|
207
|
+
chart._hoveredNodeId = NULL_NODE;
|
|
208
|
+
renderFrame();
|
|
209
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
withScissor,
|
|
21
21
|
} from "../../webgl/plot-frame";
|
|
22
22
|
import { getInstancing } from "../../webgl/instanced-attrs";
|
|
23
|
+
import { compileProgram } from "../../webgl/program-cache";
|
|
23
24
|
import { initCanvas } from "../../axis/canvas";
|
|
24
25
|
import { buildFacetGrid } from "../../layout/facet-grid";
|
|
25
26
|
import {
|
|
@@ -236,21 +237,18 @@ function ensureProgram(
|
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
const gl = glManager.gl;
|
|
239
|
-
const
|
|
240
|
+
const compiled = compileProgram<
|
|
241
|
+
{ program: WebGLProgram } & NonNullable<HeatmapChart["_locations"]>
|
|
242
|
+
>(
|
|
243
|
+
glManager,
|
|
240
244
|
"heatmap",
|
|
241
245
|
heatmapVert,
|
|
242
246
|
heatmapFrag,
|
|
247
|
+
["u_projection", "u_cell_inset", "u_cell_size", "u_gradient_lut"],
|
|
248
|
+
["a_corner", "a_cell", "a_color_t"],
|
|
243
249
|
);
|
|
244
|
-
chart._program = program;
|
|
245
|
-
chart._locations =
|
|
246
|
-
u_projection: gl.getUniformLocation(program, "u_projection"),
|
|
247
|
-
u_cell_inset: gl.getUniformLocation(program, "u_cell_inset"),
|
|
248
|
-
u_cell_size: gl.getUniformLocation(program, "u_cell_size"),
|
|
249
|
-
u_gradient_lut: gl.getUniformLocation(program, "u_gradient_lut"),
|
|
250
|
-
a_corner: gl.getAttribLocation(program, "a_corner"),
|
|
251
|
-
a_cell: gl.getAttribLocation(program, "a_cell"),
|
|
252
|
-
a_color_t: gl.getAttribLocation(program, "a_color_t"),
|
|
253
|
-
};
|
|
250
|
+
chart._program = compiled.program;
|
|
251
|
+
chart._locations = compiled;
|
|
254
252
|
|
|
255
253
|
const cornerBuffer = gl.createBuffer()!;
|
|
256
254
|
gl.bindBuffer(gl.ARRAY_BUFFER, cornerBuffer);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { WebGLContextManager } from "../../../webgl/context-manager";
|
|
14
14
|
import type { SeriesChart } from "../series";
|
|
15
15
|
import type { SeriesInfo } from "../series-build";
|
|
16
|
+
import type { InterpolateMode } from "../series-type";
|
|
16
17
|
import { compileProgram } from "../../../webgl/program-cache";
|
|
17
18
|
import areaVert from "../../../shaders/area.vert.glsl";
|
|
18
19
|
import areaFrag from "../../../shaders/area.frag.glsl";
|
|
@@ -144,6 +145,7 @@ export class AreaGlyph {
|
|
|
144
145
|
|
|
145
146
|
const entries: AreaSeriesEntry[] = [];
|
|
146
147
|
for (const s of areaSeries) {
|
|
148
|
+
const seriesInfo = chart._series[s.seriesId];
|
|
147
149
|
const strips = collectAreaStrips(
|
|
148
150
|
s,
|
|
149
151
|
N,
|
|
@@ -155,6 +157,9 @@ export class AreaGlyph {
|
|
|
155
157
|
bars.y1,
|
|
156
158
|
positions,
|
|
157
159
|
xOrigin,
|
|
160
|
+
seriesInfo.start,
|
|
161
|
+
seriesInfo.end,
|
|
162
|
+
seriesInfo.interpolateMode,
|
|
158
163
|
);
|
|
159
164
|
if (strips.totalVertices === 0) {
|
|
160
165
|
continue;
|
|
@@ -254,6 +259,21 @@ interface CollectedStrips {
|
|
|
254
259
|
* Reads stacked y0/y1 from the pre-built `barIndex` (cached on the
|
|
255
260
|
* chart at data load) so this hot path doesn't rebuild the map each
|
|
256
261
|
* call.
|
|
262
|
+
*
|
|
263
|
+
* The "present" predicate is mode-aware for the unstacked branch:
|
|
264
|
+
*
|
|
265
|
+
* - `mode = "solid"` (also coerced from `"transparent"` for area):
|
|
266
|
+
* Pass 2 has populated every cell in `[start, end]`, including
|
|
267
|
+
* leading/trailing zero-fills. Treat `c in [start, end]` as
|
|
268
|
+
* present and ignore `sampleValid` (the bit is still 0 at
|
|
269
|
+
* synthesized cells).
|
|
270
|
+
* - `mode = "skip"`: Pass 2 didn't run for this series. Interior
|
|
271
|
+
* nulls inside `[start, end]` remain and the strip must break at
|
|
272
|
+
* them — use `sampleValid` as the "is present" check, matching
|
|
273
|
+
* the pre-feature behavior.
|
|
274
|
+
*
|
|
275
|
+
* The stacked branch is unchanged: the `barIndex` lookup already
|
|
276
|
+
* encodes presence post-stacking.
|
|
257
277
|
*/
|
|
258
278
|
function collectAreaStrips(
|
|
259
279
|
s: SeriesInfo,
|
|
@@ -266,10 +286,14 @@ function collectAreaStrips(
|
|
|
266
286
|
barY1: Float64Array,
|
|
267
287
|
positions: Float64Array | null,
|
|
268
288
|
xOrigin: number,
|
|
289
|
+
seriesStart: number,
|
|
290
|
+
seriesEnd: number,
|
|
291
|
+
interpolateMode: InterpolateMode,
|
|
269
292
|
): CollectedStrips {
|
|
270
293
|
const scratch = ensureStripScratch(N * 4);
|
|
271
294
|
const descriptors: AreaStrip[] = [];
|
|
272
295
|
const seriesBase = s.seriesId * 1_000_000_000;
|
|
296
|
+
const trustRange = interpolateMode !== "skip";
|
|
273
297
|
|
|
274
298
|
let write = 0;
|
|
275
299
|
let runStart = 0;
|
|
@@ -288,7 +312,12 @@ function collectAreaStrips(
|
|
|
288
312
|
}
|
|
289
313
|
} else {
|
|
290
314
|
const idx = c * S + s.seriesId;
|
|
291
|
-
if (
|
|
315
|
+
if (trustRange) {
|
|
316
|
+
if (c >= seriesStart && c <= seriesEnd) {
|
|
317
|
+
top = samples[idx];
|
|
318
|
+
present = true;
|
|
319
|
+
}
|
|
320
|
+
} else if ((valid[idx >> 3] >> (idx & 7)) & 1) {
|
|
292
321
|
top = samples[idx];
|
|
293
322
|
present = true;
|
|
294
323
|
}
|