@rowsncolumns/charts 28.1.8 → 28.1.10

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 (47) hide show
  1. package/dist/cjs/basic-chart/basic-chart.d.ts.map +1 -1
  2. package/dist/cjs/basic-chart/basic-chart.js +483 -417
  3. package/dist/cjs/basic-chart/helpers.d.ts.map +1 -1
  4. package/dist/cjs/basic-chart/helpers.js +2 -0
  5. package/dist/cjs/plotly/basic-chart/basic-chart.d.ts.map +1 -1
  6. package/dist/cjs/plotly/basic-chart/basic-chart.js +315 -184
  7. package/dist/cjs/plotly/basic-chart/helpers.d.ts.map +1 -1
  8. package/dist/cjs/plotly/basic-chart/helpers.js +2 -0
  9. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  10. package/dist/cjs/utils/bubble.d.ts +34 -1
  11. package/dist/cjs/utils/bubble.d.ts.map +1 -1
  12. package/dist/cjs/utils/bubble.js +39 -13
  13. package/dist/cjs/utils/echarts-overlays.d.ts +46 -0
  14. package/dist/cjs/utils/echarts-overlays.d.ts.map +1 -0
  15. package/dist/cjs/utils/echarts-overlays.js +134 -0
  16. package/dist/cjs/utils/hierarchy.d.ts +23 -1
  17. package/dist/cjs/utils/hierarchy.d.ts.map +1 -1
  18. package/dist/cjs/utils/hierarchy.js +179 -55
  19. package/dist/cjs/utils/overrides.d.ts.map +1 -1
  20. package/dist/cjs/utils/overrides.js +9 -2
  21. package/dist/cjs/utils/trendline.d.ts +7 -1
  22. package/dist/cjs/utils/trendline.d.ts.map +1 -1
  23. package/dist/cjs/utils/trendline.js +82 -18
  24. package/dist/esm/basic-chart/basic-chart.d.ts.map +1 -1
  25. package/dist/esm/basic-chart/basic-chart.js +320 -239
  26. package/dist/esm/basic-chart/helpers.d.ts.map +1 -1
  27. package/dist/esm/basic-chart/helpers.js +2 -0
  28. package/dist/esm/plotly/basic-chart/basic-chart.d.ts.map +1 -1
  29. package/dist/esm/plotly/basic-chart/basic-chart.js +235 -58
  30. package/dist/esm/plotly/basic-chart/helpers.d.ts.map +1 -1
  31. package/dist/esm/plotly/basic-chart/helpers.js +2 -0
  32. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  33. package/dist/esm/utils/bubble.d.ts +34 -1
  34. package/dist/esm/utils/bubble.d.ts.map +1 -1
  35. package/dist/esm/utils/bubble.js +35 -11
  36. package/dist/esm/utils/echarts-overlays.d.ts +46 -0
  37. package/dist/esm/utils/echarts-overlays.d.ts.map +1 -0
  38. package/dist/esm/utils/echarts-overlays.js +135 -0
  39. package/dist/esm/utils/hierarchy.d.ts +23 -1
  40. package/dist/esm/utils/hierarchy.d.ts.map +1 -1
  41. package/dist/esm/utils/hierarchy.js +168 -50
  42. package/dist/esm/utils/overrides.d.ts.map +1 -1
  43. package/dist/esm/utils/overrides.js +9 -2
  44. package/dist/esm/utils/trendline.d.ts +7 -1
  45. package/dist/esm/utils/trendline.d.ts.map +1 -1
  46. package/dist/esm/utils/trendline.js +67 -14
  47. package/package.json +3 -3
@@ -0,0 +1,46 @@
1
+ type OverlaySeriesSpec = {
2
+ color?: string | null;
3
+ useSecondaryAxis?: boolean | null;
4
+ trendlines?: Array<{
5
+ type?: any;
6
+ order?: number | null;
7
+ period?: number | null;
8
+ name?: string | null;
9
+ color?: string | null;
10
+ forecastForward?: number | null;
11
+ forecastBackward?: number | null;
12
+ }> | null;
13
+ errorBars?: {
14
+ direction?: "x" | "y" | null;
15
+ barType?: "both" | "plus" | "minus" | null;
16
+ valueType?: any;
17
+ value?: number | null;
18
+ noEndCap?: boolean | null;
19
+ plusRange?: any;
20
+ minusRange?: any;
21
+ } | null;
22
+ };
23
+ export type EchartsOverlayOptions<TRange = unknown> = {
24
+ series: OverlaySeriesSpec;
25
+ /** The series' values (Y on an XY chart), index-aligned with `xValues`. */
26
+ values: (number | null | undefined)[];
27
+ /**
28
+ * Scatter / bubble: the real X coordinate per point. Omitted for a
29
+ * category chart, where the point index is the category coordinate.
30
+ */
31
+ xValues?: (number | null | undefined)[] | null;
32
+ /** Horizontal bar: the category index is the y coordinate. */
33
+ horizontal?: boolean;
34
+ dataLabel: string;
35
+ /** The data series' own colour — overlays default to it. */
36
+ seriesColor: string;
37
+ /** Reads a custom error bar's plus / minus range. */
38
+ resolveRange: (range: TRange) => unknown[] | null | undefined;
39
+ };
40
+ /**
41
+ * Build the overlay series for one data series. Returns an empty array
42
+ * when the series has neither trendlines nor error bars.
43
+ */
44
+ export declare const buildEchartsOverlaySeries: <TRange = unknown>({ series, values, xValues, horizontal, dataLabel, seriesColor, resolveRange, }: EchartsOverlayOptions<TRange>) => any[];
45
+ export {};
46
+ //# sourceMappingURL=echarts-overlays.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"echarts-overlays.d.ts","sourceRoot":"","sources":["../../../utils/echarts-overlays.ts"],"names":[],"mappings":"AAkBA,KAAK,iBAAiB,GAAG;IACvB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,gBAAgB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IAClC,UAAU,CAAC,EACP,KAAK,CAAC;QACJ,IAAI,CAAC,EAAE,GAAG,CAAC;QACX,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACrB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAClC,CAAC,GACF,IAAI,CAAC;IACT,SAAS,CAAC,EAAE;QACV,SAAS,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,IAAI,CAAC;QAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;QAC3C,SAAS,CAAC,EAAE,GAAG,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;QAC1B,SAAS,CAAC,EAAE,GAAG,CAAC;QAChB,UAAU,CAAC,EAAE,GAAG,CAAC;KAClB,GAAG,IAAI,CAAC;CACV,CAAC;AAEF,MAAM,MAAM,qBAAqB,CAAC,MAAM,GAAG,OAAO,IAAI;IACpD,MAAM,EAAE,iBAAiB,CAAC;IAC1B,2EAA2E;IAC3E,MAAM,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;IACtC;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,GAAG,IAAI,CAAC;IAC/C,8DAA8D;IAC9D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,EAAE,GAAG,IAAI,GAAG,SAAS,CAAC;CAC/D,CAAC;AAIF;;;GAGG;AACH,eAAO,MAAM,yBAAyB,GAAI,MAAM,GAAG,OAAO,EAAE,gFAQzD,qBAAqB,CAAC,MAAM,CAAC,KAAG,GAAG,EA4HrC,CAAC"}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Trendline and error-bar overlay series for the echarts renderer.
3
+ *
4
+ * One data series may carry trendlines (`<c:trendline>`) and error bars
5
+ * (`<c:errBars>`); both are drawn as extra echarts series appended after
6
+ * the data series. The same builder serves every cartesian layout:
7
+ *
8
+ * - category charts (line / column / area): the point index is the
9
+ * category coordinate, values sit on the value axis;
10
+ * - horizontal bars: the same, with the axes swapped — the category
11
+ * index is the y coordinate (rowsncolumns/spreadsheet#1061 drew the
12
+ * overlays on the swapped axes, outside the plot);
13
+ * - scatter / bubble: real (x, y) coordinates per point, and the
14
+ * trendline is a regression of Y on X (#1067).
15
+ */
16
+ import { computeErrorBars, resolveCustomErrorBarValues } from "./error-bars";
17
+ import { computeTrendline } from "./trendline";
18
+ const ERROR_BAR_CAP_HALF = 5;
19
+ /**
20
+ * Build the overlay series for one data series. Returns an empty array
21
+ * when the series has neither trendlines nor error bars.
22
+ */
23
+ export const buildEchartsOverlaySeries = ({ series, values, xValues, horizontal = false, dataLabel, seriesColor, resolveRange, }) => {
24
+ if (values.length === 0)
25
+ return [];
26
+ const overlays = [];
27
+ const isXY = Array.isArray(xValues);
28
+ const useSecondary = series.useSecondaryAxis ? { yAxisIndex: 1 } : {};
29
+ // Trendlines — one overlay line series per spec.
30
+ for (const tl of series.trendlines ?? []) {
31
+ const result = computeTrendline(values, {
32
+ type: tl.type,
33
+ order: tl.order,
34
+ period: tl.period,
35
+ forecastForward: tl.forecastForward,
36
+ forecastBackward: tl.forecastBackward,
37
+ }, isXY ? xValues : undefined);
38
+ if (!result || result.points.length < 2)
39
+ continue;
40
+ const tlName = tl.name ??
41
+ `${dataLabel ? dataLabel + " " : ""}${tl.type ?? "linear"} trend`;
42
+ overlays.push({
43
+ type: "line",
44
+ name: tlName,
45
+ // On a category chart the index x overlays the category axis even
46
+ // when forecasting extends beyond the data range; a horizontal bar
47
+ // keeps its categories on y.
48
+ data: result.points.map((p) => (horizontal ? [p.y, p.x] : [p.x, p.y])),
49
+ showSymbol: false,
50
+ smooth: tl.type === "poly",
51
+ lineStyle: {
52
+ color: tl.color ?? seriesColor,
53
+ type: "dashed",
54
+ width: 2,
55
+ },
56
+ itemStyle: { color: tl.color ?? seriesColor },
57
+ tooltip: { show: false },
58
+ z: 5,
59
+ ...useSecondary,
60
+ });
61
+ }
62
+ // Error bars — a custom series of whiskers. valueType "custom" reads
63
+ // per-point magnitudes from the spec's plus/minus ranges, resolved
64
+ // through the same extractor the series values use.
65
+ if (series.errorBars) {
66
+ const direction = series.errorBars.direction ?? "y";
67
+ // An X error bar on an XY chart sizes from the X values (a percentage
68
+ // or deviation of X); a category chart has only value-axis bars, so a
69
+ // horizontal bar's whisker runs along x whatever the direction says.
70
+ const alongX = isXY ? direction === "x" : horizontal || direction === "x";
71
+ const magnitudeSource = isXY && alongX ? xValues : values;
72
+ const magnitudes = computeErrorBars(magnitudeSource, resolveCustomErrorBarValues(series.errorBars, resolveRange));
73
+ if (magnitudes) {
74
+ const noEndCap = series.errorBars.noEndCap ?? false;
75
+ const errorData = [];
76
+ for (let i = 0; i < values.length; i++) {
77
+ const v = values[i];
78
+ const y = typeof v === "number" && Number.isFinite(v) ? v : null;
79
+ const x = isXY ? xValues?.[i] : i;
80
+ if (y === null || typeof x !== "number" || !Number.isFinite(x)) {
81
+ continue;
82
+ }
83
+ // [a, b] is the point's (x, y) data coordinate; a horizontal bar
84
+ // keeps its value on x and its category on y.
85
+ const [a, b] = !isXY && horizontal ? [y, x] : [x, y];
86
+ errorData.push([a, b, magnitudes.plus[i] ?? 0, magnitudes.minus[i] ?? 0]);
87
+ }
88
+ overlays.push({
89
+ type: "custom",
90
+ name: `${dataLabel ? dataLabel + " " : ""}error`,
91
+ data: errorData,
92
+ z: 6,
93
+ tooltip: { show: false },
94
+ ...useSecondary,
95
+ renderItem: (_params, api) => {
96
+ const a = api.value(0);
97
+ const b = api.value(1);
98
+ const plus = api.value(2);
99
+ const minus = api.value(3);
100
+ const center = api.coord([a, b]);
101
+ const hi = alongX ? api.coord([a + plus, b]) : api.coord([a, b + plus]);
102
+ const lo = alongX ? api.coord([a - minus, b]) : api.coord([a, b - minus]);
103
+ const stroke = { stroke: seriesColor, lineWidth: 1 };
104
+ const children = [
105
+ {
106
+ type: "line",
107
+ shape: alongX
108
+ ? { x1: lo[0], y1: center[1], x2: hi[0], y2: center[1] }
109
+ : { x1: center[0], y1: lo[1], x2: center[0], y2: hi[1] },
110
+ style: stroke,
111
+ },
112
+ ];
113
+ if (!noEndCap) {
114
+ const cap = ERROR_BAR_CAP_HALF;
115
+ children.push({
116
+ type: "line",
117
+ shape: alongX
118
+ ? { x1: hi[0], y1: center[1] - cap, x2: hi[0], y2: center[1] + cap }
119
+ : { x1: center[0] - cap, y1: hi[1], x2: center[0] + cap, y2: hi[1] },
120
+ style: stroke,
121
+ }, {
122
+ type: "line",
123
+ shape: alongX
124
+ ? { x1: lo[0], y1: center[1] - cap, x2: lo[0], y2: center[1] + cap }
125
+ : { x1: center[0] - cap, y1: lo[1], x2: center[0] + cap, y2: lo[1] },
126
+ style: stroke,
127
+ });
128
+ }
129
+ return { type: "group", children };
130
+ },
131
+ });
132
+ }
133
+ }
134
+ return overlays;
135
+ };
@@ -1,12 +1,21 @@
1
1
  import type { ChartSpec, SheetRange, TreemapChart } from "@rowsncolumns/spreadsheet";
2
2
  type Extractor = (range: SheetRange, hiddenStrategy?: ChartSpec["hiddenDimensionStrategy"]) => (string | number | boolean | null | undefined)[];
3
3
  export type HierarchyRow = {
4
+ /** The node this row defines — the innermost filled level cell. */
4
5
  label: string;
6
+ /** The immediate parent (the last entry of `path`), null for a root. */
5
7
  parent: string | null;
6
8
  value: number | null;
9
+ /**
10
+ * The level names above the node, top level first, after Excel's
11
+ * fill-down of blank level cells. Absent on a legacy adjacency row
12
+ * (`parent` alone, naming another row's label).
13
+ */
14
+ path?: string[];
7
15
  };
8
16
  export type HierarchyNode = {
9
17
  name: string;
18
+ /** Unique within the tree: the node's path joined with "/". */
10
19
  id?: string;
11
20
  value?: number | null;
12
21
  children?: HierarchyNode[];
@@ -19,10 +28,23 @@ type ExtractOptions = {
19
28
  export declare const getHierarchyHeaderTitle: (spec: TreemapChart, options: ExtractOptions) => string | undefined;
20
29
  export declare const extractHierarchyRows: (spec: TreemapChart, options: ExtractOptions) => HierarchyRow[];
21
30
  export declare const buildHierarchyTree: (rows: HierarchyRow[]) => HierarchyNode[];
22
- export declare const toPlotlyHierarchy: (rows: HierarchyRow[]) => {
31
+ export type PlotlyHierarchy = {
32
+ /** Node names, as drawn. */
23
33
  labels: string[];
34
+ /** Unique node ids (the path joined with "/"); `parents` reference these. */
35
+ ids: string[];
36
+ /** Parent id per node, "" for a root. */
24
37
  parents: string[];
25
38
  values: number[];
39
+ /** Depth per node, 0 for a root. */
40
+ depths: number[];
26
41
  };
42
+ export declare const toPlotlyHierarchy: (rows: HierarchyRow[]) => PlotlyHierarchy;
43
+ /**
44
+ * One solid colour per top-level category, inherited by every descendant
45
+ * (Excel colours a treemap / sunburst by its first level), in tree order.
46
+ * Returns the colour per node of `toPlotlyHierarchy`'s flat arrays.
47
+ */
48
+ export declare const hierarchyNodeColors: (hierarchy: Pick<PlotlyHierarchy, "ids" | "parents">, palette: string[]) => string[];
27
49
  export {};
28
50
  //# sourceMappingURL=hierarchy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hierarchy.d.ts","sourceRoot":"","sources":["../../../utils/hierarchy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,SAAS,EACT,UAAU,EACV,YAAY,EACb,MAAM,2BAA2B,CAAC;AAEnC,KAAK,SAAS,GAAG,CACf,KAAK,EAAE,UAAU,EACjB,cAAc,CAAC,EAAE,SAAS,CAAC,yBAAyB,CAAC,KAClD,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;AAEtD,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,wBAAwB,EAAE,SAAS,CAAC;IACpC,wBAAwB,EAAE,SAAS,CAAC;IACpC,uBAAuB,CAAC,EAAE,SAAS,CAAC,yBAAyB,CAAC,CAAC;CAChE,CAAC;AA+CF,eAAO,MAAM,uBAAuB,GAClC,MAAM,YAAY,EAClB,SAAS,cAAc,KACtB,MAAM,GAAG,SAgBX,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,MAAM,YAAY,EAClB,SAAS,cAAc,KACtB,YAAY,EAyCd,CAAC;AAyBF,eAAO,MAAM,kBAAkB,GAAI,MAAM,YAAY,EAAE,KAAG,aAAa,EAoDtE,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,MAAM,YAAY,EAAE;;;;CAoBrD,CAAC"}
1
+ {"version":3,"file":"hierarchy.d.ts","sourceRoot":"","sources":["../../../utils/hierarchy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,SAAS,EACT,UAAU,EACV,YAAY,EACb,MAAM,2BAA2B,CAAC;AAEnC,KAAK,SAAS,GAAG,CACf,KAAK,EAAE,UAAU,EACjB,cAAc,CAAC,EAAE,SAAS,CAAC,yBAAyB,CAAC,KAClD,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;AAEtD,MAAM,MAAM,YAAY,GAAG;IACzB,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,+DAA+D;IAC/D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,aAAa,EAAE,CAAC;CAC5B,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,wBAAwB,EAAE,SAAS,CAAC;IACpC,wBAAwB,EAAE,SAAS,CAAC;IACpC,uBAAuB,CAAC,EAAE,SAAS,CAAC,yBAAyB,CAAC,CAAC;CAChE,CAAC;AA0CF,eAAO,MAAM,uBAAuB,GAClC,MAAM,YAAY,EAClB,SAAS,cAAc,KACtB,MAAM,GAAG,SAgBX,CAAC;AAyCF,eAAO,MAAM,oBAAoB,GAC/B,MAAM,YAAY,EAClB,SAAS,cAAc,KACtB,YAAY,EAiFd,CAAC;AAiDF,eAAO,MAAM,kBAAkB,GAAI,MAAM,YAAY,EAAE,KAAG,aAAa,EAmDtE,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,4BAA4B;IAC5B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,6EAA6E;IAC7E,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,yCAAyC;IACzC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,oCAAoC;IACpC,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,MAAM,YAAY,EAAE,KAAG,eAwBxD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAC9B,WAAW,IAAI,CAAC,eAAe,EAAE,KAAK,GAAG,SAAS,CAAC,EACnD,SAAS,MAAM,EAAE,KAChB,MAAM,EAYR,CAAC"}
@@ -16,10 +16,6 @@ const toLabelString = (value) => {
16
16
  return "";
17
17
  return `${value}`.trim();
18
18
  };
19
- const toParentLabel = (value) => {
20
- const label = toLabelString(value);
21
- return label === "" ? null : label;
22
- };
23
19
  const toNumericValue = (value) => {
24
20
  if (typeof value === "number" && Number.isFinite(value)) {
25
21
  return value;
@@ -50,71 +46,178 @@ export const getHierarchyHeaderTitle = (spec, options) => {
50
46
  }
51
47
  return undefined;
52
48
  };
49
+ /**
50
+ * Lay the rows out as Excel does: one column per level (`parentLabels`
51
+ * entries, top level first) and the label column as the innermost level.
52
+ * Returns, per row, the filled level cells up to the row's leaf.
53
+ *
54
+ * A blank level cell BEFORE the row's first filled cell means "same as the
55
+ * row above" (Excel writes a parent only on the first row of its group).
56
+ * A blank AFTER the last filled cell ends the branch there — that cell is
57
+ * the row's leaf, so a shorter branch (`Services | Training | <blank>`)
58
+ * is a leaf of the second level, not `Services > Training > <previous
59
+ * item>` as plain per-column fill-down would make it.
60
+ */
61
+ const layoutHierarchyLevels = (levelColumns, rowCount) => {
62
+ const filled = [];
63
+ let above = levelColumns.map(() => "");
64
+ for (let row = 0; row < rowCount; row++) {
65
+ const cells = levelColumns.map((column) => column[row] ?? "");
66
+ const firstFilled = cells.findIndex((cell) => cell !== "");
67
+ if (firstFilled === -1) {
68
+ // An entirely blank row defines nothing (and inherits nothing).
69
+ filled.push([]);
70
+ continue;
71
+ }
72
+ const effective = cells.map((cell, level) => level < firstFilled ? above[level] ?? "" : cell);
73
+ let lastFilled = effective.length - 1;
74
+ while (lastFilled >= 0 && effective[lastFilled] === "")
75
+ lastFilled--;
76
+ // The row's own levels end at its last filled cell; the cells past it
77
+ // stay blank for the rows below (they inherit up to the leaf only).
78
+ above = effective.map((cell, level) => (level <= lastFilled ? cell : ""));
79
+ filled.push(effective.slice(0, lastFilled + 1).filter((cell) => cell !== ""));
80
+ }
81
+ return filled;
82
+ };
53
83
  export const extractHierarchyRows = (spec, options) => {
54
84
  const { getDomainValuesFromRange, getSeriesValuesFromRange } = options;
55
85
  const hiddenStrategy = options.hiddenDimensionStrategy;
56
86
  const skipHeaders = spec.useFirstRowAsHeaders !== false;
57
87
  const labels = flattenChartData(spec.labels, getDomainValuesFromRange, hiddenStrategy, skipHeaders).map(toLabelString);
58
- const parents = flattenChartData(spec.parentLabels, getDomainValuesFromRange, hiddenStrategy, skipHeaders);
88
+ // Each `parentLabels` entry is one hierarchy level (top level first).
89
+ const parentLevels = (spec.parentLabels ?? []).map((entry) => flattenChartData([entry], getDomainValuesFromRange, hiddenStrategy, skipHeaders).map(toLabelString));
59
90
  const sizes = flattenChartData(spec.sizeData, getSeriesValuesFromRange, hiddenStrategy, skipHeaders);
91
+ // A single parent column whose values name other rows' labels is a flat
92
+ // adjacency list (Google Sheets' label / parent-label model): every row
93
+ // is its own node and a blank parent means a root. Excel's level layout
94
+ // never lists a level name in the label column.
95
+ const labelSet = new Set(labels.filter((label) => label !== ""));
96
+ const isAdjacencyList = parentLevels.length === 1 &&
97
+ parentLevels[0].some((parent) => parent !== "" && labelSet.has(parent));
60
98
  const rows = [];
61
- const seenLabels = new Set();
62
- for (let i = 0; i < labels.length; i++) {
63
- const label = labels[i];
64
- if (!label)
99
+ const seenPaths = new Set();
100
+ if (isAdjacencyList) {
101
+ const parents = parentLevels[0];
102
+ for (let i = 0; i < labels.length; i++) {
103
+ const label = labels[i];
104
+ if (!label)
105
+ continue;
106
+ if (seenPaths.has(label)) {
107
+ throw new Error(`Duplicate hierarchy label detected: ${label}`);
108
+ }
109
+ seenPaths.add(label);
110
+ const parent = parents[i] ?? "";
111
+ rows.push({
112
+ label,
113
+ parent: parent === "" ? null : parent,
114
+ value: toNumericValue(sizes[i]),
115
+ });
116
+ }
117
+ return rows;
118
+ }
119
+ const rowCount = Math.max(labels.length, ...parentLevels.map((level) => level.length));
120
+ const levels = layoutHierarchyLevels([...parentLevels, labels], rowCount);
121
+ for (let i = 0; i < rowCount; i++) {
122
+ const cells = levels[i];
123
+ if (!cells || cells.length === 0)
65
124
  continue;
66
- if (seenLabels.has(label)) {
125
+ const label = cells[cells.length - 1];
126
+ const path = cells.slice(0, -1);
127
+ const key = [...path, label].join("\u0000");
128
+ if (seenPaths.has(key)) {
67
129
  throw new Error(`Duplicate hierarchy label detected: ${label}`);
68
130
  }
69
- seenLabels.add(label);
131
+ seenPaths.add(key);
70
132
  rows.push({
71
133
  label,
72
- parent: toParentLabel(parents[i]),
134
+ parent: path.length ? path[path.length - 1] : null,
73
135
  value: toNumericValue(sizes[i]),
136
+ path,
74
137
  });
75
138
  }
76
139
  return rows;
77
140
  };
78
- const getOrCreateNode = (nodes, name) => {
79
- const existing = nodes.get(name);
80
- if (existing)
81
- return existing;
82
- const node = {
83
- name,
84
- value: 0,
85
- parent: null,
86
- children: new Set(),
141
+ const PATH_SEPARATOR = "/";
142
+ /**
143
+ * The full path of every row, top level first, ending with the row's own
144
+ * label. A row carrying `path` (Excel's level layout) is placed under it;
145
+ * a legacy adjacency row names its parent, which is another row's label,
146
+ * so its path is that row's path plus itself (chained up to a root).
147
+ */
148
+ const resolveRowPaths = (rows) => {
149
+ const byLabel = new Map();
150
+ rows.forEach((row) => {
151
+ if (row.label && !row.path && !byLabel.has(row.label)) {
152
+ byLabel.set(row.label, row);
153
+ }
154
+ });
155
+ const memo = new Map();
156
+ const resolve = (row, visiting) => {
157
+ const cached = memo.get(row);
158
+ if (cached)
159
+ return cached;
160
+ let path;
161
+ if (row.path) {
162
+ path = [...row.path, row.label];
163
+ }
164
+ else {
165
+ const parentName = row.parent && row.parent !== row.label ? row.parent : null;
166
+ const parentRow = parentName ? byLabel.get(parentName) : undefined;
167
+ if (parentRow && parentRow !== row && !visiting.has(parentRow)) {
168
+ visiting.add(row);
169
+ path = [...resolve(parentRow, visiting), row.label];
170
+ visiting.delete(row);
171
+ }
172
+ else {
173
+ path = parentName ? [parentName, row.label] : [row.label];
174
+ }
175
+ }
176
+ memo.set(row, path);
177
+ return path;
87
178
  };
88
- nodes.set(name, node);
89
- return node;
179
+ return rows.map((row) => resolve(row, new Set()));
90
180
  };
91
181
  export const buildHierarchyTree = (rows) => {
92
- const nodes = new Map();
93
- rows.forEach((row) => {
182
+ const roots = new Map();
183
+ const nodeAt = (path) => {
184
+ let level = roots;
185
+ let node;
186
+ path.forEach((name, depth) => {
187
+ const existing = level.get(name);
188
+ if (existing) {
189
+ node = existing;
190
+ }
191
+ else {
192
+ node = {
193
+ name,
194
+ id: path.slice(0, depth + 1).join(PATH_SEPARATOR),
195
+ value: 0,
196
+ children: new Map(),
197
+ };
198
+ level.set(name, node);
199
+ }
200
+ level = node.children;
201
+ });
202
+ return node;
203
+ };
204
+ const paths = resolveRowPaths(rows);
205
+ rows.forEach((row, index) => {
94
206
  if (!row.label)
95
207
  return;
96
- const labelNode = getOrCreateNode(nodes, row.label);
97
- const numericValue = row.value ?? 0;
98
- labelNode.value += numericValue;
99
- const parentName = row.parent && row.parent !== row.label ? row.parent : null;
100
- if (parentName) {
101
- const parentNode = getOrCreateNode(nodes, parentName);
102
- parentNode.children.add(row.label);
103
- if (labelNode.parent == null) {
104
- labelNode.parent = parentName;
105
- }
106
- }
208
+ const node = nodeAt(paths[index]);
209
+ node.value += row.value ?? 0;
107
210
  });
108
211
  const buildNode = (internal) => {
109
212
  const children = internal.children.size > 0
110
- ? Array.from(internal.children).map((childName) => buildNode(getOrCreateNode(nodes, childName)))
213
+ ? Array.from(internal.children.values()).map(buildNode)
111
214
  : undefined;
112
215
  const value = children && children.length > 0
113
216
  ? children.reduce((sum, child) => sum + (child.value ?? 0), 0)
114
217
  : internal.value;
115
218
  const node = {
116
219
  name: internal.name,
117
- id: internal.name,
220
+ id: internal.id,
118
221
  value,
119
222
  };
120
223
  if (children && children.length > 0) {
@@ -122,29 +225,44 @@ export const buildHierarchyTree = (rows) => {
122
225
  }
123
226
  return node;
124
227
  };
125
- const roots = [];
126
- nodes.forEach((internal) => {
127
- if (internal.parent == null) {
128
- roots.push(buildNode(internal));
129
- }
130
- });
131
- return roots;
228
+ return Array.from(roots.values()).map(buildNode);
132
229
  };
133
230
  export const toPlotlyHierarchy = (rows) => {
134
231
  const tree = buildHierarchyTree(rows);
135
232
  const labels = [];
233
+ const ids = [];
136
234
  const parents = [];
137
235
  const values = [];
138
- const traverse = (nodes, parentName) => {
236
+ const depths = [];
237
+ const traverse = (nodes, parentId, depth) => {
139
238
  nodes.forEach((node) => {
140
239
  labels.push(node.name);
141
- parents.push(parentName ?? "");
240
+ ids.push(node.id ?? node.name);
241
+ parents.push(parentId);
142
242
  values.push(node.value ?? 0);
243
+ depths.push(depth);
143
244
  if (node.children) {
144
- traverse(node.children, node.name);
245
+ traverse(node.children, node.id ?? node.name, depth + 1);
145
246
  }
146
247
  });
147
248
  };
148
- traverse(tree, null);
149
- return { labels, parents, values };
249
+ traverse(tree, "", 0);
250
+ return { labels, ids, parents, values, depths };
251
+ };
252
+ /**
253
+ * One solid colour per top-level category, inherited by every descendant
254
+ * (Excel colours a treemap / sunburst by its first level), in tree order.
255
+ * Returns the colour per node of `toPlotlyHierarchy`'s flat arrays.
256
+ */
257
+ export const hierarchyNodeColors = (hierarchy, palette) => {
258
+ const colorById = new Map();
259
+ let rootIndex = 0;
260
+ return hierarchy.ids.map((id, index) => {
261
+ const parentId = hierarchy.parents[index];
262
+ const color = parentId === ""
263
+ ? palette[rootIndex++ % Math.max(1, palette.length)] ?? ""
264
+ : colorById.get(parentId) ?? "";
265
+ colorById.set(id, color);
266
+ return color;
267
+ });
150
268
  };
@@ -1 +1 @@
1
- {"version":3,"file":"overrides.d.ts","sourceRoot":"","sources":["../../../utils/overrides.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAWrD,oEAAoE;AACpE,eAAO,MAAM,4BAA4B,GAAI,KAAK,GAAG,oCASpD,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,qBAAqB,EAC7B,MAAM,GAAG,KACR,qBA6CF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,GAAG,EACT,MAAM,GAAG,EACT,sBAAsB,MAAM,EAC5B,SAAS,MAAM,QA4BhB,CAAC;AAgBF,eAAO,MAAM,kBAAkB,GAAI,MAAM,GAAG,EAAE,SAAS,MAAM,MAAM,QAAQ,GAAG,WAiC7E,CAAC;AAgCF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,CAAC,EACvC,YAAY,CAAC,GAAG,IAAI,GAAG,SAAS,EAChC,QAAQ;IAAE,UAAU,CAAC,EAAE,CAAC,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,KACnD,CAAC,GAAG,IAAI,GAAG,SAA6C,CAAC;AAE5D,eAAO,MAAM,sBAAsB,GACjC,MAAM,GAAG,EACT,aAAa,MAAM,EACnB,SAAS,MAAM;;;iCA/E0D,GAAG;aA4F7E,CAAC"}
1
+ {"version":3,"file":"overrides.d.ts","sourceRoot":"","sources":["../../../utils/overrides.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAWrD,oEAAoE;AACpE,eAAO,MAAM,4BAA4B,GAAI,KAAK,GAAG,oCASpD,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,qBAAqB,EAC7B,MAAM,GAAG,KACR,qBA6CF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,GAAG,EACT,MAAM,GAAG,EACT,sBAAsB,MAAM,EAC5B,SAAS,MAAM,QA4BhB,CAAC;AAgBF,eAAO,MAAM,kBAAkB,GAAI,MAAM,GAAG,EAAE,SAAS,MAAM,MAAM,QAAQ,GAAG,WA+C7E,CAAC;AAgCF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,GAAI,CAAC,EACvC,YAAY,CAAC,GAAG,IAAI,GAAG,SAAS,EAChC,QAAQ;IAAE,UAAU,CAAC,EAAE,CAAC,GAAG,IAAI,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,KACnD,CAAC,GAAG,IAAI,GAAG,SAA6C,CAAC;AAE5D,eAAO,MAAM,sBAAsB,GACjC,MAAM,GAAG,EACT,aAAa,MAAM,EACnB,SAAS,MAAM;;;iCA7F0D,GAAG;aA0G7E,CAAC"}
@@ -132,7 +132,11 @@ export const dataLabelFormatter = (opts, locale) => (params) => {
132
132
  !opts.showSeriesName
133
133
  ? true
134
134
  : !!opts.showValue;
135
- if (showVal && params.value !== undefined) {
135
+ // A scatter / bubble point's value is its [x, y, size] tuple: the label's
136
+ // value is Y and the bubble size is the third entry (#1068).
137
+ const isPointTuple = Array.isArray(params.value);
138
+ const pointValue = isPointTuple ? params.value[1] : params.value;
139
+ if (showVal && pointValue !== undefined) {
136
140
  // PERCENT_STACKED stores the raw value on the data object so the
137
141
  // label can still surface the dollar amount instead of the
138
142
  // normalized percentage. Plain stacked / clustered have just a
@@ -140,7 +144,10 @@ export const dataLabelFormatter = (opts, locale) => (params) => {
140
144
  const originalValue = typeof params.data === "object" && params.data
141
145
  ? params.data.originalValue
142
146
  : undefined;
143
- parts.push(formatLabelValue(params.value, originalValue, opts.numberFormatPattern, locale));
147
+ parts.push(formatLabelValue(pointValue, originalValue, opts.numberFormatPattern, locale));
148
+ }
149
+ if (opts.showBubbleSize && isPointTuple && params.value.length > 2) {
150
+ parts.push(formatLabelValue(params.value[2], undefined, opts.numberFormatPattern, locale));
144
151
  }
145
152
  return parts.join(": ");
146
153
  };
@@ -43,6 +43,12 @@ export type TrendlineResult = {
43
43
  * @param values Raw y-values; the 1-based category position is used as x
44
44
  * for the fit, the 0-based index is reported in the points.
45
45
  * @param spec Trendline parameters (type/order/period/forecast).
46
+ * @param xValues Scatter / bubble only — the series' real X values, index-
47
+ * aligned with `values`. The fit then runs on (x, y) pairs
48
+ * (a regression of Y on X, what Excel draws on an XY chart;
49
+ * the point index is wrong for unevenly spaced X), the
50
+ * forecast extends the X range by that many X units, and the
51
+ * points carry real X coordinates sampled across the range.
46
52
  */
47
53
  export declare const computeTrendline: (values: (number | null | undefined)[], spec?: {
48
54
  type?: TrendlineType | null;
@@ -50,5 +56,5 @@ export declare const computeTrendline: (values: (number | null | undefined)[], s
50
56
  period?: number | null;
51
57
  forecastForward?: number | null;
52
58
  forecastBackward?: number | null;
53
- }) => TrendlineResult | null;
59
+ }, xValues?: (number | null | undefined)[] | null) => TrendlineResult | null;
54
60
  //# sourceMappingURL=trendline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"trendline.d.ts","sourceRoot":"","sources":["../../../utils/trendline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,MAAM,aAAa,GACrB,QAAQ,GACR,KAAK,GACL,KAAK,GACL,MAAM,GACN,OAAO,GACP,WAAW,CAAC;AAEhB,6FAA6F;AAC7F,MAAM,MAAM,cAAc,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtD,MAAM,MAAM,eAAe,GAAG;IAC5B,sEAAsE;IACtE,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAsJF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,EACrC,OAAM;IACJ,IAAI,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,KACL,eAAe,GAAG,IAiGpB,CAAC"}
1
+ {"version":3,"file":"trendline.d.ts","sourceRoot":"","sources":["../../../utils/trendline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,MAAM,aAAa,GACrB,QAAQ,GACR,KAAK,GACL,KAAK,GACL,MAAM,GACN,OAAO,GACP,WAAW,CAAC;AAEhB,6FAA6F;AAC7F,MAAM,MAAM,cAAc,GAAG;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtD,MAAM,MAAM,eAAe,GAAG;IAC5B,sEAAsE;IACtE,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,uEAAuE;IACvE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAkKF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,EACrC,OAAM;IACJ,IAAI,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,EACN,UAAU,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,GAAG,IAAI,KAC7C,eAAe,GAAG,IA+HpB,CAAC"}