@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.
- package/dist/cjs/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/cjs/basic-chart/basic-chart.js +152 -88
- package/dist/cjs/basic-chart/helpers.d.ts +9 -0
- package/dist/cjs/basic-chart/helpers.d.ts.map +1 -1
- package/dist/cjs/basic-chart/helpers.js +55 -1
- package/dist/cjs/plotly/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/cjs/plotly/basic-chart/basic-chart.js +225 -140
- package/dist/cjs/plotly/plot.d.ts +12 -0
- package/dist/cjs/plotly/plot.d.ts.map +1 -0
- package/dist/cjs/plotly/plot.js +8 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/cjs/utils/axis-number-format.d.ts +14 -0
- package/dist/cjs/utils/axis-number-format.d.ts.map +1 -1
- package/dist/cjs/utils/axis-number-format.js +41 -1
- package/dist/cjs/utils/empty-cells.d.ts +9 -0
- package/dist/cjs/utils/empty-cells.d.ts.map +1 -1
- package/dist/cjs/utils/empty-cells.js +26 -1
- package/dist/cjs/utils/legend-layout.d.ts +168 -0
- package/dist/cjs/utils/legend-layout.d.ts.map +1 -0
- package/dist/cjs/utils/legend-layout.js +338 -0
- package/dist/cjs/utils/trendline.d.ts +18 -5
- package/dist/cjs/utils/trendline.d.ts.map +1 -1
- package/dist/cjs/utils/trendline.js +38 -16
- package/dist/esm/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/esm/basic-chart/basic-chart.js +118 -47
- package/dist/esm/basic-chart/helpers.d.ts +9 -0
- package/dist/esm/basic-chart/helpers.d.ts.map +1 -1
- package/dist/esm/basic-chart/helpers.js +30 -0
- package/dist/esm/plotly/basic-chart/basic-chart.d.ts.map +1 -1
- package/dist/esm/plotly/basic-chart/basic-chart.js +140 -13
- package/dist/esm/plotly/plot.d.ts +12 -0
- package/dist/esm/plotly/plot.d.ts.map +1 -0
- package/dist/esm/plotly/plot.js +12 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/esm/utils/axis-number-format.d.ts +14 -0
- package/dist/esm/utils/axis-number-format.d.ts.map +1 -1
- package/dist/esm/utils/axis-number-format.js +36 -0
- package/dist/esm/utils/empty-cells.d.ts +9 -0
- package/dist/esm/utils/empty-cells.d.ts.map +1 -1
- package/dist/esm/utils/empty-cells.js +21 -0
- package/dist/esm/utils/legend-layout.d.ts +168 -0
- package/dist/esm/utils/legend-layout.d.ts.map +1 -0
- package/dist/esm/utils/legend-layout.js +299 -0
- package/dist/esm/utils/trendline.d.ts +18 -5
- package/dist/esm/utils/trendline.d.ts.map +1 -1
- package/dist/esm/utils/trendline.js +36 -16
- 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
|
|
11
|
-
* category)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
|
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
|
|
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
|
|
12
|
-
* category)
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
234
|
-
var xStart = -backward;
|
|
235
|
-
var xEnd =
|
|
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
|
|
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) {
|
|
261
|
-
.
|
|
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
|
-
//
|
|
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.
|
|
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;
|
|
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"}
|