@rowsncolumns/charts 28.1.2 → 28.1.5

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 +152 -88
  3. package/dist/cjs/basic-chart/helpers.d.ts +9 -0
  4. package/dist/cjs/basic-chart/helpers.d.ts.map +1 -1
  5. package/dist/cjs/basic-chart/helpers.js +55 -1
  6. package/dist/cjs/plotly/basic-chart/basic-chart.d.ts.map +1 -1
  7. package/dist/cjs/plotly/basic-chart/basic-chart.js +225 -140
  8. package/dist/cjs/plotly/plot.d.ts +12 -0
  9. package/dist/cjs/plotly/plot.d.ts.map +1 -0
  10. package/dist/cjs/plotly/plot.js +8 -0
  11. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  12. package/dist/cjs/utils/axis-number-format.d.ts +14 -0
  13. package/dist/cjs/utils/axis-number-format.d.ts.map +1 -1
  14. package/dist/cjs/utils/axis-number-format.js +41 -1
  15. package/dist/cjs/utils/empty-cells.d.ts +9 -0
  16. package/dist/cjs/utils/empty-cells.d.ts.map +1 -1
  17. package/dist/cjs/utils/empty-cells.js +26 -1
  18. package/dist/cjs/utils/legend-layout.d.ts +168 -0
  19. package/dist/cjs/utils/legend-layout.d.ts.map +1 -0
  20. package/dist/cjs/utils/legend-layout.js +338 -0
  21. package/dist/cjs/utils/trendline.d.ts +18 -5
  22. package/dist/cjs/utils/trendline.d.ts.map +1 -1
  23. package/dist/cjs/utils/trendline.js +38 -16
  24. package/dist/esm/basic-chart/basic-chart.d.ts.map +1 -1
  25. package/dist/esm/basic-chart/basic-chart.js +118 -47
  26. package/dist/esm/basic-chart/helpers.d.ts +9 -0
  27. package/dist/esm/basic-chart/helpers.d.ts.map +1 -1
  28. package/dist/esm/basic-chart/helpers.js +30 -0
  29. package/dist/esm/plotly/basic-chart/basic-chart.d.ts.map +1 -1
  30. package/dist/esm/plotly/basic-chart/basic-chart.js +140 -13
  31. package/dist/esm/plotly/plot.d.ts +12 -0
  32. package/dist/esm/plotly/plot.d.ts.map +1 -0
  33. package/dist/esm/plotly/plot.js +12 -0
  34. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  35. package/dist/esm/utils/axis-number-format.d.ts +14 -0
  36. package/dist/esm/utils/axis-number-format.d.ts.map +1 -1
  37. package/dist/esm/utils/axis-number-format.js +36 -0
  38. package/dist/esm/utils/empty-cells.d.ts +9 -0
  39. package/dist/esm/utils/empty-cells.d.ts.map +1 -1
  40. package/dist/esm/utils/empty-cells.js +21 -0
  41. package/dist/esm/utils/legend-layout.d.ts +168 -0
  42. package/dist/esm/utils/legend-layout.d.ts.map +1 -0
  43. package/dist/esm/utils/legend-layout.js +299 -0
  44. package/dist/esm/utils/trendline.d.ts +18 -5
  45. package/dist/esm/utils/trendline.d.ts.map +1 -1
  46. package/dist/esm/utils/trendline.js +36 -16
  47. package/package.json +9 -9
@@ -0,0 +1,338 @@
1
+ "use strict";
2
+ // Legend band reservation (rowsncolumns/spreadsheet#986).
3
+ //
4
+ // Excel and Google Sheets never let the legend paint over the plot: for
5
+ // every legend position the chart reserves a band of its own — legend
6
+ // outermost, then the axis title, then the tick labels, then the plot
7
+ // area shrinks to whatever is left. Both renderers here used to pin the
8
+ // legend a fixed distance from the frame edge and leave the plot margin
9
+ // constant, so a bottom legend sat on the category labels and hid the
10
+ // horizontal axis title (and a top legend crowded the chart title).
11
+ //
12
+ // This module estimates the legend's rendered size from its actual items
13
+ // (count, label widths, font size, wrapping into rows / columns) and turns
14
+ // that into renderer geometry: the echarts `grid` + legend offsets +
15
+ // category-axis `nameGap`, and the plotly `margin` + paper-relative legend
16
+ // anchor. The estimate is deliberately generous — a few px of slack beats
17
+ // a legend on top of the labels.
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.reservePlotlyLegendBand = exports.reserveEchartsLegendBand = exports.echartsTitleBand = exports.categoryAxisNameGap = exports.echartsAxisLabelDepth = exports.legendLabelsFrom = exports.estimateLegendBand = exports.measureTextWidth = exports.LEGEND_ITEM_GAP = exports.LEGEND_PADDING = exports.EDGE_PAD = exports.BAND_GAP = void 0;
20
+ var tslib_1 = require("tslib");
21
+ /** Space kept between neighbouring bands (legend ↔ axis title ↔ labels). */
22
+ exports.BAND_GAP = 8;
23
+ /** Inset of the outermost band from the chart frame. */
24
+ exports.EDGE_PAD = 10;
25
+ /** echarts `legend.padding` default used by both adapters' legend boxes. */
26
+ exports.LEGEND_PADDING = 10;
27
+ /** echarts `legend.itemGap` default. */
28
+ exports.LEGEND_ITEM_GAP = 10;
29
+ /** Gap echarts leaves between a legend icon and its text. */
30
+ var LEGEND_ICON_TEXT_GAP = 5;
31
+ /** Approximate rendered width of `text` at `fontSize` (canvas when available). */
32
+ var measureTextWidth = function (text, fontSize) {
33
+ if (typeof document !== "undefined") {
34
+ var ctx = document.createElement("canvas").getContext("2d");
35
+ if (ctx) {
36
+ ctx.font = "".concat(fontSize, "px sans-serif");
37
+ var width = ctx.measureText(text).width;
38
+ if (Number.isFinite(width) && width > 0)
39
+ return width;
40
+ }
41
+ }
42
+ // Headless fallback: ~0.6em per character.
43
+ return text.length * fontSize * 0.6;
44
+ };
45
+ exports.measureTextWidth = measureTextWidth;
46
+ /**
47
+ * Estimate the legend's rendered box from its items. Horizontal legends
48
+ * pack items greedily into rows no wider than the frame; vertical legends
49
+ * stack items and spill into a new column when taller than the frame.
50
+ */
51
+ var estimateLegendBand = function (_a) {
52
+ var e_1, _b;
53
+ var labels = _a.labels, _c = _a.fontSize, fontSize = _c === void 0 ? 12 : _c, _d = _a.itemWidth, itemWidth = _d === void 0 ? 10 : _d, _e = _a.itemGap, itemGap = _e === void 0 ? exports.LEGEND_ITEM_GAP : _e, _f = _a.padding, padding = _f === void 0 ? exports.LEGEND_PADDING : _f, orientation = _a.orientation, availableWidth = _a.availableWidth, availableHeight = _a.availableHeight;
54
+ if (!labels.length)
55
+ return { rows: 0, columns: 0, width: 0, height: 0 };
56
+ var lineHeight = Math.ceil(fontSize * 1.3);
57
+ var itemWidths = labels.map(function (label) {
58
+ return itemWidth + LEGEND_ICON_TEXT_GAP + Math.ceil((0, exports.measureTextWidth)(label, fontSize));
59
+ });
60
+ if (orientation === "horizontal") {
61
+ var maxRowWidth = Math.max(1, availableWidth - padding * 2);
62
+ var rows_1 = 1;
63
+ var rowWidth = 0;
64
+ var widestRow = 0;
65
+ try {
66
+ for (var itemWidths_1 = tslib_1.__values(itemWidths), itemWidths_1_1 = itemWidths_1.next(); !itemWidths_1_1.done; itemWidths_1_1 = itemWidths_1.next()) {
67
+ var w = itemWidths_1_1.value;
68
+ var next = rowWidth === 0 ? w : rowWidth + itemGap + w;
69
+ if (next > maxRowWidth && rowWidth > 0) {
70
+ widestRow = Math.max(widestRow, rowWidth);
71
+ rows_1 += 1;
72
+ rowWidth = w;
73
+ }
74
+ else {
75
+ rowWidth = next;
76
+ }
77
+ }
78
+ }
79
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
80
+ finally {
81
+ try {
82
+ if (itemWidths_1_1 && !itemWidths_1_1.done && (_b = itemWidths_1.return)) _b.call(itemWidths_1);
83
+ }
84
+ finally { if (e_1) throw e_1.error; }
85
+ }
86
+ widestRow = Math.max(widestRow, rowWidth);
87
+ return {
88
+ rows: rows_1,
89
+ columns: 1,
90
+ width: padding * 2 + widestRow,
91
+ height: padding * 2 + rows_1 * lineHeight + (rows_1 - 1) * itemGap,
92
+ };
93
+ }
94
+ var maxColumnHeight = Math.max(1, availableHeight - padding * 2);
95
+ var perColumn = Math.max(1, Math.floor((maxColumnHeight + itemGap) / (lineHeight + itemGap)));
96
+ var columns = Math.ceil(labels.length / perColumn);
97
+ var rows = Math.min(labels.length, perColumn);
98
+ var widestItem = Math.max.apply(Math, tslib_1.__spreadArray([], tslib_1.__read(itemWidths), false));
99
+ return {
100
+ rows: rows,
101
+ columns: columns,
102
+ width: padding * 2 + columns * widestItem + (columns - 1) * itemGap,
103
+ height: padding * 2 + rows * lineHeight + (rows - 1) * itemGap,
104
+ };
105
+ };
106
+ exports.estimateLegendBand = estimateLegendBand;
107
+ /**
108
+ * Pull the legend entry labels out of an echarts `legend.data` (strings or
109
+ * `{ name }` items); falls back to the series names, which is what echarts
110
+ * itself lists when `data` is empty.
111
+ */
112
+ var legendLabelsFrom = function (legendData, seriesNames) {
113
+ var fromData = (legendData !== null && legendData !== void 0 ? legendData : [])
114
+ .map(function (d) {
115
+ return typeof d === "string"
116
+ ? d
117
+ : d && typeof d === "object" && typeof d.name === "string"
118
+ ? d.name
119
+ : "";
120
+ })
121
+ .filter(function (s) { return s.length > 0; });
122
+ if (fromData.length)
123
+ return fromData;
124
+ return seriesNames
125
+ .map(function (n) { return (typeof n === "string" ? n : n === undefined || n === null ? "" : String(n)); })
126
+ .filter(function (s) { return s.length > 0; });
127
+ };
128
+ exports.legendLabelsFrom = legendLabelsFrom;
129
+ /**
130
+ * Depth (px) of one row of echarts axis tick labels measured from the axis
131
+ * line: 5px tick + 8px label margin + one 12px line, rounded up. With
132
+ * `grid.containLabel` this is the part the grid already accounts for; the
133
+ * axis title sits beyond it.
134
+ */
135
+ var echartsAxisLabelDepth = function (fontSize) {
136
+ if (fontSize === void 0) { fontSize = 12; }
137
+ return 5 + 8 + Math.ceil(fontSize * 1.1);
138
+ };
139
+ exports.echartsAxisLabelDepth = echartsAxisLabelDepth;
140
+ /**
141
+ * echarts `nameGap` for a category axis whose title sits in the middle
142
+ * below the tick labels: clear one row of labels plus a small gap, measured
143
+ * to the title's centre line.
144
+ */
145
+ var categoryAxisNameGap = function (fontSize) {
146
+ if (fontSize === void 0) { fontSize = 12; }
147
+ return (0, exports.echartsAxisLabelDepth)(fontSize) + exports.BAND_GAP / 2 + Math.ceil(fontSize / 2);
148
+ };
149
+ exports.categoryAxisNameGap = categoryAxisNameGap;
150
+ /** Height of the echarts chart-title band (padding 10 each side + one line). */
151
+ var echartsTitleBand = function (titleFontSize) {
152
+ if (titleFontSize === void 0) { titleFontSize = 16; }
153
+ return exports.LEGEND_PADDING * 2 + Math.ceil(titleFontSize * 1.25);
154
+ };
155
+ exports.echartsTitleBand = echartsTitleBand;
156
+ /**
157
+ * Echarts cartesian geometry with a band reserved for the legend in its
158
+ * position, outside the axis-title band, outside the tick labels.
159
+ */
160
+ var reserveEchartsLegendBand = function (_a) {
161
+ var position = _a.position, visible = _a.visible, labels = _a.labels, _b = _a.legendFontSize, legendFontSize = _b === void 0 ? 12 : _b, _c = _a.legendItemWidth, legendItemWidth = _c === void 0 ? 10 : _c, width = _a.width, height = _a.height, hasTitle = _a.hasTitle, _d = _a.titleFontSize, titleFontSize = _d === void 0 ? 16 : _d, hasBottomAxisTitle = _a.hasBottomAxisTitle, bottomAxisNameGap = _a.bottomAxisNameGap, hasLeftAxisTitle = _a.hasLeftAxisTitle, _e = _a.axisFontSize, axisFontSize = _e === void 0 ? 12 : _e;
162
+ var pos = visible ? (position !== null && position !== void 0 ? position : "top") : "none";
163
+ var horizontal = pos === "top" || pos === "bottom";
164
+ var band = pos === "none"
165
+ ? { rows: 0, columns: 0, width: 0, height: 0 }
166
+ : (0, exports.estimateLegendBand)({
167
+ labels: labels,
168
+ fontSize: legendFontSize,
169
+ itemWidth: legendItemWidth,
170
+ orientation: horizontal ? "horizontal" : "vertical",
171
+ availableWidth: width,
172
+ availableHeight: height,
173
+ });
174
+ var titleBand = hasTitle ? (0, exports.echartsTitleBand)(titleFontSize) : 0;
175
+ var labelDepth = (0, exports.echartsAxisLabelDepth)(axisFontSize);
176
+ // The bottom axis title's centre sits `nameGap` below the axis line and
177
+ // its line box extends half a font below that; the contained labels
178
+ // already cover `labelDepth` of it.
179
+ var bottomTitleOverhang = hasBottomAxisTitle
180
+ ? Math.max(0, bottomAxisNameGap + Math.ceil(axisFontSize / 2) - labelDepth) + exports.BAND_GAP
181
+ : 0;
182
+ // The rotated left axis title clears its labels by `fontSize + 6` (see
183
+ // valueAxisNameGap) and its own line box extends half a font further.
184
+ var leftTitleOverhang = hasLeftAxisTitle
185
+ ? Math.ceil(axisFontSize * 1.5) + 6 + exports.BAND_GAP / 2
186
+ : 0;
187
+ // Half a tick label pokes past the top / right end of an axis.
188
+ var topHeadroom = Math.ceil(axisFontSize / 2) + 4;
189
+ var rightHeadroom = Math.ceil(axisFontSize / 2) + 10;
190
+ var legend = {};
191
+ var top = (hasTitle ? titleBand : exports.EDGE_PAD) + topHeadroom;
192
+ var bottom = exports.EDGE_PAD + bottomTitleOverhang;
193
+ var left = exports.EDGE_PAD + leftTitleOverhang;
194
+ var right = rightHeadroom;
195
+ switch (pos) {
196
+ case "top": {
197
+ legend.top = hasTitle ? titleBand : exports.EDGE_PAD;
198
+ legend.bottom = undefined;
199
+ top = legend.top + band.height + exports.BAND_GAP + topHeadroom;
200
+ break;
201
+ }
202
+ case "bottom": {
203
+ legend.bottom = exports.EDGE_PAD;
204
+ legend.top = undefined;
205
+ bottom = exports.EDGE_PAD + band.height + exports.BAND_GAP + bottomTitleOverhang;
206
+ break;
207
+ }
208
+ case "left": {
209
+ legend.left = exports.EDGE_PAD;
210
+ legend.right = undefined;
211
+ left = exports.EDGE_PAD + band.width + exports.BAND_GAP + leftTitleOverhang;
212
+ break;
213
+ }
214
+ case "right": {
215
+ legend.right = exports.EDGE_PAD;
216
+ legend.left = undefined;
217
+ right = exports.EDGE_PAD + band.width + exports.BAND_GAP;
218
+ break;
219
+ }
220
+ case "topRight": {
221
+ legend.right = exports.EDGE_PAD;
222
+ legend.left = undefined;
223
+ legend.top = hasTitle ? titleBand : exports.EDGE_PAD;
224
+ legend.bottom = undefined;
225
+ right = exports.EDGE_PAD + band.width + exports.BAND_GAP;
226
+ break;
227
+ }
228
+ default:
229
+ break;
230
+ }
231
+ return {
232
+ band: band,
233
+ grid: { left: left, right: right, top: top, bottom: bottom, containLabel: true },
234
+ legend: legend,
235
+ };
236
+ };
237
+ exports.reserveEchartsLegendBand = reserveEchartsLegendBand;
238
+ /** Plotly text line height (its LINE_SPACING is 1.3em). */
239
+ var plotlyLineHeight = function (fontSize) { return Math.ceil(fontSize * 1.3); };
240
+ /** Plotly default `legend.itemwidth`. */
241
+ var PLOTLY_LEGEND_ITEM_WIDTH = 30;
242
+ /**
243
+ * Plotly geometry with a band reserved for the legend in its position.
244
+ * Legend coordinates are paper-relative (fractions of the plot area), so
245
+ * they are derived from the margins this returns; `automargin` on the
246
+ * axes may still grow a margin further for unusually deep tick labels.
247
+ */
248
+ var reservePlotlyLegendBand = function (_a) {
249
+ var position = _a.position, visible = _a.visible, labels = _a.labels, _b = _a.legendFontSize, legendFontSize = _b === void 0 ? 12 : _b, width = _a.width, height = _a.height, hasTitle = _a.hasTitle, _c = _a.titleFontSize, titleFontSize = _c === void 0 ? 16 : _c, hasBottomAxisTitle = _a.hasBottomAxisTitle, hasLeftAxisTitle = _a.hasLeftAxisTitle, leftAxisDepth = _a.leftAxisDepth, _d = _a.rightAxisDepth, rightAxisDepth = _d === void 0 ? 0 : _d, _e = _a.bottomAxisTitleStandoff, bottomAxisTitleStandoff = _e === void 0 ? exports.BAND_GAP : _e, _f = _a.axisFontSize, axisFontSize = _f === void 0 ? 12 : _f;
250
+ var pos = visible ? (position !== null && position !== void 0 ? position : "right") : "none";
251
+ var horizontal = pos === "top" || pos === "bottom";
252
+ var band = pos === "none"
253
+ ? { rows: 0, columns: 0, width: 0, height: 0 }
254
+ : (0, exports.estimateLegendBand)({
255
+ labels: labels,
256
+ fontSize: legendFontSize,
257
+ itemWidth: PLOTLY_LEGEND_ITEM_WIDTH,
258
+ itemGap: 5,
259
+ padding: 5,
260
+ orientation: horizontal ? "horizontal" : "vertical",
261
+ availableWidth: width,
262
+ availableHeight: height,
263
+ });
264
+ var titleBand = hasTitle
265
+ ? exports.EDGE_PAD + plotlyLineHeight(titleFontSize) + exports.BAND_GAP
266
+ : 0;
267
+ // One row of bottom tick labels: plotly pads them ~3px off the axis.
268
+ var bottomLabelDepth = 3 + plotlyLineHeight(axisFontSize);
269
+ var bottomTitleBand = hasBottomAxisTitle
270
+ ? bottomAxisTitleStandoff + plotlyLineHeight(axisFontSize)
271
+ : 0;
272
+ var bottomAxisDepth = bottomLabelDepth + bottomTitleBand;
273
+ var topHeadroom = Math.ceil(axisFontSize / 2) + 4;
274
+ var t = Math.max(exports.EDGE_PAD + topHeadroom, titleBand + topHeadroom);
275
+ var b = bottomAxisDepth + exports.EDGE_PAD;
276
+ var l = leftAxisDepth + (hasLeftAxisTitle ? exports.BAND_GAP : 0) + exports.EDGE_PAD;
277
+ var r = Math.max(exports.EDGE_PAD + Math.ceil(axisFontSize / 2), rightAxisDepth + exports.EDGE_PAD);
278
+ var legend = null;
279
+ switch (pos) {
280
+ case "top": {
281
+ t = (hasTitle ? titleBand : exports.EDGE_PAD) + band.height + exports.BAND_GAP + topHeadroom;
282
+ legend = { orientation: "h", x: 0.5, xanchor: "center", y: 1, yanchor: "bottom" };
283
+ break;
284
+ }
285
+ case "bottom": {
286
+ b = bottomAxisDepth + exports.BAND_GAP + band.height + exports.EDGE_PAD;
287
+ legend = { orientation: "h", x: 0.5, xanchor: "center", y: 0, yanchor: "top" };
288
+ break;
289
+ }
290
+ case "left": {
291
+ l = leftAxisDepth + exports.BAND_GAP + band.width + exports.EDGE_PAD;
292
+ legend = { orientation: "v", x: 0, xanchor: "right", y: 0.5, yanchor: "middle" };
293
+ break;
294
+ }
295
+ case "right": {
296
+ r = rightAxisDepth + exports.BAND_GAP + band.width + exports.EDGE_PAD;
297
+ legend = { orientation: "v", x: 1, xanchor: "left", y: 0.5, yanchor: "middle" };
298
+ break;
299
+ }
300
+ case "topRight": {
301
+ r = rightAxisDepth + exports.BAND_GAP + band.width + exports.EDGE_PAD;
302
+ legend = { orientation: "v", x: 1, xanchor: "left", y: 1, yanchor: "top" };
303
+ break;
304
+ }
305
+ default:
306
+ break;
307
+ }
308
+ // Paper coordinates: offsets outside the plot area are fractions of it.
309
+ var plotWidth = Math.max(1, width - l - r);
310
+ var plotHeight = Math.max(1, height - t - b);
311
+ if (legend) {
312
+ switch (pos) {
313
+ case "top":
314
+ legend.y = 1 + (exports.BAND_GAP + topHeadroom) / plotHeight;
315
+ break;
316
+ case "bottom":
317
+ legend.y = -(bottomAxisDepth + exports.BAND_GAP) / plotHeight;
318
+ break;
319
+ case "left":
320
+ legend.x = -(leftAxisDepth + exports.BAND_GAP) / plotWidth;
321
+ break;
322
+ case "right":
323
+ case "topRight":
324
+ legend.x = 1 + (rightAxisDepth + exports.BAND_GAP) / plotWidth;
325
+ break;
326
+ default:
327
+ break;
328
+ }
329
+ }
330
+ return {
331
+ band: band,
332
+ margin: { l: l, r: r, t: t, b: b },
333
+ legend: legend,
334
+ bottomAxisTitleStandoff: bottomAxisTitleStandoff,
335
+ title: { yref: "container", y: 1, yanchor: "top", pad: { t: exports.EDGE_PAD } },
336
+ };
337
+ };
338
+ exports.reservePlotlyLegendBand = reservePlotlyLegendBand;
@@ -7,12 +7,24 @@
7
7
  * so the regressions are hand-rolled here as pure least-squares math and
8
8
  * the renderers draw the returned points as an overlay line series.
9
9
  *
10
- * All fits operate on the index domain x = 0, 1, 2, … (one step per
11
- * category) which matches how a category-axis chart positions points.
12
- * The caller passes the raw y-values; gaps (null/NaN) are skipped during
13
- * fitting but the fitted curve is still emitted for every x in range.
10
+ * All fits operate on Excel's category domain x = 1, 2, …, n (one step per
11
+ * category, first category = 1) — the domain Excel fits and prints for a
12
+ * category-axis chart (line, column, bar, area). Linear, polynomial and
13
+ * exponential curves are invariant to where the domain starts (only the
14
+ * equation text moves), but logarithmic and power fits are not: fitting
15
+ * them on x = 0, 1, … dropped the first category from the fit and evaluated
16
+ * ln(x) at x → 0 for it, which drew the Jan point of a 12-month log trend
17
+ * at about −524 instead of Excel's ≈6.4 (rowsncolumns/spreadsheet#1028).
18
+ *
19
+ * The emitted points keep the renderer-facing 0-based CATEGORY INDEX in `x`
20
+ * (echarts plots `[x, y]` on the category axis, plotly maps `x` onto the
21
+ * category labels), so a forecast of k periods still extends to index
22
+ * n−1+k forward and −k backward. The caller passes the raw y-values; gaps
23
+ * (null/NaN) are skipped during fitting but the fitted curve is still
24
+ * emitted for every x in range.
14
25
  */
15
26
  export type TrendlineType = "linear" | "exp" | "log" | "poly" | "power" | "movingAvg";
27
+ /** `x` is the 0-based category index (−k … n−1+k with forecasting), `y` the fitted value. */
16
28
  export type TrendlinePoint = {
17
29
  x: number;
18
30
  y: number;
@@ -28,7 +40,8 @@ export type TrendlineResult = {
28
40
  /**
29
41
  * Compute a trendline for a series of y-values.
30
42
  *
31
- * @param values Raw y-values; the category index is used as x.
43
+ * @param values Raw y-values; the 1-based category position is used as x
44
+ * for the fit, the 0-based index is reported in the points.
32
45
  * @param spec Trendline parameters (type/order/period/forecast).
33
46
  */
34
47
  export declare const computeTrendline: (values: (number | null | undefined)[], spec?: {
@@ -1 +1 @@
1
- {"version":3,"file":"trendline.d.ts","sourceRoot":"","sources":["../../../utils/trendline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,MAAM,aAAa,GACrB,QAAQ,GACR,KAAK,GACL,KAAK,GACL,MAAM,GACN,OAAO,GACP,WAAW,CAAC;AAEhB,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;AA+IF;;;;;GAKG;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,IA6FpB,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;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"}
@@ -8,10 +8,21 @@
8
8
  * so the regressions are hand-rolled here as pure least-squares math and
9
9
  * the renderers draw the returned points as an overlay line series.
10
10
  *
11
- * All fits operate on the index domain x = 0, 1, 2, … (one step per
12
- * category) which matches how a category-axis chart positions points.
13
- * The caller passes the raw y-values; gaps (null/NaN) are skipped during
14
- * fitting but the fitted curve is still emitted for every x in range.
11
+ * All fits operate on Excel's category domain x = 1, 2, …, n (one step per
12
+ * category, first category = 1) — the domain Excel fits and prints for a
13
+ * category-axis chart (line, column, bar, area). Linear, polynomial and
14
+ * exponential curves are invariant to where the domain starts (only the
15
+ * equation text moves), but logarithmic and power fits are not: fitting
16
+ * them on x = 0, 1, … dropped the first category from the fit and evaluated
17
+ * ln(x) at x → 0 for it, which drew the Jan point of a 12-month log trend
18
+ * at about −524 instead of Excel's ≈6.4 (rowsncolumns/spreadsheet#1028).
19
+ *
20
+ * The emitted points keep the renderer-facing 0-based CATEGORY INDEX in `x`
21
+ * (echarts plots `[x, y]` on the category axis, plotly maps `x` onto the
22
+ * category labels), so a forecast of k periods still extends to index
23
+ * n−1+k forward and −k backward. The caller passes the raw y-values; gaps
24
+ * (null/NaN) are skipped during fitting but the fitted curve is still
25
+ * emitted for every x in range.
15
26
  */
16
27
  Object.defineProperty(exports, "__esModule", { value: true });
17
28
  exports.computeTrendline = void 0;
@@ -20,7 +31,8 @@ var isFiniteNum = function (v) {
20
31
  return typeof v === "number" && Number.isFinite(v);
21
32
  };
22
33
  /**
23
- * Build (x, y) pairs from raw y-values using the category index as x.
34
+ * Build (x, y) pairs from raw y-values using Excel's 1-based category
35
+ * position as x (the first category is x = 1, see the module comment).
24
36
  * Non-finite y-values are dropped (they cannot be fitted).
25
37
  */
26
38
  var toPairs = function (values) {
@@ -28,7 +40,7 @@ var toPairs = function (values) {
28
40
  for (var i = 0; i < values.length; i++) {
29
41
  var y = values[i];
30
42
  if (isFiniteNum(y))
31
- pairs.push({ x: i, y: y });
43
+ pairs.push({ x: i + 1, y: y });
32
44
  }
33
45
  return pairs;
34
46
  };
@@ -66,6 +78,11 @@ var round = function (n, digits) {
66
78
  var f = Math.pow(10, digits);
67
79
  return Math.round(n * f) / f;
68
80
  };
81
+ /** `+ 2` / `- 2` for an additive term, the way Excel prints an equation. */
82
+ var signed = function (n) {
83
+ var r = round(n);
84
+ return r < 0 ? "- ".concat(Math.abs(r)) : "+ ".concat(r);
85
+ };
69
86
  /** Solve a small linear system A·x = b via Gaussian elimination. */
70
87
  var solveLinearSystem = function (A, b) {
71
88
  var _a;
@@ -198,7 +215,8 @@ var fitPower = function (pairs) {
198
215
  /**
199
216
  * Compute a trendline for a series of y-values.
200
217
  *
201
- * @param values Raw y-values; the category index is used as x.
218
+ * @param values Raw y-values; the 1-based category position is used as x
219
+ * for the fit, the 0-based index is reported in the points.
202
220
  * @param spec Trendline parameters (type/order/period/forecast).
203
221
  */
204
222
  var computeTrendline = function (values, spec) {
@@ -230,9 +248,9 @@ var computeTrendline = function (values, spec) {
230
248
  }
231
249
  return { points: points_1, rSquared: null, equation: null };
232
250
  }
233
- var lastX = values.length - 1;
234
- var xStart = -backward;
235
- var xEnd = lastX + forward;
251
+ // Fit domain is 1..n; a forecast of k periods extends it to 1−k / n+k.
252
+ var xStart = 1 - backward;
253
+ var xEnd = values.length + forward;
236
254
  var xs = [];
237
255
  for (var x = xStart; x <= xEnd; x++)
238
256
  xs.push(x);
@@ -244,7 +262,7 @@ var computeTrendline = function (values, spec) {
244
262
  if (!fit_1)
245
263
  return null;
246
264
  predict = function (x) { return fit_1.m * x + fit_1.b; };
247
- equation = "y = ".concat(round(fit_1.m), "x + ").concat(round(fit_1.b));
265
+ equation = "y = ".concat(round(fit_1.m), "x ").concat(signed(fit_1.b));
248
266
  break;
249
267
  }
250
268
  case "poly": {
@@ -257,8 +275,10 @@ var computeTrendline = function (values, spec) {
257
275
  equation =
258
276
  "y = " +
259
277
  coeffs_1
260
- .map(function (c, i) { return (i === 0 ? "".concat(round(c)) : "".concat(round(c), "x^").concat(i)); })
261
- .join(" + ");
278
+ .map(function (c, i) {
279
+ return i === 0 ? "".concat(round(c)) : "".concat(signed(c), "x").concat(i === 1 ? "" : "^".concat(i));
280
+ })
281
+ .join(" ");
262
282
  break;
263
283
  }
264
284
  case "exp": {
@@ -273,9 +293,10 @@ var computeTrendline = function (values, spec) {
273
293
  var fit_3 = fitLog(pairs);
274
294
  if (!fit_3)
275
295
  return null;
276
- // ln(0) and ln(negative) are undefined; clamp x to >0 for the curve.
296
+ // Only a backward forecast reaches x ≤ 0 (the fit domain starts at 1);
297
+ // ln(0) and ln(negative) are undefined, so clamp x to >0 for the curve.
277
298
  predict = function (x) { return fit_3.a + fit_3.b * Math.log(x <= 0 ? 1e-9 : x); };
278
- equation = "y = ".concat(round(fit_3.a), " + ").concat(round(fit_3.b), "ln(x)");
299
+ equation = "y = ".concat(round(fit_3.b), "ln(x) ").concat(signed(fit_3.a));
279
300
  break;
280
301
  }
281
302
  case "power": {
@@ -289,8 +310,9 @@ var computeTrendline = function (values, spec) {
289
310
  default:
290
311
  return null;
291
312
  }
313
+ // Report the renderer-facing 0-based category index (see module comment).
292
314
  var points = xs
293
- .map(function (x) { return ({ x: x, y: predict(x) }); })
315
+ .map(function (x) { return ({ x: x - 1, y: predict(x) }); })
294
316
  .filter(function (p) { return Number.isFinite(p.y); });
295
317
  if (points.length < 2)
296
318
  return null;
@@ -1 +1 @@
1
- {"version":3,"file":"basic-chart.d.ts","sourceRoot":"","sources":["../../../basic-chart/basic-chart.tsx"],"names":[],"mappings":"AA4DA,OAAO,KAAiD,MAAM,OAAO,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AA0PrD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAkEnD,eAAO,MAAM,UAAU,GAAI,wOAiBxB,eAAe,sBAmuGjB,CAAC"}
1
+ {"version":3,"file":"basic-chart.d.ts","sourceRoot":"","sources":["../../../basic-chart/basic-chart.tsx"],"names":[],"mappings":"AA4DA,OAAO,KAAiD,MAAM,OAAO,CAAC;AAEtE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAmQrD,MAAM,MAAM,eAAe,GAAG,oBAAoB,CAAC;AAqEnD,eAAO,MAAM,UAAU,GAAI,wOAiBxB,eAAe,sBAmzGjB,CAAC"}