1212
1313import type { ColumnDataMap } from "../../data/view-reader" ;
1414import type { CategoricalLevel } from "../../chrome/categorical-axis" ;
15+ import { buildGroupRuns } from "../../chrome/categorical-axis-core" ;
1516
1617export interface CategoryAxisResult {
1718 /**
18- * Zero-copy views over the `__ROW_PATH_N__` dictionaries, sliced to
19- * skip leading empty rows ( the "Total" aggregate header that the
20- * view produces when `group_by` is non-empty). Empty when `groupBy`
21- * is empty.
19+ * Fully materialized hierarchical levels — labels and group runs are
20+ * pre-resolved from the view's `__ROW_PATH_N__` dictionaries so the
21+ * chart can retain them past the `with_typed_arrays` callback scope.
22+ * Empty when `groupBy` is empty.
2223 */
2324 rowPaths : CategoricalLevel [ ] ;
2425 /** Rows that actually contribute a category (post-offset). */
@@ -30,8 +31,9 @@ export interface CategoryAxisResult {
3031/**
3132 * Resolve the category axis for a categorical-X chart (bar, candlestick,
3233 * ohlc, …). Walks the `__ROW_PATH_N__` hierarchy columns, skips the
33- * rollup rows at the top ("Total" parent aggregates), and returns zero-
34- * copy dictionary views plus the trimmed category count.
34+ * rollup rows at the top ("Total" parent aggregates), and returns fully
35+ * JS-owned level structures (precomputed labels + runs) plus the
36+ * trimmed category count.
3537 *
3638 * When `groupByLen === 0`, there are no row-path columns and the
3739 * category axis falls back to the raw row index — callers infer that
@@ -42,7 +44,8 @@ export function resolveCategoryAxis(
4244 numRows : number ,
4345 groupByLen : number ,
4446) : CategoryAxisResult {
45- const rawRowPaths : CategoricalLevel [ ] = [ ] ;
47+ type RawLevel = { indices : Int32Array ; dictionary : string [ ] } ;
48+ const rawRowPaths : RawLevel [ ] = [ ] ;
4649 for ( let n = 0 ; ; n ++ ) {
4750 const rp = columns . get ( `__ROW_PATH_${ n } __` ) ;
4851 if ( ! rp || rp . type !== "string" || ! rp . indices || ! rp . dictionary ) break ;
@@ -66,15 +69,34 @@ export function resolveCategoryAxis(
6669 }
6770 const numCategories = Math . max ( 0 , numRows - rowOffset ) ;
6871
72+ const L = rawRowPaths . length ;
6973 const rowPaths : CategoricalLevel [ ] =
70- groupByLen > 0 && rawRowPaths . length > 0
71- ? rawRowPaths . map ( ( rp ) => ( {
72- indices :
73- rowOffset === 0
74- ? rp . indices
75- : rp . indices . subarray ( rowOffset ) ,
76- dictionary : rp . dictionary ,
77- } ) )
74+ groupByLen > 0 && L > 0
75+ ? rawRowPaths . map ( ( rp , levelIdx ) => {
76+ const labels = new Array < string > ( numCategories ) ;
77+ let maxLabelChars = 0 ;
78+ for ( let r = 0 ; r < numCategories ; r ++ ) {
79+ const s = rp . dictionary [ rp . indices [ r + rowOffset ] ] ?? "" ;
80+ labels [ r ] = s ;
81+ if ( s . length > maxLabelChars ) maxLabelChars = s . length ;
82+ }
83+ // Only outer levels need the run-length encoding for
84+ // bracket rendering; leaves render per-row.
85+ const runs =
86+ levelIdx === L - 1
87+ ? [ ]
88+ : buildGroupRuns (
89+ rp . indices ,
90+ rp . dictionary ,
91+ rowOffset ,
92+ rowOffset + numCategories ,
93+ ) . map ( ( run ) => ( {
94+ startIdx : run . startIdx - rowOffset ,
95+ endIdx : run . endIdx - rowOffset ,
96+ label : run . label ,
97+ } ) ) ;
98+ return { labels, runs, maxLabelChars } ;
99+ } )
78100 : [ ] ;
79101
80102 return { rowPaths, numCategories, rowOffset } ;
0 commit comments