@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
|
@@ -34,7 +34,6 @@ import { renderCanvasTooltip } from "../../interaction/tooltip-controller";
|
|
|
34
34
|
import {
|
|
35
35
|
computeTicks,
|
|
36
36
|
renderGridlines,
|
|
37
|
-
renderAxesChrome,
|
|
38
37
|
renderCellXAxis,
|
|
39
38
|
renderCellYAxis,
|
|
40
39
|
renderOuterXAxis,
|
|
@@ -42,6 +41,14 @@ import {
|
|
|
42
41
|
type AxisDomain,
|
|
43
42
|
} from "../../axis/numeric-axis";
|
|
44
43
|
import { initCanvas, getScaledContext } from "../../axis/canvas";
|
|
44
|
+
import {
|
|
45
|
+
type CategoricalDomain,
|
|
46
|
+
type CategoricalLevel,
|
|
47
|
+
measureCategoricalAxisHeight,
|
|
48
|
+
measureCategoricalAxisWidth,
|
|
49
|
+
renderCategoricalXTicks,
|
|
50
|
+
renderCategoricalYTicks,
|
|
51
|
+
} from "../../axis/categorical-axis";
|
|
45
52
|
import {
|
|
46
53
|
renderLegend,
|
|
47
54
|
renderLegendAt,
|
|
@@ -240,6 +247,88 @@ function buildYDomain(
|
|
|
240
247
|
};
|
|
241
248
|
}
|
|
242
249
|
|
|
250
|
+
/**
|
|
251
|
+
* Wrap a value-axis dictionary into the single-level `CategoricalDomain`
|
|
252
|
+
* the categorical axis painter expects. Caches by reference identity on
|
|
253
|
+
* the chart so chrome-overlay redraws on hover don't rebuild the domain.
|
|
254
|
+
*/
|
|
255
|
+
function buildCategoricalDomainFromDict(
|
|
256
|
+
dictionary: string[],
|
|
257
|
+
label: string,
|
|
258
|
+
): CategoricalDomain {
|
|
259
|
+
let maxLabelChars = 0;
|
|
260
|
+
for (const s of dictionary) {
|
|
261
|
+
if (s.length > maxLabelChars) {
|
|
262
|
+
maxLabelChars = s.length;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const level: CategoricalLevel = {
|
|
267
|
+
labels: dictionary.slice(),
|
|
268
|
+
runs: [],
|
|
269
|
+
maxLabelChars,
|
|
270
|
+
};
|
|
271
|
+
return {
|
|
272
|
+
levels: [level],
|
|
273
|
+
numRows: dictionary.length,
|
|
274
|
+
levelLabels: [label],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Dispatch each axis side to the categorical painter when its source
|
|
280
|
+
* column is post-aggregation `string`-typed, or to the numeric painter
|
|
281
|
+
* otherwise. Used by both single-plot and faceted (per-cell) chrome
|
|
282
|
+
* overlays.
|
|
283
|
+
*/
|
|
284
|
+
function renderCartesianCellAxes(
|
|
285
|
+
chart: CartesianChart,
|
|
286
|
+
canvas: Canvas2D,
|
|
287
|
+
layout: PlotLayout,
|
|
288
|
+
xDomain: AxisDomain,
|
|
289
|
+
yDomain: AxisDomain,
|
|
290
|
+
xTicks: number[],
|
|
291
|
+
yTicks: number[],
|
|
292
|
+
theme: Theme,
|
|
293
|
+
dpr: number,
|
|
294
|
+
): void {
|
|
295
|
+
if (chart._xIsString && chart._xCategoryDomain) {
|
|
296
|
+
const ctx = getScaledContext(canvas, dpr);
|
|
297
|
+
if (ctx) {
|
|
298
|
+
renderCategoricalXTicks(ctx, layout, chart._xCategoryDomain, theme);
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
renderCellXAxis(
|
|
302
|
+
canvas,
|
|
303
|
+
xDomain,
|
|
304
|
+
layout,
|
|
305
|
+
xTicks,
|
|
306
|
+
theme,
|
|
307
|
+
true,
|
|
308
|
+
dpr,
|
|
309
|
+
chart.getColumnFormatter(chart._xName, "tick"),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (chart._yIsString && chart._yCategoryDomain) {
|
|
314
|
+
const ctx = getScaledContext(canvas, dpr);
|
|
315
|
+
if (ctx) {
|
|
316
|
+
renderCategoricalYTicks(ctx, layout, chart._yCategoryDomain, theme);
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
renderCellYAxis(
|
|
320
|
+
canvas,
|
|
321
|
+
yDomain,
|
|
322
|
+
layout,
|
|
323
|
+
yTicks,
|
|
324
|
+
theme,
|
|
325
|
+
true,
|
|
326
|
+
dpr,
|
|
327
|
+
chart.getColumnFormatter(chart._yName, "tick"),
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
243
332
|
/**
|
|
244
333
|
* Original single-plot render path — all series drawn into one
|
|
245
334
|
* `PlotLayout` with one projection matrix. Used when splits are absent
|
|
@@ -255,10 +344,42 @@ function renderSinglePlotFrame(
|
|
|
255
344
|
const gl = glManager.gl;
|
|
256
345
|
const { cssWidth, cssHeight, xIsDate, yIsDate, hasColorCol } = ctx;
|
|
257
346
|
|
|
347
|
+
// Materialize per-axis categorical domains from the build-pass
|
|
348
|
+
// dictionaries before measuring gutters — the leaf-rotation budget
|
|
349
|
+
// in `measureCategoricalAxisHeight` depends on `maxLabelChars`.
|
|
350
|
+
chart._xCategoryDomain =
|
|
351
|
+
chart._xIsString && chart._xCategoryDictionary.length > 0
|
|
352
|
+
? buildCategoricalDomainFromDict(
|
|
353
|
+
chart._xCategoryDictionary,
|
|
354
|
+
chart._xLabel || chart._xName || "",
|
|
355
|
+
)
|
|
356
|
+
: null;
|
|
357
|
+
chart._yCategoryDomain =
|
|
358
|
+
chart._yIsString && chart._yCategoryDictionary.length > 0
|
|
359
|
+
? buildCategoricalDomainFromDict(
|
|
360
|
+
chart._yCategoryDictionary,
|
|
361
|
+
chart._yLabel || chart._yName || "",
|
|
362
|
+
)
|
|
363
|
+
: null;
|
|
364
|
+
|
|
365
|
+
// One-pass plot-width / plot-height estimate to size the
|
|
366
|
+
// categorical gutter overrides; same approach as series-render.
|
|
367
|
+
const estRight = hasColorCol ? 80 : 16;
|
|
368
|
+
const estLeftPlain = 55 + (chart._yLabel ? 16 : 0);
|
|
369
|
+
const estPlotWidth = Math.max(1, cssWidth - estLeftPlain - estRight);
|
|
370
|
+
const leftExtra = chart._yCategoryDomain
|
|
371
|
+
? measureCategoricalAxisWidth(chart._yCategoryDomain)
|
|
372
|
+
: undefined;
|
|
373
|
+
const bottomExtra = chart._xCategoryDomain
|
|
374
|
+
? measureCategoricalAxisHeight(chart._xCategoryDomain, estPlotWidth)
|
|
375
|
+
: undefined;
|
|
376
|
+
|
|
258
377
|
const layout = new PlotLayout(cssWidth, cssHeight, {
|
|
259
378
|
hasXLabel: !!chart._xLabel,
|
|
260
379
|
hasYLabel: !!chart._yLabel,
|
|
261
380
|
hasLegend: hasColorCol,
|
|
381
|
+
leftExtra,
|
|
382
|
+
bottomExtra,
|
|
262
383
|
});
|
|
263
384
|
chart._lastLayout = layout;
|
|
264
385
|
if (chart._zoomController) {
|
|
@@ -279,7 +400,9 @@ function renderSinglePlotFrame(
|
|
|
279
400
|
|
|
280
401
|
const xDomain = buildXDomain(chart, domain.xMin, domain.xMax, xIsDate);
|
|
281
402
|
const yDomain = buildYDomain(chart, domain.yMin, domain.yMax, yIsDate);
|
|
282
|
-
const
|
|
403
|
+
const numericTicks = computeTicks(xDomain, yDomain, layout);
|
|
404
|
+
const xTicks = chart._xIsString ? [] : numericTicks.xTicks;
|
|
405
|
+
const yTicks = chart._yIsString ? [] : numericTicks.yTicks;
|
|
283
406
|
|
|
284
407
|
const isMap = chart._renderMode === "map";
|
|
285
408
|
|
|
@@ -349,6 +472,26 @@ function renderFacetedFrame(
|
|
|
349
472
|
const gl = glManager.gl;
|
|
350
473
|
const { cssWidth, cssHeight, xIsDate, yIsDate } = ctx;
|
|
351
474
|
|
|
475
|
+
// Materialize per-axis categorical domains (shared across facets:
|
|
476
|
+
// the build dictionary is global, so slot N refers to the same
|
|
477
|
+
// string in every cell). Faceted layout still uses the default
|
|
478
|
+
// per-cell gutters from `buildFacetGrid` — rotated leaf labels in
|
|
479
|
+
// tight cells may overflow but won't crash.
|
|
480
|
+
chart._xCategoryDomain =
|
|
481
|
+
chart._xIsString && chart._xCategoryDictionary.length > 0
|
|
482
|
+
? buildCategoricalDomainFromDict(
|
|
483
|
+
chart._xCategoryDictionary,
|
|
484
|
+
chart._xLabel || chart._xName || "",
|
|
485
|
+
)
|
|
486
|
+
: null;
|
|
487
|
+
chart._yCategoryDomain =
|
|
488
|
+
chart._yIsString && chart._yCategoryDictionary.length > 0
|
|
489
|
+
? buildCategoricalDomainFromDict(
|
|
490
|
+
chart._yCategoryDictionary,
|
|
491
|
+
chart._yLabel || chart._yName || "",
|
|
492
|
+
)
|
|
493
|
+
: null;
|
|
494
|
+
|
|
352
495
|
const labels = chart._splitGroups.map((g) => g.prefix);
|
|
353
496
|
|
|
354
497
|
// Legend: reserve space only when the user wired a color column.
|
|
@@ -415,11 +558,15 @@ function renderFacetedFrame(
|
|
|
415
558
|
|
|
416
559
|
// Gridlines + per-facet axes use the first cell's layout for tick
|
|
417
560
|
// sampling (all cells have identical plotRect dimensions). Per-facet
|
|
418
|
-
// rendering then reuses the same tick arrays.
|
|
561
|
+
// rendering then reuses the same tick arrays. Categorical sides
|
|
562
|
+
// skip numeric tick computation; the categorical painter handles
|
|
563
|
+
// its own label placement off the dictionary.
|
|
419
564
|
const sampleLayout = grid.cells[0]?.layout;
|
|
420
|
-
const
|
|
565
|
+
const numericFacetTicks = sampleLayout
|
|
421
566
|
? computeTicks(xDomain, yDomain, sampleLayout)
|
|
422
567
|
: { xTicks: [], yTicks: [] };
|
|
568
|
+
const xTicks = chart._xIsString ? [] : numericFacetTicks.xTicks;
|
|
569
|
+
const yTicks = chart._yIsString ? [] : numericFacetTicks.yTicks;
|
|
423
570
|
|
|
424
571
|
// One-shot destructive prep for the gridline + WebGL canvases.
|
|
425
572
|
// Both phases below are per-facet; calling their destructive
|
|
@@ -566,17 +713,16 @@ function renderSinglePlotChromeOverlay(chart: CartesianChart): void {
|
|
|
566
713
|
if (isMap) {
|
|
567
714
|
chart.renderMapChrome(chart._chromeCanvas!, layout, theme, dpr);
|
|
568
715
|
} else {
|
|
569
|
-
|
|
716
|
+
renderCartesianCellAxes(
|
|
717
|
+
chart,
|
|
570
718
|
chart._chromeCanvas!,
|
|
719
|
+
layout,
|
|
571
720
|
chart._lastXDomain!,
|
|
572
721
|
chart._lastYDomain!,
|
|
573
|
-
layout,
|
|
574
722
|
chart._lastXTicks!,
|
|
575
723
|
chart._lastYTicks!,
|
|
576
724
|
theme,
|
|
577
725
|
dpr,
|
|
578
|
-
chart.getColumnFormatter(chart._xName, "tick"),
|
|
579
|
-
chart.getColumnFormatter(chart._yName, "tick"),
|
|
580
726
|
);
|
|
581
727
|
}
|
|
582
728
|
|
|
@@ -633,8 +779,13 @@ function renderFacetedChromeOverlay(chart: CartesianChart): void {
|
|
|
633
779
|
// — these already fold in the independent-zoom override (outer
|
|
634
780
|
// axes are incompatible with per-cell viewports), so `sharedX` /
|
|
635
781
|
// `sharedY` true here implies shared-zoom too.
|
|
636
|
-
|
|
637
|
-
|
|
782
|
+
//
|
|
783
|
+
// Categorical axes additionally force per-cell rendering: the
|
|
784
|
+
// outer-axis painter is numeric-only and the shared dictionary
|
|
785
|
+
// already produces the same labels in every cell, so a per-cell
|
|
786
|
+
// categorical axis is equivalent to a shared one visually.
|
|
787
|
+
const sharedX = chart._lastEffectiveSharedX && !chart._xIsString;
|
|
788
|
+
const sharedY = chart._lastEffectiveSharedY && !chart._yIsString;
|
|
638
789
|
const independent = chart._facetConfig.zoom_mode === "independent";
|
|
639
790
|
|
|
640
791
|
// Shared X axis: one outer band across the bottom of the grid,
|
|
@@ -689,29 +840,53 @@ function renderFacetedChromeOverlay(chart: CartesianChart): void {
|
|
|
689
840
|
: { xTicks: sharedXTicks, yTicks: sharedYTicks };
|
|
690
841
|
|
|
691
842
|
if (!isMap && !sharedX) {
|
|
692
|
-
|
|
693
|
-
canvas,
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
843
|
+
if (chart._xIsString && chart._xCategoryDomain) {
|
|
844
|
+
const cellCtx = getScaledContext(canvas, dpr);
|
|
845
|
+
if (cellCtx) {
|
|
846
|
+
renderCategoricalXTicks(
|
|
847
|
+
cellCtx,
|
|
848
|
+
cell.layout,
|
|
849
|
+
chart._xCategoryDomain,
|
|
850
|
+
theme,
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
} else {
|
|
854
|
+
renderCellXAxis(
|
|
855
|
+
canvas,
|
|
856
|
+
localX,
|
|
857
|
+
cell.layout,
|
|
858
|
+
ticks.xTicks,
|
|
859
|
+
theme,
|
|
860
|
+
!!chart._xLabel,
|
|
861
|
+
dpr,
|
|
862
|
+
chart.getColumnFormatter(chart._xName, "tick"),
|
|
863
|
+
);
|
|
864
|
+
}
|
|
702
865
|
}
|
|
703
866
|
|
|
704
867
|
if (!isMap && !sharedY) {
|
|
705
|
-
|
|
706
|
-
canvas,
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
868
|
+
if (chart._yIsString && chart._yCategoryDomain) {
|
|
869
|
+
const cellCtx = getScaledContext(canvas, dpr);
|
|
870
|
+
if (cellCtx) {
|
|
871
|
+
renderCategoricalYTicks(
|
|
872
|
+
cellCtx,
|
|
873
|
+
cell.layout,
|
|
874
|
+
chart._yCategoryDomain,
|
|
875
|
+
theme,
|
|
876
|
+
);
|
|
877
|
+
}
|
|
878
|
+
} else {
|
|
879
|
+
renderCellYAxis(
|
|
880
|
+
canvas,
|
|
881
|
+
localY,
|
|
882
|
+
cell.layout,
|
|
883
|
+
ticks.yTicks,
|
|
884
|
+
theme,
|
|
885
|
+
!!chart._yLabel,
|
|
886
|
+
dpr,
|
|
887
|
+
chart.getColumnFormatter(chart._yName, "tick"),
|
|
888
|
+
);
|
|
889
|
+
}
|
|
715
890
|
}
|
|
716
891
|
|
|
717
892
|
if (cell.titleRect) {
|
|
@@ -16,6 +16,7 @@ import { AbstractChart } from "../chart-base";
|
|
|
16
16
|
import { SpatialHitTester } from "../../interaction/hit-test";
|
|
17
17
|
import { PlotLayout } from "../../layout/plot-layout";
|
|
18
18
|
import { type AxisDomain } from "../../axis/numeric-axis";
|
|
19
|
+
import type { CategoricalDomain } from "../../axis/categorical-axis";
|
|
19
20
|
import type { GradientTextureCache } from "../../webgl/gradient-texture";
|
|
20
21
|
import type { Glyph } from "./glyph";
|
|
21
22
|
import {
|
|
@@ -146,6 +147,32 @@ export class CartesianChart extends AbstractChart {
|
|
|
146
147
|
_sizeName = "";
|
|
147
148
|
_labelName = "";
|
|
148
149
|
_colorIsString = false;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* When the X (or Y) axis source column is post-aggregation
|
|
153
|
+
* `string`-typed, the build pipeline writes per-row dictionary slot
|
|
154
|
+
* indices into `_xData` (or `_yData`) instead of numeric values,
|
|
155
|
+
* and the render pass dispatches `renderCategoricalXTicks` /
|
|
156
|
+
* `renderCategoricalYTicks` instead of the numeric axis painter.
|
|
157
|
+
*
|
|
158
|
+
* The companion `_xCategoryDictionary` / `_xCategorySeen` pair is
|
|
159
|
+
* built lazily during `processCartesianChunk` in first-seen row
|
|
160
|
+
* order; `(null)` is appended on first encounter of an invalid row
|
|
161
|
+
* rather than reserved at slot 0, so charts without missing values
|
|
162
|
+
* don't get a phantom slot.
|
|
163
|
+
*
|
|
164
|
+
* `_xCategoryDomain` is materialized once per frame in
|
|
165
|
+
* `cartesian-render` and held for chrome overlay redraws (same
|
|
166
|
+
* lifecycle as `_lastXDomain` on the numeric path).
|
|
167
|
+
*/
|
|
168
|
+
_xIsString = false;
|
|
169
|
+
_yIsString = false;
|
|
170
|
+
_xCategoryDictionary: string[] = [];
|
|
171
|
+
_yCategoryDictionary: string[] = [];
|
|
172
|
+
_xCategorySeen: Map<string, number> = new Map();
|
|
173
|
+
_yCategorySeen: Map<string, number> = new Map();
|
|
174
|
+
_xCategoryDomain: CategoricalDomain | null = null;
|
|
175
|
+
_yCategoryDomain: CategoricalDomain | null = null;
|
|
149
176
|
_splitGroups: SplitGroup[] = [];
|
|
150
177
|
|
|
151
178
|
// Data extents
|
|
@@ -375,11 +402,23 @@ export class CartesianChart extends AbstractChart {
|
|
|
375
402
|
// (the prior accumulator); the union is in `_xMin` etc., so we
|
|
376
403
|
// copy it back. Idempotent across multi-chunk uploads — every
|
|
377
404
|
// chunk leaves the accumulator equal to the running union.
|
|
405
|
+
//
|
|
406
|
+
// Categorical axes opt out: slot indices are first-seen-order
|
|
407
|
+
// and only meaningful within a single frame's dictionary, so
|
|
408
|
+
// expanding across frames would mix dictionaries and shift
|
|
409
|
+
// every category's slot. Force-fit those axes per frame
|
|
410
|
+
// instead.
|
|
378
411
|
if (this._pluginConfig.domain_mode === "expand") {
|
|
379
|
-
this.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
412
|
+
if (!this._xIsString) {
|
|
413
|
+
this._expandedXMin = this._xMin;
|
|
414
|
+
this._expandedXMax = this._xMax;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!this._yIsString) {
|
|
418
|
+
this._expandedYMin = this._yMin;
|
|
419
|
+
this._expandedYMax = this._yMax;
|
|
420
|
+
}
|
|
421
|
+
|
|
383
422
|
this._expandedColorMin = this._colorMin;
|
|
384
423
|
this._expandedColorMax = this._colorMax;
|
|
385
424
|
this._expandedSizeMin = this._sizeMin;
|
|
@@ -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,
|