@rowsncolumns/charts 28.1.10 → 28.1.12

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.
@@ -45,17 +45,58 @@ export const computePareto = (bins) => {
45
45
  const formatBinBound = (value) => {
46
46
  if (!Number.isFinite(value))
47
47
  return String(value);
48
+ if (value === 0)
49
+ return "0";
48
50
  const abs = Math.abs(value);
49
51
  if (abs >= 1000 || abs < 0.01)
50
52
  return value.toExponential(2);
51
53
  return Number.parseFloat(value.toFixed(2)).toString();
52
54
  };
55
+ const isFinite_ = (v) => typeof v === "number" && Number.isFinite(v);
56
+ /** True when the spec authored any Excel binning option. */
57
+ export const hasExcelBinning = (options) => Boolean(options &&
58
+ (isFinite_(options.binSize) ||
59
+ options.intervalClosed === "l" ||
60
+ options.intervalClosed === "r" ||
61
+ isFinite_(options.overflow) ||
62
+ isFinite_(options.underflow)));
63
+ /** Round up to two significant digits (31 for 30.77, 0.038 for 0.0377). */
64
+ const ceilToTwoSignificant = (value) => {
65
+ if (!(value > 0))
66
+ return value;
67
+ const magnitude = 10 ** (Math.floor(Math.log10(value)) - 1);
68
+ return Math.ceil(value / magnitude - 1e-9) * magnitude;
69
+ };
70
+ /**
71
+ * Excel's automatic bin width: Scott's normal reference rule,
72
+ * 3.5 · σ / ∛n with the sample standard deviation, rounded up to two
73
+ * significant digits. For the 20 values 12 … 95 of #1087 (σ ≈ 23.86) that
74
+ * is 30.77 → 31, i.e. Excel's three bins [12, 43], (43, 74], (74, 105].
75
+ */
76
+ export const excelAutoBinWidth = (numeric) => {
77
+ const n = numeric.length;
78
+ if (n < 2)
79
+ return 1;
80
+ const mean = numeric.reduce((sum, v) => sum + v, 0) / n;
81
+ const variance = numeric.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (n - 1);
82
+ const sigma = Math.sqrt(variance);
83
+ if (!(sigma > 0))
84
+ return 1;
85
+ return ceilToTwoSignificant((3.5 * sigma) / Math.cbrt(n));
86
+ };
53
87
  /**
54
- * Bin a set of numeric values into `binCount` equal-width bins.
55
- * When `binCount` is omitted, uses Sturges' rule: ceil(log2(n) + 1).
88
+ * Bin a set of numeric values into equal-width bins.
89
+ *
90
+ * Default (no Excel options): `binCount` bins over [min, max], closed on
91
+ * the left — or Sturges' rule, ceil(log2(n) + 1), when `binCount` is
92
+ * omitted. With Excel binning options (#1087): bins start at the minimum
93
+ * (or the underflow bound), are `binSize` wide — else (max − min) /
94
+ * `binCount`, else Excel's automatic width — closed on the right unless
95
+ * `intervalClosed` is `l`, and values beyond `overflow` / `underflow`
96
+ * collect in their own end bins.
56
97
  */
57
- export const binHistogram = (values, binCount) => {
58
- const numeric = values.filter((v) => typeof v === "number" && Number.isFinite(v));
98
+ export const binHistogram = (values, binCount, options) => {
99
+ const numeric = values.filter(isFinite_);
59
100
  if (numeric.length === 0) {
60
101
  return { labels: [], counts: [] };
61
102
  }
@@ -67,6 +108,9 @@ export const binHistogram = (values, binCount) => {
67
108
  if (v > max)
68
109
  max = v;
69
110
  }
111
+ if (hasExcelBinning(options)) {
112
+ return binExcel(numeric, min, max, binCount, options);
113
+ }
70
114
  const sturges = Math.max(1, Math.ceil(Math.log2(numeric.length) + 1));
71
115
  const bins = typeof binCount === "number" && binCount > 0
72
116
  ? Math.floor(binCount)
@@ -93,3 +137,126 @@ export const binHistogram = (values, binCount) => {
93
137
  }
94
138
  return { labels, counts };
95
139
  };
140
+ const binExcel = (numeric, min, max, binCount, options) => {
141
+ const rightClosed = options.intervalClosed !== "l";
142
+ const underflow = isFinite_(options.underflow) ? options.underflow : null;
143
+ const overflow = isFinite_(options.overflow) ? options.overflow : null;
144
+ // The regular bins span the values between the end bins.
145
+ const inRange = numeric.filter((v) => (underflow === null || v >= underflow) &&
146
+ (overflow === null || v <= overflow));
147
+ const start = underflow ?? (inRange.length ? Math.min(...inRange) : min);
148
+ const end = overflow ?? (inRange.length ? Math.max(...inRange) : max);
149
+ const span = end - start;
150
+ let width;
151
+ if (isFinite_(options.binSize) && options.binSize > 0) {
152
+ width = options.binSize;
153
+ }
154
+ else if (typeof binCount === "number" && binCount > 0) {
155
+ width = span > 0 ? span / Math.floor(binCount) : 1;
156
+ }
157
+ else {
158
+ width = excelAutoBinWidth(inRange.length ? inRange : numeric);
159
+ }
160
+ // Enough bins to reach the last value; a right-closed layout needs one
161
+ // more when the span is an exact multiple (the boundary value belongs to
162
+ // the bin below, so the top bin stays).
163
+ let bins = span > 0 ? Math.ceil(span / width - 1e-9) : 1;
164
+ bins = Math.max(1, bins);
165
+ const counts = new Array(bins).fill(0);
166
+ const edges = Array.from({ length: bins + 1 }, (_, i) => start + width * i);
167
+ let under = 0;
168
+ let over = 0;
169
+ for (const v of numeric) {
170
+ if (underflow !== null && v < underflow) {
171
+ under++;
172
+ continue;
173
+ }
174
+ if (overflow !== null && v > overflow) {
175
+ over++;
176
+ continue;
177
+ }
178
+ let idx;
179
+ if (rightClosed) {
180
+ // (lo, hi]: a value on a boundary belongs to the bin below it; the
181
+ // first bin also takes its own lower bound.
182
+ idx = Math.ceil((v - start) / width - 1e-9) - 1;
183
+ if (idx < 0)
184
+ idx = 0;
185
+ }
186
+ else {
187
+ idx = Math.floor((v - start) / width + 1e-9);
188
+ }
189
+ if (idx >= bins)
190
+ idx = bins - 1;
191
+ counts[idx]++;
192
+ }
193
+ const labels = edges.slice(0, -1).map((lo, i) => {
194
+ const hi = edges[i + 1];
195
+ if (rightClosed) {
196
+ const opening = i === 0 ? "[" : "(";
197
+ return `${opening}${formatBinBound(lo)}, ${formatBinBound(hi)}]`;
198
+ }
199
+ const closing = i === bins - 1 ? "]" : ")";
200
+ return `[${formatBinBound(lo)}, ${formatBinBound(hi)}${closing}`;
201
+ });
202
+ if (underflow !== null) {
203
+ labels.unshift(`< ${formatBinBound(underflow)}`);
204
+ counts.unshift(under);
205
+ }
206
+ if (overflow !== null) {
207
+ labels.push(`> ${formatBinBound(overflow)}`);
208
+ counts.push(over);
209
+ }
210
+ return { labels, counts };
211
+ };
212
+ /**
213
+ * Excel's histogram "By Category" / Pareto over categories
214
+ * (`<cx:aggregation/>`, #1087): one column per distinct category in order
215
+ * of first appearance, its value the aggregate (sum by default) of the
216
+ * series values of that category — instead of binning the values.
217
+ */
218
+ export const aggregateByCategory = (categories, values, aggregateType = "SUM") => {
219
+ const groups = new Map();
220
+ const order = [];
221
+ const count = Math.max(categories.length, values.length);
222
+ for (let i = 0; i < count; i++) {
223
+ const raw = categories[i];
224
+ const label = raw === null || raw === undefined ? "" : `${raw}`.trim();
225
+ if (label === "")
226
+ continue;
227
+ if (!groups.has(label)) {
228
+ groups.set(label, []);
229
+ order.push(label);
230
+ }
231
+ const v = values[i];
232
+ if (isFinite_(v))
233
+ groups.get(label).push(v);
234
+ }
235
+ const reduce = (nums) => {
236
+ if (nums.length === 0)
237
+ return 0;
238
+ switch (aggregateType ?? "SUM") {
239
+ case "COUNT":
240
+ return nums.length;
241
+ case "AVERAGE":
242
+ return nums.reduce((a, b) => a + b, 0) / nums.length;
243
+ case "MAX":
244
+ return Math.max(...nums);
245
+ case "MIN":
246
+ return Math.min(...nums);
247
+ case "MEDIAN": {
248
+ const sorted = [...nums].sort((a, b) => a - b);
249
+ const mid = Math.floor(sorted.length / 2);
250
+ return sorted.length % 2
251
+ ? sorted[mid]
252
+ : (sorted[mid - 1] + sorted[mid]) / 2;
253
+ }
254
+ default:
255
+ return nums.reduce((a, b) => a + b, 0);
256
+ }
257
+ };
258
+ return {
259
+ labels: order,
260
+ counts: order.map((label) => reduce(groups.get(label) ?? [])),
261
+ };
262
+ };
@@ -8,7 +8,7 @@ export declare const applyLegendOverrides: (legend: LegendComponentOption, opts:
8
8
  * becomes the tick-label formatter.
9
9
  */
10
10
  export declare const applyAxisOverrides: (axis: any, opts: any, numberFormatPattern?: string, locale?: string) => any;
11
- export declare const dataLabelFormatter: (opts: any, locale?: string) => (params: any) => string;
11
+ export declare const dataLabelFormatter: (opts: any, locale?: string, joiner?: string) => (params: any) => string;
12
12
  /**
13
13
  * The data-label options one series renders with: its own
14
14
  * `ChartData.dataLabels` when it has them (OOXML's `<c:ser><c:dLbls>`
@@ -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,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"}
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,GAC7B,MAAM,GAAG,EACT,SAAS,MAAM,EAEf,eAAa,MACT,QAAQ,GAAG,WAoDhB,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;;;iCAlGH,GAAG;aA+GhB,CAAC"}
@@ -117,7 +117,9 @@ export const applyAxisOverrides = (axis, opts, numberFormatPattern, locale) => {
117
117
  * — Excel's behaviour.
118
118
  */
119
119
  const formatLabelValue = (rawValue, originalValue, pattern, locale) => formatAxisValue(pattern, originalValue ?? rawValue, locale);
120
- export const dataLabelFormatter = (opts, locale) => (params) => {
120
+ export const dataLabelFormatter = (opts, locale,
121
+ // A treemap block stacks the parts (name, value) on their own lines.
122
+ joiner = ": ") => (params) => {
121
123
  const parts = [];
122
124
  if (opts.showSeriesName && params.seriesName)
123
125
  parts.push(params.seriesName);
@@ -126,10 +128,15 @@ export const dataLabelFormatter = (opts, locale) => (params) => {
126
128
  if (opts.showPercent && params.percent !== undefined) {
127
129
  parts.push(`${params.percent}%`);
128
130
  }
131
+ // The value shows by default unless another field was asked for instead:
132
+ // an importer records only the flags that are ON, so `<c:showVal val="0"/>`
133
+ // + `<c:showBubbleSize val="1"/>` arrives as `{ showBubbleSize: true }` and
134
+ // must label the bubble with its size alone (#1068 / #1079).
129
135
  const showVal = opts.showValue !== false &&
130
136
  !opts.showCategoryName &&
131
137
  !opts.showPercent &&
132
- !opts.showSeriesName
138
+ !opts.showSeriesName &&
139
+ !opts.showBubbleSize
133
140
  ? true
134
141
  : !!opts.showValue;
135
142
  // A scatter / bubble point's value is its [x, y, size] tuple: the label's
@@ -149,7 +156,7 @@ export const dataLabelFormatter = (opts, locale) => (params) => {
149
156
  if (opts.showBubbleSize && isPointTuple && params.value.length > 2) {
150
157
  parts.push(formatLabelValue(params.value[2], undefined, opts.numberFormatPattern, locale));
151
158
  }
152
- return parts.join(": ");
159
+ return parts.join(joiner);
153
160
  };
154
161
  const ECHARTS_DLBL_POS = {
155
162
  center: "inside",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rowsncolumns/charts",
3
- "version": "28.1.10",
3
+ "version": "28.1.12",
4
4
  "author": "Vinay M",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "main": "./dist/cjs/index.js",
@@ -70,7 +70,7 @@
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.8",
73
+ "@rowsncolumns/spreadsheet": ">=10.4.10",
74
74
  "@rowsncolumns/spreadsheet-state": ">=29.1.8",
75
75
  "@rowsncolumns/toolkit": ">=24.5.10",
76
76
  "@rowsncolumns/ui": ">=16.0.4",