@perspective-dev/viewer-charts 4.5.0 → 4.5.1
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/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 +0 -1
- package/dist/esm/theme/palette.d.ts +0 -5
- package/dist/esm/transport/protocol.d.ts +2 -7
- 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/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 +100 -184
- 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/worker/dispatch.ts +0 -1
- package/src/ts/worker/renderer.worker.ts +10 -46
|
@@ -15,6 +15,7 @@ import type { CartesianChart } from "../cartesian";
|
|
|
15
15
|
import type { Glyph } from "../glyph";
|
|
16
16
|
import { bindGradientTexture } from "../../../webgl/gradient-texture";
|
|
17
17
|
import { getInstancing } from "../../../webgl/instanced-attrs";
|
|
18
|
+
import { compileProgram } from "../../../webgl/program-cache";
|
|
18
19
|
import { buildPointRowTooltipLines } from "../tooltip-lines";
|
|
19
20
|
import splatVert from "../../../shaders/density-splat.vert.glsl";
|
|
20
21
|
import splatFrag from "../../../shaders/density-splat.frag.glsl";
|
|
@@ -183,16 +184,6 @@ export class DensityGlyph implements Glyph {
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
const gl = glManager.gl;
|
|
186
|
-
const splatProgram = glManager.shaders.getOrCreate(
|
|
187
|
-
"density-splat",
|
|
188
|
-
splatVert,
|
|
189
|
-
splatFrag,
|
|
190
|
-
);
|
|
191
|
-
const resolveProgram = glManager.shaders.getOrCreate(
|
|
192
|
-
"density-resolve",
|
|
193
|
-
resolveVert,
|
|
194
|
-
resolveFrag,
|
|
195
|
-
);
|
|
196
187
|
|
|
197
188
|
const quadCornerBuffer = gl.createBuffer()!;
|
|
198
189
|
gl.bindBuffer(gl.ARRAY_BUFFER, quadCornerBuffer);
|
|
@@ -217,24 +208,28 @@ export class DensityGlyph implements Glyph {
|
|
|
217
208
|
const heatFramebuffer = gl.createFramebuffer()!;
|
|
218
209
|
|
|
219
210
|
this._cache = {
|
|
220
|
-
splat:
|
|
211
|
+
splat: compileSplatProgram(
|
|
212
|
+
glManager,
|
|
213
|
+
"density-splat",
|
|
214
|
+
splatVert,
|
|
215
|
+
splatFrag,
|
|
216
|
+
),
|
|
221
217
|
extremeSplat: null,
|
|
222
218
|
mrtSplat: null,
|
|
223
|
-
resolve:
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
219
|
+
resolve: compileProgram<DensityCache["resolve"]>(
|
|
220
|
+
glManager,
|
|
221
|
+
"density-resolve",
|
|
222
|
+
resolveVert,
|
|
223
|
+
resolveFrag,
|
|
224
|
+
[
|
|
225
|
+
"u_heat",
|
|
226
|
+
"u_extreme",
|
|
229
227
|
"u_gradient_lut",
|
|
230
|
-
|
|
231
|
-
u_heat_max: gl.getUniformLocation(resolveProgram, "u_heat_max"),
|
|
232
|
-
u_color_mode: gl.getUniformLocation(
|
|
233
|
-
resolveProgram,
|
|
228
|
+
"u_heat_max",
|
|
234
229
|
"u_color_mode",
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
230
|
+
],
|
|
231
|
+
["a_corner"],
|
|
232
|
+
),
|
|
238
233
|
quadCornerBuffer,
|
|
239
234
|
tripleCornerBuffer,
|
|
240
235
|
heatTexture,
|
|
@@ -541,12 +536,12 @@ export class DensityGlyph implements Glyph {
|
|
|
541
536
|
return cache.extremeSplat;
|
|
542
537
|
}
|
|
543
538
|
|
|
544
|
-
|
|
539
|
+
cache.extremeSplat = compileSplatProgram(
|
|
540
|
+
glManager,
|
|
545
541
|
"density-extreme",
|
|
546
542
|
splatVert,
|
|
547
543
|
extremeFrag,
|
|
548
544
|
);
|
|
549
|
-
cache.extremeSplat = extractSplatLocations(glManager.gl, program);
|
|
550
545
|
return cache.extremeSplat;
|
|
551
546
|
}
|
|
552
547
|
|
|
@@ -568,12 +563,12 @@ export class DensityGlyph implements Glyph {
|
|
|
568
563
|
// the legacy GLSL 100 splat vert can't link against it because
|
|
569
564
|
// a program's shaders must share a version. Use the paired
|
|
570
565
|
// `density-mrt.vert.glsl` instead — same math, 300 ES dialect.
|
|
571
|
-
|
|
566
|
+
cache.mrtSplat = compileSplatProgram(
|
|
567
|
+
glManager,
|
|
572
568
|
"density-mrt",
|
|
573
569
|
mrtVert,
|
|
574
570
|
mrtFrag,
|
|
575
571
|
);
|
|
576
|
-
cache.mrtSplat = extractSplatLocations(glManager.gl, program);
|
|
577
572
|
return cache.mrtSplat;
|
|
578
573
|
}
|
|
579
574
|
|
|
@@ -1088,20 +1083,20 @@ function createAccumTexture(
|
|
|
1088
1083
|
return tex;
|
|
1089
1084
|
}
|
|
1090
1085
|
|
|
1091
|
-
function
|
|
1092
|
-
|
|
1093
|
-
|
|
1086
|
+
function compileSplatProgram(
|
|
1087
|
+
glManager: WebGLContextManager,
|
|
1088
|
+
key: string,
|
|
1089
|
+
vert: string,
|
|
1090
|
+
frag: string,
|
|
1094
1091
|
): SplatProgramCache {
|
|
1095
|
-
return
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
a_corner
|
|
1102
|
-
|
|
1103
|
-
a_color_value: gl.getAttribLocation(program, "a_color_value"),
|
|
1104
|
-
};
|
|
1092
|
+
return compileProgram<SplatProgramCache>(
|
|
1093
|
+
glManager,
|
|
1094
|
+
key,
|
|
1095
|
+
vert,
|
|
1096
|
+
frag,
|
|
1097
|
+
["u_projection", "u_radius_ndc", "u_intensity", "u_color_range"],
|
|
1098
|
+
["a_corner", "a_position", "a_color_value"],
|
|
1099
|
+
);
|
|
1105
1100
|
}
|
|
1106
1101
|
|
|
1107
1102
|
/**
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
createLineCornerBuffer,
|
|
19
19
|
getInstancing,
|
|
20
20
|
} from "../../../webgl/instanced-attrs";
|
|
21
|
+
import { compileProgram } from "../../../webgl/program-cache";
|
|
21
22
|
import { formatTickValue, formatDateTickValue } from "../../../layout/ticks";
|
|
22
23
|
import lineVert from "../../../shaders/line.vert.glsl";
|
|
23
24
|
import lineFrag from "../../../shaders/line.frag.glsl";
|
|
@@ -56,26 +57,23 @@ export class LineGlyph implements Glyph {
|
|
|
56
57
|
return;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
const
|
|
60
|
-
|
|
60
|
+
const partial = compileProgram<Omit<LineCache, "cornerBuffer">>(
|
|
61
|
+
glManager,
|
|
61
62
|
"line",
|
|
62
63
|
lineVert,
|
|
63
64
|
lineFrag,
|
|
65
|
+
[
|
|
66
|
+
"u_projection",
|
|
67
|
+
"u_resolution",
|
|
68
|
+
"u_line_width",
|
|
69
|
+
"u_color_range",
|
|
70
|
+
"u_gradient_lut",
|
|
71
|
+
],
|
|
72
|
+
["a_start", "a_end", "a_color_start", "a_color_end", "a_corner"],
|
|
64
73
|
);
|
|
65
|
-
const cornerBuffer = createLineCornerBuffer(gl);
|
|
66
74
|
this._cache = {
|
|
67
|
-
|
|
68
|
-
cornerBuffer,
|
|
69
|
-
u_projection: gl.getUniformLocation(program, "u_projection"),
|
|
70
|
-
u_resolution: gl.getUniformLocation(program, "u_resolution"),
|
|
71
|
-
u_line_width: gl.getUniformLocation(program, "u_line_width"),
|
|
72
|
-
u_color_range: gl.getUniformLocation(program, "u_color_range"),
|
|
73
|
-
u_gradient_lut: gl.getUniformLocation(program, "u_gradient_lut"),
|
|
74
|
-
a_start: gl.getAttribLocation(program, "a_start"),
|
|
75
|
-
a_end: gl.getAttribLocation(program, "a_end"),
|
|
76
|
-
a_color_start: gl.getAttribLocation(program, "a_color_start"),
|
|
77
|
-
a_color_end: gl.getAttribLocation(program, "a_color_end"),
|
|
78
|
-
a_corner: gl.getAttribLocation(program, "a_corner"),
|
|
75
|
+
...partial,
|
|
76
|
+
cornerBuffer: createLineCornerBuffer(glManager.gl),
|
|
79
77
|
};
|
|
80
78
|
}
|
|
81
79
|
|
|
@@ -14,6 +14,7 @@ import type { WebGLContextManager } from "../../../webgl/context-manager";
|
|
|
14
14
|
import type { CartesianChart } from "../cartesian";
|
|
15
15
|
import type { Glyph } from "../glyph";
|
|
16
16
|
import { bindGradientTexture } from "../../../webgl/gradient-texture";
|
|
17
|
+
import { compileProgram } from "../../../webgl/program-cache";
|
|
17
18
|
import { buildPointRowTooltipLines } from "../tooltip-lines";
|
|
18
19
|
import scatterVert from "../../../shaders/scatter.vert.glsl";
|
|
19
20
|
import scatterFrag from "../../../shaders/scatter.frag.glsl";
|
|
@@ -53,27 +54,21 @@ export class PointGlyph implements Glyph {
|
|
|
53
54
|
return;
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
this._cache = compileProgram<PointCache>(
|
|
58
|
+
glManager,
|
|
58
59
|
"scatter",
|
|
59
60
|
scatterVert,
|
|
60
61
|
scatterFrag,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
u_gradient_lut: gl.getUniformLocation(program, "u_gradient_lut"),
|
|
68
|
-
u_size_range: gl.getUniformLocation(program, "u_size_range"),
|
|
69
|
-
u_point_size_range: gl.getUniformLocation(
|
|
70
|
-
program,
|
|
62
|
+
[
|
|
63
|
+
"u_projection",
|
|
64
|
+
"u_point_size",
|
|
65
|
+
"u_color_range",
|
|
66
|
+
"u_gradient_lut",
|
|
67
|
+
"u_size_range",
|
|
71
68
|
"u_point_size_range",
|
|
72
|
-
|
|
73
|
-
a_position
|
|
74
|
-
|
|
75
|
-
a_size_value: gl.getAttribLocation(program, "a_size_value"),
|
|
76
|
-
};
|
|
69
|
+
],
|
|
70
|
+
["a_position", "a_color_value", "a_size_value"],
|
|
71
|
+
);
|
|
77
72
|
}
|
|
78
73
|
|
|
79
74
|
draw(
|
|
@@ -46,6 +46,14 @@ import type { ViewConfig } from "@perspective-dev/client";
|
|
|
46
46
|
import { resolveThemeFromVars, type Theme } from "../theme/theme";
|
|
47
47
|
import { requestRender as scheduleRender } from "../render/scheduler";
|
|
48
48
|
|
|
49
|
+
// TODO I don't know if this is the behavior we want. On the plus side, this
|
|
50
|
+
// ad-hoc formatter scales well to small and large data ranges, making a good
|
|
51
|
+
// guess at the right format without user input. On the minus side, this
|
|
52
|
+
// behavior is inconsistent with datagrid and the rest of the app, and the ad-hoc
|
|
53
|
+
// surprising behavior when overriding one field in `number_format` and suddenly
|
|
54
|
+
// the entire formatter is replaced.
|
|
55
|
+
const REGRESSION_BEHAVIOR = true;
|
|
56
|
+
|
|
49
57
|
/**
|
|
50
58
|
* Locale-aware fallback formatter applied to numeric tooltip / legend
|
|
51
59
|
* values when the column has no `number_format` configured. Two
|
|
@@ -55,9 +63,12 @@ import { requestRender as scheduleRender } from "../render/scheduler";
|
|
|
55
63
|
const DEFAULT_VALUE_FORMATTER: (v: number) => string = ((): ((
|
|
56
64
|
v: number,
|
|
57
65
|
) => string) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
if (REGRESSION_BEHAVIOR) {
|
|
67
|
+
return formatTickValue;
|
|
68
|
+
} else {
|
|
69
|
+
const intl = createNumberFormatter("float");
|
|
70
|
+
return (v) => intl.format(v);
|
|
71
|
+
}
|
|
61
72
|
})();
|
|
62
73
|
|
|
63
74
|
/**
|
|
@@ -69,9 +80,12 @@ const DEFAULT_VALUE_FORMATTER: (v: number) => string = ((): ((
|
|
|
69
80
|
const DEFAULT_DATETIME_FORMATTER: (v: number) => string = ((): ((
|
|
70
81
|
v: number,
|
|
71
82
|
) => string) => {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
83
|
+
if (REGRESSION_BEHAVIOR) {
|
|
84
|
+
return formatDateTickValue;
|
|
85
|
+
} else {
|
|
86
|
+
const intl = createDatetimeFormatter();
|
|
87
|
+
return (v) => intl.format(v);
|
|
88
|
+
}
|
|
75
89
|
})();
|
|
76
90
|
|
|
77
91
|
/**
|
package/src/ts/charts/chart.ts
CHANGED
|
@@ -411,7 +411,7 @@ export const DEFAULT_PLUGIN_CONFIG: PluginConfig = {
|
|
|
411
411
|
facet_zoom_mode: "shared",
|
|
412
412
|
series_zoom_mode: "dynamic",
|
|
413
413
|
include_zero: false,
|
|
414
|
-
domain_mode: "
|
|
414
|
+
domain_mode: "expand",
|
|
415
415
|
line_width_px: 2.0,
|
|
416
416
|
point_size_px: 8.0,
|
|
417
417
|
band_inner_frac: 0.5,
|
|
@@ -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
|
+
}
|