@rowsncolumns/charts 28.1.7 → 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 (61) hide show
  1. package/dist/cjs/basic-chart/basic-chart.d.ts.map +1 -1
  2. package/dist/cjs/basic-chart/basic-chart.js +525 -419
  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 +351 -192
  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/doughnut.d.ts +19 -0
  14. package/dist/cjs/utils/doughnut.d.ts.map +1 -0
  15. package/dist/cjs/utils/doughnut.js +28 -0
  16. package/dist/cjs/utils/echarts-overlays.d.ts +46 -0
  17. package/dist/cjs/utils/echarts-overlays.d.ts.map +1 -0
  18. package/dist/cjs/utils/echarts-overlays.js +134 -0
  19. package/dist/cjs/utils/hierarchy.d.ts +23 -1
  20. package/dist/cjs/utils/hierarchy.d.ts.map +1 -1
  21. package/dist/cjs/utils/hierarchy.js +179 -55
  22. package/dist/cjs/utils/overrides.d.ts +9 -0
  23. package/dist/cjs/utils/overrides.d.ts.map +1 -1
  24. package/dist/cjs/utils/overrides.js +18 -3
  25. package/dist/cjs/utils/radar.d.ts +31 -5
  26. package/dist/cjs/utils/radar.d.ts.map +1 -1
  27. package/dist/cjs/utils/radar.js +67 -27
  28. package/dist/cjs/utils/trendline.d.ts +7 -1
  29. package/dist/cjs/utils/trendline.d.ts.map +1 -1
  30. package/dist/cjs/utils/trendline.js +82 -18
  31. package/dist/esm/basic-chart/basic-chart.d.ts.map +1 -1
  32. package/dist/esm/basic-chart/basic-chart.js +376 -248
  33. package/dist/esm/basic-chart/helpers.d.ts.map +1 -1
  34. package/dist/esm/basic-chart/helpers.js +2 -0
  35. package/dist/esm/plotly/basic-chart/basic-chart.d.ts.map +1 -1
  36. package/dist/esm/plotly/basic-chart/basic-chart.js +279 -69
  37. package/dist/esm/plotly/basic-chart/helpers.d.ts.map +1 -1
  38. package/dist/esm/plotly/basic-chart/helpers.js +2 -0
  39. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  40. package/dist/esm/utils/bubble.d.ts +34 -1
  41. package/dist/esm/utils/bubble.d.ts.map +1 -1
  42. package/dist/esm/utils/bubble.js +35 -11
  43. package/dist/esm/utils/doughnut.d.ts +19 -0
  44. package/dist/esm/utils/doughnut.d.ts.map +1 -0
  45. package/dist/esm/utils/doughnut.js +21 -0
  46. package/dist/esm/utils/echarts-overlays.d.ts +46 -0
  47. package/dist/esm/utils/echarts-overlays.d.ts.map +1 -0
  48. package/dist/esm/utils/echarts-overlays.js +135 -0
  49. package/dist/esm/utils/hierarchy.d.ts +23 -1
  50. package/dist/esm/utils/hierarchy.d.ts.map +1 -1
  51. package/dist/esm/utils/hierarchy.js +168 -50
  52. package/dist/esm/utils/overrides.d.ts +9 -0
  53. package/dist/esm/utils/overrides.d.ts.map +1 -1
  54. package/dist/esm/utils/overrides.js +16 -2
  55. package/dist/esm/utils/radar.d.ts +31 -5
  56. package/dist/esm/utils/radar.d.ts.map +1 -1
  57. package/dist/esm/utils/radar.js +44 -16
  58. package/dist/esm/utils/trendline.d.ts +7 -1
  59. package/dist/esm/utils/trendline.d.ts.map +1 -1
  60. package/dist/esm/utils/trendline.js +67 -14
  61. package/package.json +4 -4
@@ -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
  };
@@ -9,6 +9,15 @@ export declare const applyLegendOverrides: (legend: LegendComponentOption, opts:
9
9
  */
10
10
  export declare const applyAxisOverrides: (axis: any, opts: any, numberFormatPattern?: string, locale?: string) => any;
11
11
  export declare const dataLabelFormatter: (opts: any, locale?: string) => (params: any) => string;
12
+ /**
13
+ * The data-label options one series renders with: its own
14
+ * `ChartData.dataLabels` when it has them (OOXML's `<c:ser><c:dLbls>`
15
+ * override — `{ show: false }` turns a labelled chart's labels off for
16
+ * that series alone), else the chart-level `ChartSpec.dataLabels`.
17
+ */
18
+ export declare const resolveSeriesDataLabels: <T>(chartLevel: T | null | undefined, series: {
19
+ dataLabels?: T | null;
20
+ } | null | undefined) => T | null | undefined;
12
21
  export declare const buildSeriesLabelOption: (opts: any, seriesType?: string, locale?: string) => {
13
22
  readonly show: true;
14
23
  readonly position: any;
@@ -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,eAAO,MAAM,sBAAsB,GACjC,MAAM,GAAG,EACT,aAAa,MAAM,EACnB,SAAS,MAAM;;;iCApE0D,GAAG;aAiF7E,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
  };
@@ -173,6 +180,13 @@ const ECHARTS_POINT_DLBL_POS = {
173
180
  right: "right",
174
181
  };
175
182
  const POINT_SERIES_TYPES = new Set(["line", "scatter", "effectScatter"]);
183
+ /**
184
+ * The data-label options one series renders with: its own
185
+ * `ChartData.dataLabels` when it has them (OOXML's `<c:ser><c:dLbls>`
186
+ * override — `{ show: false }` turns a labelled chart's labels off for
187
+ * that series alone), else the chart-level `ChartSpec.dataLabels`.
188
+ */
189
+ export const resolveSeriesDataLabels = (chartLevel, series) => series?.dataLabels ?? chartLevel;
176
190
  export const buildSeriesLabelOption = (opts, seriesType, locale) => {
177
191
  if (!opts?.show)
178
192
  return undefined;
@@ -16,15 +16,41 @@ export type RadarSeriesInput = {
16
16
  };
17
17
  export type RadarIndicator = {
18
18
  name: string;
19
+ min: number;
19
20
  max: number;
20
21
  };
22
+ export type RadarAxisScale = {
23
+ min: number;
24
+ max: number;
25
+ };
26
+ /** The value-axis options a radar's common scale reads (`ChartAxisOptions`). */
27
+ export type RadarAxisOptions = {
28
+ min?: number | null;
29
+ max?: number | null;
30
+ visible?: boolean | null;
31
+ } | null | undefined;
32
+ /**
33
+ * The next "nice" axis bound at or above `value` — the top tick a value
34
+ * axis would auto-scale to: the step is 1 / 2 / 5 × a power of ten sized
35
+ * for about five divisions, and the bound is the first multiple of that
36
+ * step at or above the value (9 → 10, 33 → 40, 0.7 → 0.8).
37
+ */
38
+ export declare const niceAxisCeiling: (value: number) => number;
39
+ /**
40
+ * One value scale shared by every radar axis, as Excel draws it: `max` is
41
+ * the spec's `valueAxisOptions.max` when set, else the largest value across
42
+ * all series and categories rounded up to a nice bound (never below 1);
43
+ * `min` is `valueAxisOptions.min` when set (and below `max`), else 0.
44
+ */
45
+ export declare const radarValueScale: (series: RadarSeriesInput[], axisOptions?: RadarAxisOptions) => RadarAxisScale;
21
46
  /**
22
- * Build echarts radar indicators from category labels and series.
23
- * The `max` per indicator is the largest value seen across all series at
24
- * that category (rounded up a touch so the polygon doesn't touch the
25
- * rim). When every series is empty at an index, `max` defaults to 1.
47
+ * Build echarts radar indicators from category labels and series. Every
48
+ * indicator carries the SAME `min` / `max` — the common scale of
49
+ * {@link radarValueScale} — so values on different axes compare, and a
50
+ * single series draws the shape of its data rather than a regular polygon
51
+ * (rowsncolumns/spreadsheet#1066).
26
52
  */
27
- export declare const buildRadarIndicators: (categories: (string | number | boolean | null | undefined)[], series: RadarSeriesInput[]) => RadarIndicator[];
53
+ export declare const buildRadarIndicators: (categories: (string | number | boolean | null | undefined)[], series: RadarSeriesInput[], axisOptions?: RadarAxisOptions) => RadarIndicator[];
28
54
  /**
29
55
  * Shape series into echarts radar data: one entry per series with its
30
56
  * value vector aligned to the indicator order. Missing/non-numeric
@@ -1 +1 @@
1
- {"version":3,"file":"radar.d.ts","sourceRoot":"","sources":["../../../utils/radar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAQ3D;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAC/B,YAAY,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,EAC5D,QAAQ,gBAAgB,EAAE,KACzB,cAAc,EAahB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,gBAAgB,EAAE,EAC1B,gBAAgB,MAAM,KACrB,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAQzC,CAAC"}
1
+ {"version":3,"file":"radar.d.ts","sourceRoot":"","sources":["../../../utils/radar.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAQxE,MAAM,MAAM,cAAc,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1D,gFAAgF;AAChF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAC1B,GAAG,IAAI,GAAG,SAAS,CAAC;AAErB;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,KAAG,MAW/C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAC1B,QAAQ,gBAAgB,EAAE,EAC1B,cAAc,gBAAgB,KAC7B,cAaF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,GAC/B,YAAY,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,EAC5D,QAAQ,gBAAgB,EAAE,EAC1B,cAAc,gBAAgB,KAC7B,cAAc,EAGhB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,gBAAgB,EAAE,EAC1B,gBAAgB,MAAM,KACrB,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAQzC,CAAC"}
@@ -13,25 +13,53 @@
13
13
  const isFiniteNum = (v) => typeof v === "number" && Number.isFinite(v);
14
14
  const toNumber = (v) => isFiniteNum(v) ? v : 0;
15
15
  /**
16
- * Build echarts radar indicators from category labels and series.
17
- * The `max` per indicator is the largest value seen across all series at
18
- * that category (rounded up a touch so the polygon doesn't touch the
19
- * rim). When every series is empty at an index, `max` defaults to 1.
16
+ * The next "nice" axis bound at or above `value` — the top tick a value
17
+ * axis would auto-scale to: the step is 1 / 2 / 5 × a power of ten sized
18
+ * for about five divisions, and the bound is the first multiple of that
19
+ * step at or above the value (9 → 10, 33 → 40, 0.7 → 0.8).
20
20
  */
21
- export const buildRadarIndicators = (categories, series) => {
22
- return categories.map((label, i) => {
23
- let max = 0;
24
- for (const s of series) {
25
- const v = s.values[i];
21
+ export const niceAxisCeiling = (value) => {
22
+ if (!isFiniteNum(value) || value <= 0)
23
+ return 1;
24
+ const rough = value / 5;
25
+ const magnitude = 10 ** Math.floor(Math.log10(rough));
26
+ const residual = rough / magnitude;
27
+ const step = (residual <= 1 ? 1 : residual <= 2 ? 2 : residual <= 5 ? 5 : 10) *
28
+ magnitude;
29
+ const ceiling = Math.ceil(value / step - 1e-9) * step;
30
+ // Avoid 0.30000000000000004-style bounds from the float arithmetic.
31
+ return Number(ceiling.toPrecision(12));
32
+ };
33
+ /**
34
+ * One value scale shared by every radar axis, as Excel draws it: `max` is
35
+ * the spec's `valueAxisOptions.max` when set, else the largest value across
36
+ * all series and categories rounded up to a nice bound (never below 1);
37
+ * `min` is `valueAxisOptions.min` when set (and below `max`), else 0.
38
+ */
39
+ export const radarValueScale = (series, axisOptions) => {
40
+ let largest = 0;
41
+ for (const s of series) {
42
+ for (const v of s.values) {
26
43
  if (isFiniteNum(v))
27
- max = Math.max(max, v);
44
+ largest = Math.max(largest, v);
28
45
  }
29
- return {
30
- name: String(label ?? ""),
31
- // Pad by 10% so the outermost ring has breathing room; never 0.
32
- max: max > 0 ? max * 1.1 : 1,
33
- };
34
- });
46
+ }
47
+ const max = isFiniteNum(axisOptions?.max)
48
+ ? axisOptions.max
49
+ : niceAxisCeiling(largest);
50
+ const min = isFiniteNum(axisOptions?.min) && axisOptions.min < max ? axisOptions.min : 0;
51
+ return { min, max };
52
+ };
53
+ /**
54
+ * Build echarts radar indicators from category labels and series. Every
55
+ * indicator carries the SAME `min` / `max` — the common scale of
56
+ * {@link radarValueScale} — so values on different axes compare, and a
57
+ * single series draws the shape of its data rather than a regular polygon
58
+ * (rowsncolumns/spreadsheet#1066).
59
+ */
60
+ export const buildRadarIndicators = (categories, series, axisOptions) => {
61
+ const { min, max } = radarValueScale(series, axisOptions);
62
+ return categories.map((label) => ({ name: String(label ?? ""), min, max }));
35
63
  };
36
64
  /**
37
65
  * Shape series into echarts radar data: one entry per series with its
@@ -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"}
@@ -29,15 +29,25 @@ const isFiniteNum = (v) => typeof v === "number" && Number.isFinite(v);
29
29
  * position as x (the first category is x = 1, see the module comment).
30
30
  * Non-finite y-values are dropped (they cannot be fitted).
31
31
  */
32
- const toPairs = (values) => {
32
+ const toPairs = (values, xValues) => {
33
33
  const pairs = [];
34
34
  for (let i = 0; i < values.length; i++) {
35
35
  const y = values[i];
36
- if (isFiniteNum(y))
36
+ if (!isFiniteNum(y))
37
+ continue;
38
+ if (xValues) {
39
+ const x = xValues[i];
40
+ if (isFiniteNum(x))
41
+ pairs.push({ x, y });
42
+ }
43
+ else {
37
44
  pairs.push({ x: i + 1, y });
45
+ }
38
46
  }
39
47
  return pairs;
40
48
  };
49
+ /** Samples along a scatter trendline's x range: enough for the curved fits to read as curves. */
50
+ const XY_TRENDLINE_SAMPLES = 64;
41
51
  /** Compute R² between observed y and predicted f(x) over the fit pairs. */
42
52
  const rSquaredOf = (pairs, predict) => {
43
53
  if (pairs.length < 2)
@@ -177,14 +187,22 @@ const fitPower = (pairs) => {
177
187
  * @param values Raw y-values; the 1-based category position is used as x
178
188
  * for the fit, the 0-based index is reported in the points.
179
189
  * @param spec Trendline parameters (type/order/period/forecast).
190
+ * @param xValues Scatter / bubble only — the series' real X values, index-
191
+ * aligned with `values`. The fit then runs on (x, y) pairs
192
+ * (a regression of Y on X, what Excel draws on an XY chart;
193
+ * the point index is wrong for unevenly spaced X), the
194
+ * forecast extends the X range by that many X units, and the
195
+ * points carry real X coordinates sampled across the range.
180
196
  */
181
- export const computeTrendline = (values, spec = {}) => {
182
- const pairs = toPairs(values);
197
+ export const computeTrendline = (values, spec = {}, xValues) => {
198
+ const pairs = toPairs(values, xValues);
183
199
  if (pairs.length < 2)
184
200
  return null;
185
201
  const type = spec.type ?? "linear";
186
- const forward = Math.max(0, Math.floor(spec.forecastForward ?? 0));
187
- const backward = Math.max(0, Math.floor(spec.forecastBackward ?? 0));
202
+ const forwardRaw = Math.max(0, spec.forecastForward ?? 0);
203
+ const backwardRaw = Math.max(0, spec.forecastBackward ?? 0);
204
+ const forward = xValues ? forwardRaw : Math.floor(forwardRaw);
205
+ const backward = xValues ? backwardRaw : Math.floor(backwardRaw);
188
206
  // Moving average is a smoothing of the observed values, not a fitted
189
207
  // function, so it is handled separately and ignores forecasting.
190
208
  if (type === "movingAvg") {
@@ -200,17 +218,51 @@ export const computeTrendline = (values, spec = {}) => {
200
218
  count++;
201
219
  }
202
220
  }
203
- if (count > 0)
221
+ if (count === 0)
222
+ continue;
223
+ if (xValues) {
224
+ const x = xValues[i];
225
+ if (isFiniteNum(x))
226
+ points.push({ x, y: sum / count });
227
+ }
228
+ else {
204
229
  points.push({ x: i, y: sum / count });
230
+ }
205
231
  }
232
+ if (xValues)
233
+ points.sort((a, b) => a.x - b.x);
206
234
  return { points, rSquared: null, equation: null };
207
235
  }
208
- // Fit domain is 1..n; a forecast of k periods extends it to 1−k / n+k.
209
- const xStart = 1 - backward;
210
- const xEnd = values.length + forward;
211
236
  const xs = [];
212
- for (let x = xStart; x <= xEnd; x++)
213
- xs.push(x);
237
+ if (xValues) {
238
+ // Sample the fitted curve across the data's X range, extended by the
239
+ // forecast in X units.
240
+ let minX = Infinity;
241
+ let maxX = -Infinity;
242
+ for (const p of pairs) {
243
+ if (p.x < minX)
244
+ minX = p.x;
245
+ if (p.x > maxX)
246
+ maxX = p.x;
247
+ }
248
+ const xStart = minX - backward;
249
+ const xEnd = maxX + forward;
250
+ if (xEnd > xStart) {
251
+ for (let i = 0; i <= XY_TRENDLINE_SAMPLES; i++) {
252
+ xs.push(xStart + ((xEnd - xStart) * i) / XY_TRENDLINE_SAMPLES);
253
+ }
254
+ }
255
+ else {
256
+ xs.push(xStart);
257
+ }
258
+ }
259
+ else {
260
+ // Fit domain is 1..n; a forecast of k periods extends it to 1−k / n+k.
261
+ const xStart = 1 - backward;
262
+ const xEnd = values.length + forward;
263
+ for (let x = xStart; x <= xEnd; x++)
264
+ xs.push(x);
265
+ }
214
266
  let predict = null;
215
267
  let equation = null;
216
268
  switch (type) {
@@ -263,9 +315,10 @@ export const computeTrendline = (values, spec = {}) => {
263
315
  default:
264
316
  return null;
265
317
  }
266
- // Report the renderer-facing 0-based category index (see module comment).
318
+ // Report the renderer-facing 0-based category index (see module comment);
319
+ // an XY fit reports the real X it was sampled at.
267
320
  const points = xs
268
- .map((x) => ({ x: x - 1, y: predict(x) }))
321
+ .map((x) => ({ x: xValues ? x : x - 1, y: predict(x) }))
269
322
  .filter((p) => Number.isFinite(p.y));
270
323
  if (points.length < 2)
271
324
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rowsncolumns/charts",
3
- "version": "28.1.7",
3
+ "version": "28.1.10",
4
4
  "author": "Vinay M",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "main": "./dist/cjs/index.js",
@@ -70,9 +70,9 @@
70
70
  "@rowsncolumns/common-types": ">=5.3.2",
71
71
  "@rowsncolumns/grid": ">=13.2.35",
72
72
  "@rowsncolumns/icons": ">=7.2.33",
73
- "@rowsncolumns/spreadsheet": ">=10.4.6",
74
- "@rowsncolumns/spreadsheet-state": ">=29.1.7",
75
- "@rowsncolumns/toolkit": ">=24.5.8",
73
+ "@rowsncolumns/spreadsheet": ">=10.4.8",
74
+ "@rowsncolumns/spreadsheet-state": ">=29.1.8",
75
+ "@rowsncolumns/toolkit": ">=24.5.10",
76
76
  "@rowsncolumns/ui": ">=16.0.4",
77
77
  "@rowsncolumns/utils": ">=9.5.5",
78
78
  "@types/react-plotly.js": "^2.6.3",