@perspective-dev/viewer-charts 4.3.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/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 +1 -1
- 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
|
@@ -16,6 +16,49 @@ import type { WebGLContextManager } from "../../webgl/context-manager";
|
|
|
16
16
|
import type { CartesianChart, SplitGroup } from "./cartesian";
|
|
17
17
|
import { LabelInterner } from "./label-interner";
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a row's string value into a slot index in `dictionary`,
|
|
21
|
+
* inserting on first encounter. Invalid / missing values land in a
|
|
22
|
+
* lazily-added `"(null)"` slot — no reserved slot 0 when the data has
|
|
23
|
+
* no nulls. Shared by the X and Y categorical paths in
|
|
24
|
+
* `processCartesianChunk`.
|
|
25
|
+
*/
|
|
26
|
+
function lookupCategorySlot(
|
|
27
|
+
col: ColumnData | null,
|
|
28
|
+
rowIdx: number,
|
|
29
|
+
dictionary: string[],
|
|
30
|
+
seen: Map<string, number>,
|
|
31
|
+
): number {
|
|
32
|
+
let label: string;
|
|
33
|
+
if (!col) {
|
|
34
|
+
label = "(null)";
|
|
35
|
+
} else {
|
|
36
|
+
const valid = col.valid;
|
|
37
|
+
const isValid = valid
|
|
38
|
+
? !!((valid[rowIdx >> 3] >> (rowIdx & 7)) & 1)
|
|
39
|
+
: true;
|
|
40
|
+
if (!isValid) {
|
|
41
|
+
label = "(null)";
|
|
42
|
+
} else if (col.indices && col.dictionary) {
|
|
43
|
+
label = col.dictionary[col.indices[rowIdx]] ?? "(null)";
|
|
44
|
+
} else if (col.values) {
|
|
45
|
+
const v = col.values[rowIdx];
|
|
46
|
+
label = v == null ? "(null)" : String(v);
|
|
47
|
+
} else {
|
|
48
|
+
label = "(null)";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let slot = seen.get(label);
|
|
53
|
+
if (slot === undefined) {
|
|
54
|
+
slot = dictionary.length;
|
|
55
|
+
dictionary.push(label);
|
|
56
|
+
seen.set(label, slot);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return slot;
|
|
60
|
+
}
|
|
61
|
+
|
|
19
62
|
/**
|
|
20
63
|
* Resolve per-split-prefix column-name tuples. `colorBase`/`sizeBase`
|
|
21
64
|
* are optional (empty string when the corresponding slot is unset).
|
|
@@ -122,6 +165,41 @@ export function initCartesianPipeline(
|
|
|
122
165
|
chart._yLabel = yBase;
|
|
123
166
|
chart._xIsRowIndex = !xBase;
|
|
124
167
|
|
|
168
|
+
// Post-aggregation `string` columns on X / Y switch the axis to
|
|
169
|
+
// categorical: per-row slot indices are written into `_xData` /
|
|
170
|
+
// `_yData` instead of raw values, and the chrome overlay paints a
|
|
171
|
+
// categorical axis. Reset the per-frame dictionary state here at
|
|
172
|
+
// chunk 0 so slot 0 is always the first non-null label encountered
|
|
173
|
+
// in arrival order (matching the perspective view's sort).
|
|
174
|
+
chart._xIsString = !!xBase && chart._columnTypes[xBase] === "string";
|
|
175
|
+
chart._yIsString = !!yBase && chart._columnTypes[yBase] === "string";
|
|
176
|
+
chart._xCategoryDictionary = [];
|
|
177
|
+
chart._yCategoryDictionary = [];
|
|
178
|
+
chart._xCategorySeen = new Map();
|
|
179
|
+
chart._yCategorySeen = new Map();
|
|
180
|
+
chart._xCategoryDomain = null;
|
|
181
|
+
chart._yCategoryDomain = null;
|
|
182
|
+
|
|
183
|
+
// Categorical axes use 0-based slot indices, so the rebase origin
|
|
184
|
+
// is fixed at 0. Skipping the NaN-init guard below prevents the
|
|
185
|
+
// first-seen-slot from being adopted as the origin (which would
|
|
186
|
+
// shift every other slot's pixel position).
|
|
187
|
+
if (chart._xIsString) {
|
|
188
|
+
chart._xOrigin = 0;
|
|
189
|
+
chart._xMin = 0;
|
|
190
|
+
chart._xMax = 0;
|
|
191
|
+
chart._expandedXMin = Infinity;
|
|
192
|
+
chart._expandedXMax = -Infinity;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (chart._yIsString) {
|
|
196
|
+
chart._yOrigin = 0;
|
|
197
|
+
chart._yMin = 0;
|
|
198
|
+
chart._yMax = 0;
|
|
199
|
+
chart._expandedYMin = Infinity;
|
|
200
|
+
chart._expandedYMax = -Infinity;
|
|
201
|
+
}
|
|
202
|
+
|
|
125
203
|
// Capture the per-series row budget BEFORE any split expansion. When
|
|
126
204
|
// split_by is active we grow `totalCapacity` to fit `numSplits`
|
|
127
205
|
// parallel slot ranges; reading `totalCapacity` again after that
|
|
@@ -245,9 +323,16 @@ export function processCartesianChunk(
|
|
|
245
323
|
// carries the user's selected color column. The color-resolution
|
|
246
324
|
// logic in the inner loop reads uniformly from `ser.colorCol`
|
|
247
325
|
// across both modes.
|
|
326
|
+
//
|
|
327
|
+
// `xColData` / `yColData` carry the full `ColumnData` so the
|
|
328
|
+
// categorical path can read `indices` + `dictionary` for slot
|
|
329
|
+
// lookup; `xCol` / `yCol` keep the numeric fast path zero-cost
|
|
330
|
+
// (and stay `null` on string columns where `values` is unset).
|
|
248
331
|
type SeriesSrc = {
|
|
249
332
|
xCol: Float32Array | Float64Array | Int32Array | null;
|
|
250
|
-
yCol: Float32Array | Float64Array | Int32Array;
|
|
333
|
+
yCol: Float32Array | Float64Array | Int32Array | null;
|
|
334
|
+
xColData: ColumnData | null;
|
|
335
|
+
yColData: ColumnData | null;
|
|
251
336
|
xValid: Uint8Array | undefined;
|
|
252
337
|
yValid: Uint8Array | undefined;
|
|
253
338
|
colorCol: ColumnData | null;
|
|
@@ -260,7 +345,11 @@ export function processCartesianChunk(
|
|
|
260
345
|
for (const sg of chart._splitGroups) {
|
|
261
346
|
const xc = sg.xColName ? columns.get(sg.xColName) : null;
|
|
262
347
|
const yc = columns.get(sg.yColName);
|
|
263
|
-
if (!yc
|
|
348
|
+
if (!yc) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!chart._yIsString && !yc.values) {
|
|
264
353
|
continue;
|
|
265
354
|
}
|
|
266
355
|
|
|
@@ -273,7 +362,9 @@ export function processCartesianChunk(
|
|
|
273
362
|
: null;
|
|
274
363
|
series.push({
|
|
275
364
|
xCol: xc?.values ?? null,
|
|
276
|
-
yCol: yc.values,
|
|
365
|
+
yCol: yc.values ?? null,
|
|
366
|
+
xColData: xc ?? null,
|
|
367
|
+
yColData: yc,
|
|
277
368
|
xValid: xc?.valid,
|
|
278
369
|
yValid: yc.valid,
|
|
279
370
|
colorCol: cc,
|
|
@@ -284,7 +375,11 @@ export function processCartesianChunk(
|
|
|
284
375
|
} else {
|
|
285
376
|
const xc = chart._xName ? columns.get(chart._xName) : null;
|
|
286
377
|
const yc = chart._yName ? columns.get(chart._yName) : null;
|
|
287
|
-
if (!yc
|
|
378
|
+
if (!yc) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!chart._yIsString && !yc.values) {
|
|
288
383
|
return;
|
|
289
384
|
}
|
|
290
385
|
|
|
@@ -296,7 +391,9 @@ export function processCartesianChunk(
|
|
|
296
391
|
: null;
|
|
297
392
|
series.push({
|
|
298
393
|
xCol: xc?.values ?? null,
|
|
299
|
-
yCol: yc.values,
|
|
394
|
+
yCol: yc.values ?? null,
|
|
395
|
+
xColData: xc ?? null,
|
|
396
|
+
yColData: yc,
|
|
300
397
|
xValid: xc?.valid,
|
|
301
398
|
yValid: yc?.valid,
|
|
302
399
|
colorCol: cc,
|
|
@@ -386,11 +483,20 @@ export function processCartesianChunk(
|
|
|
386
483
|
let writeIdx = 0;
|
|
387
484
|
for (let j = 0; j < sourceLength && writeIdx < maxWrite; j++) {
|
|
388
485
|
const i = j;
|
|
389
|
-
|
|
486
|
+
// Numeric axes filter out null/invalid rows entirely;
|
|
487
|
+
// categorical axes route them into a `"(null)"` slot
|
|
488
|
+
// instead, so the validity / NaN guards only apply on
|
|
489
|
+
// the numeric branch.
|
|
490
|
+
if (
|
|
491
|
+
!chart._yIsString &&
|
|
492
|
+
ser.yValid &&
|
|
493
|
+
!((ser.yValid[i >> 3] >> (i & 7)) & 1)
|
|
494
|
+
) {
|
|
390
495
|
continue;
|
|
391
496
|
}
|
|
392
497
|
|
|
393
498
|
if (
|
|
499
|
+
!chart._xIsString &&
|
|
394
500
|
ser.xCol &&
|
|
395
501
|
ser.xValid &&
|
|
396
502
|
!((ser.xValid[i >> 3] >> (i & 7)) & 1)
|
|
@@ -402,12 +508,40 @@ export function processCartesianChunk(
|
|
|
402
508
|
colorValid !== undefined &&
|
|
403
509
|
!((colorValid[i >> 3] >> (i & 7)) & 1);
|
|
404
510
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
511
|
+
let rawY: number;
|
|
512
|
+
if (chart._yIsString) {
|
|
513
|
+
rawY = lookupCategorySlot(
|
|
514
|
+
ser.yColData,
|
|
515
|
+
i,
|
|
516
|
+
chart._yCategoryDictionary,
|
|
517
|
+
chart._yCategorySeen,
|
|
518
|
+
);
|
|
519
|
+
} else if (ser.yCol) {
|
|
520
|
+
rawY = ser.yCol[i] as number;
|
|
521
|
+
if (isNaN(rawY)) {
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
} else {
|
|
408
525
|
continue;
|
|
409
526
|
}
|
|
410
527
|
|
|
528
|
+
let rawX: number;
|
|
529
|
+
if (chart._xIsString) {
|
|
530
|
+
rawX = lookupCategorySlot(
|
|
531
|
+
ser.xColData,
|
|
532
|
+
i,
|
|
533
|
+
chart._xCategoryDictionary,
|
|
534
|
+
chart._xCategorySeen,
|
|
535
|
+
);
|
|
536
|
+
} else if (ser.xCol) {
|
|
537
|
+
rawX = ser.xCol[i] as number;
|
|
538
|
+
if (isNaN(rawX)) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
} else {
|
|
542
|
+
rawX = startRow + i;
|
|
543
|
+
}
|
|
544
|
+
|
|
411
545
|
// Project raw (x, y) → data-space (x, y). Default is
|
|
412
546
|
// identity for cartesian charts; map subclasses override
|
|
413
547
|
// to apply Mercator. Second NaN guard catches projection
|
|
@@ -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;
|