@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.
Files changed (84) hide show
  1. package/LICENSE.md +193 -0
  2. package/dist/cdn/perspective-viewer-charts.js +2 -2
  3. package/dist/cdn/perspective-viewer-charts.js.map +3 -3
  4. package/dist/esm/axis/bar-axis.d.ts +9 -1
  5. package/dist/esm/axis/categorical-axis.d.ts +0 -2
  6. package/dist/esm/charts/cartesian/cartesian.d.ts +26 -0
  7. package/dist/esm/charts/common/category-axis-resolver.d.ts +43 -1
  8. package/dist/esm/charts/common/expand-domain.d.ts +20 -0
  9. package/dist/esm/charts/common/tree-chart.d.ts +7 -0
  10. package/dist/esm/charts/common/tree-chrome.d.ts +23 -1
  11. package/dist/esm/charts/common/tree-interact.d.ts +46 -0
  12. package/dist/esm/charts/series/glyphs/draw-lines.d.ts +11 -4
  13. package/dist/esm/charts/series/series-build.d.ts +38 -2
  14. package/dist/esm/charts/series/series-render.d.ts +1 -4
  15. package/dist/esm/charts/series/series-type.d.ts +19 -17
  16. package/dist/esm/charts/series/series.d.ts +16 -0
  17. package/dist/esm/charts/sunburst/sunburst-interact.d.ts +1 -1
  18. package/dist/esm/charts/treemap/treemap-interact.d.ts +1 -6
  19. package/dist/esm/config.d.ts +19 -0
  20. package/dist/esm/interaction/host-sink-message.d.ts +10 -28
  21. package/dist/esm/interaction/raw-event-forwarder.d.ts +6 -7
  22. package/dist/esm/interaction/zoom-controller.d.ts +31 -20
  23. package/dist/esm/interaction/zoom-router.d.ts +3 -26
  24. package/dist/esm/perspective-viewer-charts.js +2 -2
  25. package/dist/esm/perspective-viewer-charts.js.map +3 -3
  26. package/dist/esm/plugin/plugin.d.ts +15 -3
  27. package/dist/esm/render/scheduler.d.ts +23 -0
  28. package/dist/esm/theme/palette.d.ts +0 -5
  29. package/dist/esm/transport/protocol.d.ts +2 -7
  30. package/dist/esm/webgl/context-manager.d.ts +100 -117
  31. package/dist/esm/webgl/context-pool.d.ts +40 -0
  32. package/dist/esm/webgl/gl-context.d.ts +54 -0
  33. package/dist/esm/worker/renderer.worker.d.ts +2 -4
  34. package/package.json +45 -45
  35. package/src/ts/axis/bar-axis.ts +74 -45
  36. package/src/ts/axis/categorical-axis.ts +0 -2
  37. package/src/ts/charts/candlestick/candlestick-render.ts +10 -7
  38. package/src/ts/charts/candlestick/candlestick.ts +10 -29
  39. package/src/ts/charts/candlestick/glyphs/draw-candlesticks.ts +36 -2
  40. package/src/ts/charts/candlestick/glyphs/draw-ohlc.ts +36 -2
  41. package/src/ts/charts/cartesian/cartesian-build.ts +143 -9
  42. package/src/ts/charts/cartesian/cartesian-render.ts +205 -30
  43. package/src/ts/charts/cartesian/cartesian.ts +43 -4
  44. package/src/ts/charts/cartesian/glyphs/density.ts +36 -41
  45. package/src/ts/charts/cartesian/glyphs/lines.ts +13 -15
  46. package/src/ts/charts/cartesian/glyphs/points.ts +12 -17
  47. package/src/ts/charts/chart-base.ts +20 -6
  48. package/src/ts/charts/chart.ts +1 -1
  49. package/src/ts/charts/common/category-axis-resolver.ts +135 -1
  50. package/src/ts/charts/common/expand-domain.ts +40 -0
  51. package/src/ts/charts/common/tree-chart.ts +16 -0
  52. package/src/ts/charts/common/tree-chrome.ts +86 -1
  53. package/src/ts/charts/common/tree-interact.ts +209 -0
  54. package/src/ts/charts/heatmap/heatmap-render.ts +9 -11
  55. package/src/ts/charts/series/glyphs/draw-areas.ts +30 -1
  56. package/src/ts/charts/series/glyphs/draw-lines.ts +151 -76
  57. package/src/ts/charts/series/series-build.ts +394 -21
  58. package/src/ts/charts/series/series-render.ts +159 -38
  59. package/src/ts/charts/series/series-type.ts +37 -17
  60. package/src/ts/charts/series/series.ts +63 -68
  61. package/src/ts/charts/sunburst/sunburst-interact.ts +18 -162
  62. package/src/ts/charts/sunburst/sunburst-render.ts +24 -89
  63. package/src/ts/charts/sunburst/sunburst.ts +1 -15
  64. package/src/ts/charts/treemap/treemap-interact.ts +22 -189
  65. package/src/ts/charts/treemap/treemap-render.ts +19 -46
  66. package/src/ts/charts/treemap/treemap.ts +1 -16
  67. package/src/ts/config.ts +21 -1
  68. package/src/ts/interaction/host-sink-message.ts +33 -22
  69. package/src/ts/interaction/raw-event-forwarder.ts +10 -12
  70. package/src/ts/interaction/zoom-controller.ts +120 -83
  71. package/src/ts/interaction/zoom-router.ts +3 -126
  72. package/src/ts/map/tile-layer.ts +13 -13
  73. package/src/ts/plugin/plugin.ts +176 -196
  74. package/src/ts/render/scheduler.ts +125 -42
  75. package/src/ts/shaders/line-uniform.frag.glsl +2 -1
  76. package/src/ts/shaders/line-uniform.vert.glsl +19 -0
  77. package/src/ts/theme/palette.ts +1 -4
  78. package/src/ts/transport/protocol.ts +3 -8
  79. package/src/ts/webgl/context-manager.ts +195 -189
  80. package/src/ts/webgl/context-pool.ts +78 -0
  81. package/src/ts/webgl/gl-context.ts +191 -0
  82. package/src/ts/worker/dispatch.ts +0 -1
  83. package/src/ts/worker/renderer.worker.ts +72 -65
  84. 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 { xTicks, yTicks } = computeTicks(xDomain, yDomain, layout);
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 { xTicks, yTicks } = sampleLayout
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
- renderAxesChrome(
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
- const sharedX = chart._lastEffectiveSharedX;
637
- const sharedY = chart._lastEffectiveSharedY;
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
- renderCellXAxis(
693
- canvas,
694
- localX,
695
- cell.layout,
696
- ticks.xTicks,
697
- theme,
698
- !!chart._xLabel,
699
- dpr,
700
- chart.getColumnFormatter(chart._xName, "tick"),
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
- renderCellYAxis(
706
- canvas,
707
- localY,
708
- cell.layout,
709
- ticks.yTicks,
710
- theme,
711
- !!chart._yLabel,
712
- dpr,
713
- chart.getColumnFormatter(chart._yName, "tick"),
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._expandedXMin = this._xMin;
380
- this._expandedXMax = this._xMax;
381
- this._expandedYMin = this._yMin;
382
- this._expandedYMax = this._yMax;
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: extractSplatLocations(gl, splatProgram),
211
+ splat: compileSplatProgram(
212
+ glManager,
213
+ "density-splat",
214
+ splatVert,
215
+ splatFrag,
216
+ ),
221
217
  extremeSplat: null,
222
218
  mrtSplat: null,
223
- resolve: {
224
- program: resolveProgram,
225
- u_heat: gl.getUniformLocation(resolveProgram, "u_heat"),
226
- u_extreme: gl.getUniformLocation(resolveProgram, "u_extreme"),
227
- u_gradient_lut: gl.getUniformLocation(
228
- resolveProgram,
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
- a_corner: gl.getAttribLocation(resolveProgram, "a_corner"),
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
- const program = glManager.shaders.getOrCreate(
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
- const program = glManager.shaders.getOrCreate(
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 extractSplatLocations(
1092
- gl: WebGL2RenderingContext | WebGLRenderingContext,
1093
- program: WebGLProgram,
1086
+ function compileSplatProgram(
1087
+ glManager: WebGLContextManager,
1088
+ key: string,
1089
+ vert: string,
1090
+ frag: string,
1094
1091
  ): SplatProgramCache {
1095
- return {
1096
- program,
1097
- u_projection: gl.getUniformLocation(program, "u_projection"),
1098
- u_radius_ndc: gl.getUniformLocation(program, "u_radius_ndc"),
1099
- u_intensity: gl.getUniformLocation(program, "u_intensity"),
1100
- u_color_range: gl.getUniformLocation(program, "u_color_range"),
1101
- a_corner: gl.getAttribLocation(program, "a_corner"),
1102
- a_position: gl.getAttribLocation(program, "a_position"),
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 gl = glManager.gl;
60
- const program = glManager.shaders.getOrCreate(
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
- program,
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
- const gl = glManager.gl;
57
- const program = glManager.shaders.getOrCreate(
57
+ this._cache = compileProgram<PointCache>(
58
+ glManager,
58
59
  "scatter",
59
60
  scatterVert,
60
61
  scatterFrag,
61
- );
62
- this._cache = {
63
- program,
64
- u_projection: gl.getUniformLocation(program, "u_projection"),
65
- u_point_size: gl.getUniformLocation(program, "u_point_size"),
66
- u_color_range: gl.getUniformLocation(program, "u_color_range"),
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: gl.getAttribLocation(program, "a_position"),
74
- a_color_value: gl.getAttribLocation(program, "a_color_value"),
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
- return formatTickValue;
59
- // const intl = createNumberFormatter("float");
60
- // return (v) => intl.format(v);
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
- return formatDateTickValue;
73
- // const intl = createDatetimeFormatter();
74
- // return (v) => intl.format(v);
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
  /**
@@ -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: "fit",
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,