@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
|
@@ -12,21 +12,28 @@
|
|
|
12
12
|
|
|
13
13
|
import type { ColumnDataMap } from "../../data/view-reader";
|
|
14
14
|
import { buildSplitGroups } from "../../data/split-groups";
|
|
15
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
CategoricalDomain,
|
|
17
|
+
CategoricalLevel,
|
|
18
|
+
} from "../../axis/categorical-axis";
|
|
16
19
|
import {
|
|
17
20
|
resolveAxisMode,
|
|
18
21
|
resolveCategoryAxis,
|
|
19
22
|
resolveNumericCategoryDomain,
|
|
23
|
+
resolveValueCategoryDomain,
|
|
20
24
|
type AxisMode,
|
|
21
25
|
type NumericCategoryDomain,
|
|
26
|
+
type ValueCategoryColumn,
|
|
22
27
|
} from "../common/category-axis-resolver";
|
|
23
28
|
import { computeSlotGeometry } from "../common/band-layout";
|
|
24
29
|
import {
|
|
25
30
|
resolveChartType,
|
|
26
31
|
resolveStack,
|
|
27
32
|
resolveAltAxis,
|
|
33
|
+
resolveInterpolate,
|
|
28
34
|
type ChartType,
|
|
29
35
|
type ColumnChartConfig,
|
|
36
|
+
type InterpolateMode,
|
|
30
37
|
} from "./series-type";
|
|
31
38
|
|
|
32
39
|
const DUAL_Y_RATIO_THRESHOLD = 50;
|
|
@@ -42,6 +49,26 @@ export interface SeriesInfo {
|
|
|
42
49
|
axis: 0 | 1;
|
|
43
50
|
chartType: ChartType;
|
|
44
51
|
stack: boolean;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* First / last category index this series contributes data to, in
|
|
55
|
+
* the post-Pass-2 sample grid. For line+any mode and area+solid:
|
|
56
|
+
* every cell in `[start, end]` has a value (real or synthesized).
|
|
57
|
+
* For area+skip: `[start, end]` is the real-data extent; interior
|
|
58
|
+
* cells with `sampleValid=0` are gaps. `start = -1` (with `end = -1`)
|
|
59
|
+
* means the series has no real samples — downstream skips it.
|
|
60
|
+
*/
|
|
61
|
+
start: number;
|
|
62
|
+
end: number;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolved interpolation mode for this aggregate. The build
|
|
66
|
+
* pipeline reads it to decide whether Pass 2 runs for area
|
|
67
|
+
* (and which fills to apply); the line glyph reads it at draw
|
|
68
|
+
* time to set `u_interp_alpha`. Always one of the three modes;
|
|
69
|
+
* never the legacy boolean form.
|
|
70
|
+
*/
|
|
71
|
+
interpolateMode: InterpolateMode;
|
|
45
72
|
}
|
|
46
73
|
|
|
47
74
|
/**
|
|
@@ -326,6 +353,26 @@ export interface SeriesPipelineResult {
|
|
|
326
353
|
leftDomain: { min: number; max: number };
|
|
327
354
|
rightDomain: { min: number; max: number } | null;
|
|
328
355
|
hasRightAxis: boolean;
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Per-axis-side value mode discriminator. `"category"` fires when
|
|
359
|
+
* every aggregate on that side is post-aggregation `string`-typed
|
|
360
|
+
* (all-or-nothing rule). Bar y0/y1 then hold dictionary slot
|
|
361
|
+
* indices and the chrome overlay paints a categorical axis on
|
|
362
|
+
* that side. `null` for the alt side when there are no series
|
|
363
|
+
* pinned to alt.
|
|
364
|
+
*/
|
|
365
|
+
leftValueAxisMode: "numeric" | "category";
|
|
366
|
+
rightValueAxisMode: "numeric" | "category" | null;
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Single-level `CategoricalDomain` shared across every aggregate
|
|
370
|
+
* on the corresponding side. Set only when that side's mode is
|
|
371
|
+
* `"category"`; the chrome renderer in `series-render` materializes
|
|
372
|
+
* the side's `BarCategoryAxis` from this.
|
|
373
|
+
*/
|
|
374
|
+
leftValueCategoryDomain: CategoricalDomain | null;
|
|
375
|
+
rightValueCategoryDomain: CategoricalDomain | null;
|
|
329
376
|
}
|
|
330
377
|
|
|
331
378
|
function setValidBit(valid: Uint8Array, idx: number): void {
|
|
@@ -381,6 +428,10 @@ export function buildSeriesPipeline(
|
|
|
381
428
|
leftDomain: { min: 0, max: 0 },
|
|
382
429
|
rightDomain: null,
|
|
383
430
|
hasRightAxis: false,
|
|
431
|
+
leftValueAxisMode: "numeric",
|
|
432
|
+
rightValueAxisMode: null,
|
|
433
|
+
leftValueCategoryDomain: null,
|
|
434
|
+
rightValueCategoryDomain: null,
|
|
384
435
|
};
|
|
385
436
|
|
|
386
437
|
const aggregates = columnSlots.filter((s): s is string => !!s);
|
|
@@ -438,6 +489,11 @@ export function buildSeriesPipeline(
|
|
|
438
489
|
defaultChartType,
|
|
439
490
|
);
|
|
440
491
|
const stack = resolveStack(aggName, chartType, columnsConfig);
|
|
492
|
+
const interpolateMode = resolveInterpolate(
|
|
493
|
+
aggName,
|
|
494
|
+
chartType,
|
|
495
|
+
columnsConfig,
|
|
496
|
+
);
|
|
441
497
|
series.push({
|
|
442
498
|
seriesId: k * P + p,
|
|
443
499
|
aggIdx: k,
|
|
@@ -449,6 +505,9 @@ export function buildSeriesPipeline(
|
|
|
449
505
|
axis: 0,
|
|
450
506
|
chartType,
|
|
451
507
|
stack,
|
|
508
|
+
start: -1,
|
|
509
|
+
end: -1,
|
|
510
|
+
interpolateMode,
|
|
452
511
|
});
|
|
453
512
|
}
|
|
454
513
|
}
|
|
@@ -539,6 +598,102 @@ export function buildSeriesPipeline(
|
|
|
539
598
|
}
|
|
540
599
|
}
|
|
541
600
|
|
|
601
|
+
// Per-aggregate string-ness flag, plus default axis side from the
|
|
602
|
+
// `columns_config.alt_axis` pin. `series[].axis` may still flip
|
|
603
|
+
// again via the auto-alt heuristic below, but we suppress that
|
|
604
|
+
// heuristic entirely once a string aggregate is present (numeric
|
|
605
|
+
// extent ratios are not defined on categorical data).
|
|
606
|
+
const aggIsString = new Array<boolean>(M);
|
|
607
|
+
const defaultAxisSide = new Array<number>(M);
|
|
608
|
+
let anyStringAgg = false;
|
|
609
|
+
for (let k = 0; k < M; k++) {
|
|
610
|
+
const aggName = aggregates[k];
|
|
611
|
+
const splitKey = splitPrefixes[0];
|
|
612
|
+
const colName = splitKey === "" ? aggName : `${splitKey}|${aggName}`;
|
|
613
|
+
aggIsString[k] = columns.get(colName)?.type === "string";
|
|
614
|
+
defaultAxisSide[k] = resolveAltAxis(aggName, columnsConfig) ? 1 : 0;
|
|
615
|
+
if (aggIsString[k]) {
|
|
616
|
+
anyStringAgg = true;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Per-side categorical resolution. Apply the all-or-nothing rule:
|
|
621
|
+
// a side becomes categorical only if every aggregate currently
|
|
622
|
+
// assigned to it (by `defaultAxisSide` — the alt_axis pin) is
|
|
623
|
+
// string-typed. Auto-alt-axis can't re-assign across modes since
|
|
624
|
+
// we disable it whenever any string aggregate exists.
|
|
625
|
+
const primaryAggs: ValueCategoryColumn[] = [];
|
|
626
|
+
const altAggs: ValueCategoryColumn[] = [];
|
|
627
|
+
const primaryAggColIdx: number[] = [];
|
|
628
|
+
const altAggColIdx: number[] = [];
|
|
629
|
+
for (let k = 0; k < M; k++) {
|
|
630
|
+
const aggName = aggregates[k];
|
|
631
|
+
for (let p = 0; p < P; p++) {
|
|
632
|
+
const splitKey = splitPrefixes[p];
|
|
633
|
+
const colName =
|
|
634
|
+
splitKey === "" ? aggName : `${splitKey}|${aggName}`;
|
|
635
|
+
const colIdx = k * P + p;
|
|
636
|
+
const entry: ValueCategoryColumn = {
|
|
637
|
+
name: colName,
|
|
638
|
+
type: aggIsString[k] ? "string" : "numeric",
|
|
639
|
+
data: columns.get(colName),
|
|
640
|
+
};
|
|
641
|
+
if (defaultAxisSide[k] === 0) {
|
|
642
|
+
primaryAggs.push(entry);
|
|
643
|
+
primaryAggColIdx.push(colIdx);
|
|
644
|
+
} else {
|
|
645
|
+
altAggs.push(entry);
|
|
646
|
+
altAggColIdx.push(colIdx);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const primaryValueAxisLabel = primaryAggs
|
|
652
|
+
.map((c) => c.name)
|
|
653
|
+
.filter((s, i, arr) => arr.indexOf(s) === i)
|
|
654
|
+
.join(", ");
|
|
655
|
+
const altValueAxisLabel = altAggs
|
|
656
|
+
.map((c) => c.name)
|
|
657
|
+
.filter((s, i, arr) => arr.indexOf(s) === i)
|
|
658
|
+
.join(", ");
|
|
659
|
+
|
|
660
|
+
const primaryCategorical =
|
|
661
|
+
primaryAggs.length > 0
|
|
662
|
+
? resolveValueCategoryDomain(
|
|
663
|
+
primaryAggs,
|
|
664
|
+
numRows,
|
|
665
|
+
rowOffset,
|
|
666
|
+
primaryValueAxisLabel,
|
|
667
|
+
)
|
|
668
|
+
: null;
|
|
669
|
+
const altCategorical =
|
|
670
|
+
altAggs.length > 0
|
|
671
|
+
? resolveValueCategoryDomain(
|
|
672
|
+
altAggs,
|
|
673
|
+
numRows,
|
|
674
|
+
rowOffset,
|
|
675
|
+
altValueAxisLabel,
|
|
676
|
+
)
|
|
677
|
+
: null;
|
|
678
|
+
|
|
679
|
+
// Per-column slot buffers indexed in the same `colIdx = k * P + p`
|
|
680
|
+
// space as `colValues`. `colSlots[colIdx]` is non-null exactly when
|
|
681
|
+
// the side `defaultAxisSide[k]` is categorical and that side's
|
|
682
|
+
// resolver returned slot buffers.
|
|
683
|
+
const colSlots: (Int32Array | null)[] = new Array(M * P).fill(null);
|
|
684
|
+
if (primaryCategorical) {
|
|
685
|
+
for (let i = 0; i < primaryAggColIdx.length; i++) {
|
|
686
|
+
colSlots[primaryAggColIdx[i]] =
|
|
687
|
+
primaryCategorical.perColumnSlots[i];
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (altCategorical) {
|
|
692
|
+
for (let i = 0; i < altAggColIdx.length; i++) {
|
|
693
|
+
colSlots[altAggColIdx[i]] = altCategorical.perColumnSlots[i];
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
542
697
|
// Pre-allocate columnar bar storage at N*M*P upper bound. The
|
|
543
698
|
// pipeline emits at most one record per (cat, agg, split) cell;
|
|
544
699
|
// `bars.count` tracks the active prefix.
|
|
@@ -546,22 +701,32 @@ export function buildSeriesPipeline(
|
|
|
546
701
|
const bars = ensureBarColumnsCapacity(scratchBars ?? null, barCap);
|
|
547
702
|
let barWrite = 0;
|
|
548
703
|
|
|
704
|
+
// Pass 1 — populate the raw sample grid + valid bitset. Stacking
|
|
705
|
+
// and bar-record emission run in pass 3 (below) so that pass 2
|
|
706
|
+
// can interpolate interior nulls for line/area series before the
|
|
707
|
+
// stack accumulator sees them; otherwise an interpolated cell in
|
|
708
|
+
// a stacked area would not contribute to the running y0/y1 of
|
|
709
|
+
// subsequent series at the same catIdx.
|
|
549
710
|
for (let catI = 0; catI < N; catI++) {
|
|
550
711
|
const row = catI + rowOffset;
|
|
551
|
-
|
|
552
|
-
// Hoist the category center — same value across all (k, p) for
|
|
553
|
-
// the current catI.
|
|
554
|
-
const catCenter = categoryPositions ? categoryPositions[catI] : catI;
|
|
555
|
-
|
|
556
712
|
for (let k = 0; k < M; k++) {
|
|
557
|
-
const slotOffset = slotOffsets[k];
|
|
558
|
-
const xCenter = catCenter + slotOffset;
|
|
559
|
-
const ext = aggExtents[k];
|
|
560
|
-
|
|
561
713
|
for (let p = 0; p < P; p++) {
|
|
562
|
-
const seriesId = k * P + p;
|
|
563
|
-
const s = series[seriesId];
|
|
564
714
|
const colIdx = k * P + p;
|
|
715
|
+
|
|
716
|
+
// Categorical value-axis branch: `colSlots[colIdx]` is
|
|
717
|
+
// a per-catI Int32Array of dictionary slot indices, with
|
|
718
|
+
// `(null)` already routed to its own slot — every row
|
|
719
|
+
// is a valid sample and stack/extent logic just runs
|
|
720
|
+
// against the slot integer.
|
|
721
|
+
const slots = colSlots[colIdx];
|
|
722
|
+
if (slots) {
|
|
723
|
+
const seriesId = k * P + p;
|
|
724
|
+
const sampleIdx = catI * S + seriesId;
|
|
725
|
+
samples[sampleIdx] = slots[catI];
|
|
726
|
+
setValidBit(sampleValid, sampleIdx);
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
|
|
565
730
|
const values = colValues[colIdx];
|
|
566
731
|
if (!values) {
|
|
567
732
|
continue;
|
|
@@ -580,11 +745,155 @@ export function buildSeriesPipeline(
|
|
|
580
745
|
continue;
|
|
581
746
|
}
|
|
582
747
|
|
|
583
|
-
|
|
584
|
-
// glyph that needs it (line, scatter, non-stacking bar/area).
|
|
748
|
+
const seriesId = k * P + p;
|
|
585
749
|
const sampleIdx = catI * S + seriesId;
|
|
586
750
|
samples[sampleIdx] = v;
|
|
587
751
|
setValidBit(sampleValid, sampleIdx);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Compute per-series [start, end] from sampleValid (post-Pass 1).
|
|
757
|
+
// Drives Pass 2's interpolation range, Pass 3's stack/bar emission,
|
|
758
|
+
// axis-extent calc, and downstream rendering. Series with no real
|
|
759
|
+
// samples keep start = end = -1 and are skipped everywhere.
|
|
760
|
+
for (let seriesId = 0; seriesId < S; seriesId++) {
|
|
761
|
+
let first = -1;
|
|
762
|
+
let last = -1;
|
|
763
|
+
for (let c = 0; c < N; c++) {
|
|
764
|
+
const idx = c * S + seriesId;
|
|
765
|
+
if ((sampleValid[idx >> 3] >> (idx & 7)) & 1) {
|
|
766
|
+
if (first === -1) {
|
|
767
|
+
first = c;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
last = c;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
series[seriesId].start = first;
|
|
775
|
+
series[seriesId].end = last;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Pass 2 — synthesize values for nulls covered by interpolation.
|
|
779
|
+
// Writes `samples[c]` but deliberately does NOT touch `sampleValid`:
|
|
780
|
+
// the renderer derives "synthesized cell" from
|
|
781
|
+
// `c in [start, end] && sampleValid[c] === 0`, so the bit must stay
|
|
782
|
+
// 0 at synthesized cells. Per-series gating:
|
|
783
|
+
//
|
|
784
|
+
// - line, solid / transparent: interior linear interpolation.
|
|
785
|
+
// - area, solid: every synthesized cell (interior null,
|
|
786
|
+
// leading/trailing null) gets value 0. Stacked areas above the
|
|
787
|
+
// null sit on the unchanged baseline — interpolating to a
|
|
788
|
+
// non-zero value here would phantom-lift the upper series at
|
|
789
|
+
// the gap. Range collapses to [0, N-1].
|
|
790
|
+
// - any series with mode = "skip": skipped (the renderer's
|
|
791
|
+
// [start, end] iteration treats interior nulls correctly via
|
|
792
|
+
// the sampleValid lookup for area, and shader alpha=0 for line).
|
|
793
|
+
// - other chart types: skipped.
|
|
794
|
+
//
|
|
795
|
+
// X-axis units for line interpolation match the rendering: numeric
|
|
796
|
+
// mode uses `categoryPositions[c]`, category mode uses the cat
|
|
797
|
+
// index. `samples` is freshly allocated each build (Float32Array
|
|
798
|
+
// zero-init), so interior null cells already hold 0 — area's
|
|
799
|
+
// "zero-fill interior" is implicit and needs no explicit writes
|
|
800
|
+
// there; only the leading/trailing range extension is written.
|
|
801
|
+
for (let seriesId = 0; seriesId < S; seriesId++) {
|
|
802
|
+
const s = series[seriesId];
|
|
803
|
+
if (s.start < 0) {
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (s.interpolateMode === "skip") {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (s.chartType === "line") {
|
|
812
|
+
let lastValid = s.start;
|
|
813
|
+
for (let c = s.start + 1; c <= s.end; c++) {
|
|
814
|
+
const idx = c * S + seriesId;
|
|
815
|
+
const ok = (sampleValid[idx >> 3] >> (idx & 7)) & 1;
|
|
816
|
+
if (!ok) {
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
if (c - lastValid > 1) {
|
|
821
|
+
const startIdx = lastValid * S + seriesId;
|
|
822
|
+
const startV = samples[startIdx];
|
|
823
|
+
const endV = samples[idx];
|
|
824
|
+
const xStart = categoryPositions
|
|
825
|
+
? categoryPositions[lastValid]
|
|
826
|
+
: lastValid;
|
|
827
|
+
const xEnd = categoryPositions ? categoryPositions[c] : c;
|
|
828
|
+
const dx = xEnd - xStart;
|
|
829
|
+
for (let g = 1; g < c - lastValid; g++) {
|
|
830
|
+
const cc = lastValid + g;
|
|
831
|
+
const xMid = categoryPositions
|
|
832
|
+
? categoryPositions[cc]
|
|
833
|
+
: cc;
|
|
834
|
+
const t = dx === 0 ? 0 : (xMid - xStart) / dx;
|
|
835
|
+
samples[cc * S + seriesId] =
|
|
836
|
+
startV + (endV - startV) * t;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
lastValid = c;
|
|
841
|
+
}
|
|
842
|
+
} else if (s.chartType === "area") {
|
|
843
|
+
// Leading / trailing zero-fill. Interior nulls already sit
|
|
844
|
+
// at 0 from Float32Array zero-init; no per-cell write
|
|
845
|
+
// needed in (s.start, s.end). Range collapses to [0, N-1]
|
|
846
|
+
// so Pass 3 and the area glyph treat the whole span as
|
|
847
|
+
// renderable (continuous strip resting on the baseline at
|
|
848
|
+
// synthesized cells).
|
|
849
|
+
for (let c = 0; c < s.start; c++) {
|
|
850
|
+
samples[c * S + seriesId] = 0;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
for (let c = s.end + 1; c < N; c++) {
|
|
854
|
+
samples[c * S + seriesId] = 0;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
s.start = 0;
|
|
858
|
+
s.end = N - 1;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Pass 3 — emit stack/bar records and update per-aggregate extents
|
|
863
|
+
// from the (possibly synthesized) samples grid. The cell-validity
|
|
864
|
+
// predicate is mode-aware: for line (any mode) and area+solid,
|
|
865
|
+
// Pass 2 has guaranteed every cell in `[start, end]` carries a
|
|
866
|
+
// meaningful value (real or synthesized) — `sampleValid` is the
|
|
867
|
+
// "is real" mask, not the "has value" mask, so we trust the range
|
|
868
|
+
// alone. For area+skip and bar/scatter, fall back to the original
|
|
869
|
+
// per-cell `sampleValid` check.
|
|
870
|
+
for (let catI = 0; catI < N; catI++) {
|
|
871
|
+
const catCenter = categoryPositions ? categoryPositions[catI] : catI;
|
|
872
|
+
for (let k = 0; k < M; k++) {
|
|
873
|
+
const slotOffset = slotOffsets[k];
|
|
874
|
+
const xCenter = catCenter + slotOffset;
|
|
875
|
+
const ext = aggExtents[k];
|
|
876
|
+
|
|
877
|
+
for (let p = 0; p < P; p++) {
|
|
878
|
+
const seriesId = k * P + p;
|
|
879
|
+
const s = series[seriesId];
|
|
880
|
+
if (catI < s.start || catI > s.end) {
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const treatRangeAsValid =
|
|
885
|
+
s.chartType === "line" ||
|
|
886
|
+
(s.chartType === "area" && s.interpolateMode !== "skip");
|
|
887
|
+
const sampleIdx = catI * S + seriesId;
|
|
888
|
+
if (!treatRangeAsValid) {
|
|
889
|
+
if (
|
|
890
|
+
!((sampleValid[sampleIdx >> 3] >> (sampleIdx & 7)) & 1)
|
|
891
|
+
) {
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const v = samples[sampleIdx];
|
|
588
897
|
|
|
589
898
|
// Stacking-glyph path: emit a record with running y0/y1.
|
|
590
899
|
if (
|
|
@@ -592,7 +901,25 @@ export function buildSeriesPipeline(
|
|
|
592
901
|
s.stack
|
|
593
902
|
) {
|
|
594
903
|
if (v === 0) {
|
|
595
|
-
|
|
904
|
+
// Non-area, or area+skip: a zero-value record
|
|
905
|
+
// is degenerate (zero-height bar / invisible
|
|
906
|
+
// strip wedge) and just costs allocation —
|
|
907
|
+
// drop it.
|
|
908
|
+
//
|
|
909
|
+
// Area + non-skip: keep the record so the
|
|
910
|
+
// stacked strip stays continuous through
|
|
911
|
+
// synthesized cells (interior zero-fill +
|
|
912
|
+
// leading / trailing zero-fill). `y1 = y0`
|
|
913
|
+
// makes it a zero-height vertex pair in the
|
|
914
|
+
// strip; posStack doesn't increment, so the
|
|
915
|
+
// series above stacks on the unchanged
|
|
916
|
+
// baseline.
|
|
917
|
+
if (
|
|
918
|
+
s.chartType !== "area" ||
|
|
919
|
+
s.interpolateMode === "skip"
|
|
920
|
+
) {
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
596
923
|
}
|
|
597
924
|
|
|
598
925
|
const stackIdx = catI * M + k;
|
|
@@ -684,7 +1011,12 @@ export function buildSeriesPipeline(
|
|
|
684
1011
|
bars.count = barWrite;
|
|
685
1012
|
|
|
686
1013
|
let hasRightAxis = false;
|
|
687
|
-
|
|
1014
|
+
// Auto-alt-axis compares numeric magnitudes; a string aggregate
|
|
1015
|
+
// contributes no extent and would always land on the smaller side.
|
|
1016
|
+
// Skip the heuristic when any aggregate is string and let the
|
|
1017
|
+
// user's explicit `columns_config.alt_axis` pin (resolved below)
|
|
1018
|
+
// be the only axis-side override.
|
|
1019
|
+
if (autoAltYAxis && M >= 2 && !anyStringAgg) {
|
|
688
1020
|
const extents: number[] = new Array(M);
|
|
689
1021
|
let maxExt = 0;
|
|
690
1022
|
let minExt = Infinity;
|
|
@@ -784,11 +1116,20 @@ export function buildSeriesPipeline(
|
|
|
784
1116
|
continue; // already counted via bars
|
|
785
1117
|
}
|
|
786
1118
|
|
|
1119
|
+
if (s.start < 0) {
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const treatRangeAsValid =
|
|
1124
|
+
s.chartType === "line" ||
|
|
1125
|
+
(s.chartType === "area" && s.interpolateMode !== "skip");
|
|
787
1126
|
const ext = s.axis === 0 ? leftExtent : rightExtent;
|
|
788
|
-
for (let catI =
|
|
1127
|
+
for (let catI = s.start; catI <= s.end; catI++) {
|
|
789
1128
|
const sampleIdx = catI * S + seriesId;
|
|
790
|
-
if (!
|
|
791
|
-
|
|
1129
|
+
if (!treatRangeAsValid) {
|
|
1130
|
+
if (!((sampleValid[sampleIdx >> 3] >> (sampleIdx & 7)) & 1)) {
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
792
1133
|
}
|
|
793
1134
|
|
|
794
1135
|
const v = samples[sampleIdx];
|
|
@@ -826,6 +1167,34 @@ export function buildSeriesPipeline(
|
|
|
826
1167
|
: rightExtent
|
|
827
1168
|
: null;
|
|
828
1169
|
|
|
1170
|
+
// Categorical value-axis: override the numeric extent with the
|
|
1171
|
+
// slot-index range `[0, dictLen-1]`. The chrome renderer paints
|
|
1172
|
+
// the dictionary; the numeric domain we surface is only used by
|
|
1173
|
+
// the projection matrix and pixel mapping, both of which work on
|
|
1174
|
+
// raw slot indices when `*ValueAxisMode === "category"`.
|
|
1175
|
+
const leftValueAxisMode: "numeric" | "category" = primaryCategorical
|
|
1176
|
+
? "category"
|
|
1177
|
+
: "numeric";
|
|
1178
|
+
const rightValueAxisMode: "numeric" | "category" | null = hasRightAxis
|
|
1179
|
+
? altCategorical
|
|
1180
|
+
? "category"
|
|
1181
|
+
: "numeric"
|
|
1182
|
+
: null;
|
|
1183
|
+
const finalLeftDomain =
|
|
1184
|
+
primaryCategorical && primaryCategorical.domain.numRows > 0
|
|
1185
|
+
? {
|
|
1186
|
+
min: 0,
|
|
1187
|
+
max: Math.max(0, primaryCategorical.domain.numRows - 1),
|
|
1188
|
+
}
|
|
1189
|
+
: leftExtent;
|
|
1190
|
+
const finalRightDomain =
|
|
1191
|
+
altCategorical && altCategorical.domain.numRows > 0 && hasRightAxis
|
|
1192
|
+
? {
|
|
1193
|
+
min: 0,
|
|
1194
|
+
max: Math.max(0, altCategorical.domain.numRows - 1),
|
|
1195
|
+
}
|
|
1196
|
+
: rightDomain;
|
|
1197
|
+
|
|
829
1198
|
return {
|
|
830
1199
|
aggregates,
|
|
831
1200
|
splitPrefixes,
|
|
@@ -841,8 +1210,12 @@ export function buildSeriesPipeline(
|
|
|
841
1210
|
negStack,
|
|
842
1211
|
samples,
|
|
843
1212
|
sampleValid,
|
|
844
|
-
leftDomain:
|
|
845
|
-
rightDomain,
|
|
1213
|
+
leftDomain: finalLeftDomain,
|
|
1214
|
+
rightDomain: finalRightDomain,
|
|
846
1215
|
hasRightAxis,
|
|
1216
|
+
leftValueAxisMode,
|
|
1217
|
+
rightValueAxisMode,
|
|
1218
|
+
leftValueCategoryDomain: primaryCategorical?.domain ?? null,
|
|
1219
|
+
rightValueCategoryDomain: altCategorical?.domain ?? null,
|
|
847
1220
|
};
|
|
848
1221
|
}
|