@trebco/treb 28.5.2 → 28.10.0
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/treb-spreadsheet-light.mjs +15 -15
- package/dist/treb-spreadsheet.mjs +15 -15
- package/dist/treb.d.ts +37 -6
- package/package.json +1 -1
- package/treb-base-types/src/api_types.ts +1 -1
- package/treb-base-types/src/area.ts +1 -1
- package/treb-base-types/src/basic_types.ts +21 -21
- package/treb-base-types/src/cell.ts +1 -1
- package/treb-base-types/src/cells.ts +1 -1
- package/treb-base-types/src/color.ts +1 -1
- package/treb-base-types/src/dom-utilities.ts +1 -1
- package/treb-base-types/src/gradient.ts +1 -1
- package/treb-base-types/src/import.ts +1 -1
- package/treb-base-types/src/index-standalone.ts +9 -9
- package/treb-base-types/src/index.ts +1 -1
- package/treb-base-types/src/layout.ts +21 -21
- package/treb-base-types/src/localization.ts +27 -21
- package/treb-base-types/src/rectangle.ts +1 -1
- package/treb-base-types/src/render_text.ts +1 -1
- package/treb-base-types/src/style.ts +1 -1
- package/treb-base-types/src/table.ts +1 -1
- package/treb-base-types/src/text_part.ts +21 -21
- package/treb-base-types/src/theme.ts +1 -1
- package/treb-base-types/src/union.ts +1 -1
- package/treb-base-types/src/value-type.ts +1 -1
- package/treb-base-types/style/resizable.css +21 -21
- package/treb-calculator/src/calculator.ts +24 -31
- package/treb-calculator/src/complex-math.ts +1 -1
- package/treb-calculator/src/dag/array-vertex.ts +1 -1
- package/treb-calculator/src/dag/calculation_leaf_vertex.ts +8 -1
- package/treb-calculator/src/dag/graph.ts +8 -1
- package/treb-calculator/src/dag/spreadsheet_vertex.ts +1 -1
- package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +21 -21
- package/treb-calculator/src/dag/state_leaf_vertex.ts +1 -1
- package/treb-calculator/src/dag/vertex.ts +21 -21
- package/treb-calculator/src/descriptors.ts +1 -1
- package/treb-calculator/src/expression-calculator.ts +1 -1
- package/treb-calculator/src/function-error.ts +1 -1
- package/treb-calculator/src/function-library.ts +1 -1
- package/treb-calculator/src/functions/base-functions.ts +31 -2
- package/treb-calculator/src/functions/checkbox.ts +1 -1
- package/treb-calculator/src/functions/complex-functions.ts +1 -1
- package/treb-calculator/src/functions/finance-functions.ts +1 -1
- package/treb-calculator/src/functions/information-functions.ts +1 -1
- package/treb-calculator/src/functions/matrix-functions.ts +1 -1
- package/treb-calculator/src/functions/sparkline.ts +1 -1
- package/treb-calculator/src/functions/statistics-functions.ts +1 -1
- package/treb-calculator/src/functions/text-functions.ts +1 -1
- package/treb-calculator/src/index.ts +1 -1
- package/treb-calculator/src/notifier-types.ts +1 -1
- package/treb-calculator/src/primitives.ts +1 -1
- package/treb-calculator/src/utilities.ts +1 -1
- package/treb-charts/src/chart-functions.ts +12 -1
- package/treb-charts/src/chart-types.ts +39 -21
- package/treb-charts/src/chart-utils.ts +783 -0
- package/treb-charts/src/chart.ts +96 -1291
- package/treb-charts/src/default-chart-renderer.ts +560 -0
- package/treb-charts/src/index.ts +5 -4
- package/treb-charts/src/main.ts +17 -17
- package/treb-charts/src/rectangle.ts +21 -21
- package/treb-charts/src/renderer-type.ts +32 -0
- package/treb-charts/src/renderer.ts +82 -1
- package/treb-charts/src/util.ts +1 -1
- package/treb-charts/style/charts.scss +9 -1
- package/treb-charts/style/old-charts.scss +21 -21
- package/treb-embed/markup/toolbar.html +35 -34
- package/treb-embed/src/custom-element/treb-global.ts +10 -2
- package/treb-embed/src/embedded-spreadsheet.ts +72 -113
- package/treb-embed/src/language-model.ts +1 -1
- package/treb-embed/src/options.ts +37 -1
- package/treb-embed/src/progress-dialog.ts +1 -1
- package/treb-embed/src/spinner.ts +1 -1
- package/treb-embed/src/types.ts +1 -1
- package/treb-embed/style/autocomplete.scss +1 -1
- package/treb-embed/style/dark-theme.scss +1 -1
- package/treb-embed/style/defaults.scss +1 -1
- package/treb-embed/style/dialog.scss +1 -1
- package/treb-embed/style/dropdown-select.scss +1 -1
- package/treb-embed/style/formula-bar.scss +1 -1
- package/treb-embed/style/grid.scss +1 -1
- package/treb-embed/style/layout.scss +4 -0
- package/treb-embed/style/mouse-mask.scss +1 -1
- package/treb-embed/style/note.scss +1 -1
- package/treb-embed/style/overlay-editor.scss +1 -1
- package/treb-embed/style/spinner.scss +1 -1
- package/treb-embed/style/tab-bar.scss +1 -1
- package/treb-embed/style/table.scss +1 -1
- package/treb-embed/style/theme-defaults.scss +1 -1
- package/treb-embed/style/toolbar.scss +37 -0
- package/treb-embed/style/tooltip.scss +1 -1
- package/treb-embed/style/z-index.scss +1 -1
- package/treb-export/src/address-type.ts +21 -21
- package/treb-export/src/base-template.ts +1 -1
- package/treb-export/src/column-width.ts +1 -1
- package/treb-export/src/drawing2/chart-template-components2.ts +1 -1
- package/treb-export/src/drawing2/chart2.ts +1 -1
- package/treb-export/src/drawing2/column-chart-template2.ts +1 -1
- package/treb-export/src/drawing2/donut-chart-template2.ts +1 -1
- package/treb-export/src/drawing2/drawing2.ts +1 -1
- package/treb-export/src/drawing2/embedded-image.ts +1 -1
- package/treb-export/src/drawing2/scatter-chart-template2.ts +1 -1
- package/treb-export/src/export-worker/export-worker.ts +1 -1
- package/treb-export/src/export-worker/index.worker.ts +1 -1
- package/treb-export/src/export2.ts +1 -1
- package/treb-export/src/import2.ts +1 -1
- package/treb-export/src/relationship.ts +1 -1
- package/treb-export/src/shared-strings2.ts +1 -1
- package/treb-export/src/template-2.ts +2 -2
- package/treb-export/src/workbook-sheet2.ts +1 -1
- package/treb-export/src/workbook-style2.ts +1 -1
- package/treb-export/src/workbook-theme2.ts +1 -1
- package/treb-export/src/workbook2.ts +1 -1
- package/treb-export/src/xml-utils.ts +1 -1
- package/treb-format/src/format.test.ts +21 -21
- package/treb-format/src/format.ts +1 -1
- package/treb-format/src/format_cache.ts +21 -21
- package/treb-format/src/format_parser.ts +1 -1
- package/treb-format/src/index.ts +4 -4
- package/treb-format/src/number_format_section.ts +21 -21
- package/treb-format/src/value_parser.ts +1 -1
- package/treb-grid/src/editors/autocomplete.ts +1 -1
- package/treb-grid/src/editors/autocomplete_matcher.ts +21 -21
- package/treb-grid/src/editors/editor.ts +1 -1
- package/treb-grid/src/editors/formula_bar.ts +1 -1
- package/treb-grid/src/editors/overlay_editor.ts +1 -1
- package/treb-grid/src/index.ts +1 -1
- package/treb-grid/src/layout/base_layout.ts +1 -1
- package/treb-grid/src/layout/grid_layout.ts +1 -1
- package/treb-grid/src/layout/rectangle_cache.ts +21 -21
- package/treb-grid/src/render/selection-renderer.ts +1 -1
- package/treb-grid/src/render/svg_header_overlay.ts +1 -1
- package/treb-grid/src/render/svg_selection_block.ts +1 -1
- package/treb-grid/src/render/tile_renderer.ts +1 -1
- package/treb-grid/src/types/annotation.ts +1 -1
- package/treb-grid/src/types/border_constants.ts +1 -1
- package/treb-grid/src/types/clipboard_data.ts +1 -1
- package/treb-grid/src/types/conditional_format.ts +1 -1
- package/treb-grid/src/types/data_model.ts +1 -1
- package/treb-grid/src/types/drag_mask.ts +21 -21
- package/treb-grid/src/types/grid.ts +12 -2
- package/treb-grid/src/types/grid_base.ts +128 -6
- package/treb-grid/src/types/grid_command.ts +33 -1
- package/treb-grid/src/types/grid_events.ts +8 -1
- package/treb-grid/src/types/grid_options.ts +9 -1
- package/treb-grid/src/types/grid_selection.ts +1 -1
- package/treb-grid/src/types/named_range.ts +1 -1
- package/treb-grid/src/types/scale-control.ts +1 -1
- package/treb-grid/src/types/serialize_options.ts +1 -1
- package/treb-grid/src/types/set_range_options.ts +1 -1
- package/treb-grid/src/types/sheet.ts +1 -57
- package/treb-grid/src/types/sheet_types.ts +1 -1
- package/treb-grid/src/types/tab_bar.ts +1 -1
- package/treb-grid/src/types/tile.ts +21 -21
- package/treb-grid/src/types/update_flags.ts +2 -1
- package/treb-grid/src/util/fontmetrics2.ts +1 -1
- package/treb-grid/src/util/ua.ts +21 -21
- package/treb-parser/src/csv-parser.ts +21 -21
- package/treb-parser/src/index.ts +5 -5
- package/treb-parser/src/md-parser.ts +1 -1
- package/treb-parser/src/parser-types.ts +1 -1
- package/treb-parser/src/parser.test.ts +21 -21
- package/treb-parser/src/parser.ts +1 -1
- package/treb-utils/src/event_source.ts +1 -1
- package/treb-utils/src/ievent_source.ts +13 -13
- package/treb-utils/src/index.ts +1 -1
- package/treb-utils/src/measurement.ts +1 -1
- package/treb-utils/src/scale.ts +21 -21
- package/treb-utils/src/serialize_html.ts +1 -1
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
|
|
2
|
+
import { type UnionValue, ValueType, type ArrayUnion } from 'treb-base-types';
|
|
3
|
+
import { LegendStyle } from './chart-types';
|
|
4
|
+
import type { SubSeries, SeriesType, BarData, ChartDataBaseType, ChartData, ScatterData2, LineData, DonutSlice } from './chart-types';
|
|
5
|
+
import { NumberFormatCache } from 'treb-format';
|
|
6
|
+
import { Util } from './util';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* this file is the concrete translation from function arguments
|
|
10
|
+
* to chart data. chart data is a (somewhat complicated) type with
|
|
11
|
+
* specializations for various chart types. we're splitting the
|
|
12
|
+
* generation of that data from the actual layout/rendering with
|
|
13
|
+
* a view towards building a new (or several new) renderers.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const DEFAULT_FORMAT = '#,##0.00'; // why not use "general", or whatever the usual default is?
|
|
17
|
+
|
|
18
|
+
export const ReadSeries = (data: Array<any>): SeriesType => {
|
|
19
|
+
|
|
20
|
+
// in this case it's (label, X, Y)
|
|
21
|
+
const series: SeriesType = {
|
|
22
|
+
x: { data: [] },
|
|
23
|
+
y: { data: [] },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
if (data[3] && typeof data[3] === 'number') {
|
|
27
|
+
series.index = data[3];
|
|
28
|
+
}
|
|
29
|
+
if (data[4]) {
|
|
30
|
+
series.subtype = data[4].toString();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (data[0]) {
|
|
34
|
+
|
|
35
|
+
const flat = Util.Flatten(data[0]);
|
|
36
|
+
|
|
37
|
+
// this could be a string, if it's a literal, or metadata
|
|
38
|
+
// [why would we want metadata?]
|
|
39
|
+
//
|
|
40
|
+
// OK, check that, should be a string (or other literal)
|
|
41
|
+
|
|
42
|
+
if (typeof flat[0] === 'object') {
|
|
43
|
+
series.label = flat[0]?.value?.toString() || '';
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
series.label = flat[0].toString();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// read [2] first, so we can default for [1] if necessary
|
|
51
|
+
|
|
52
|
+
if (!!data[2] && (typeof data[2] === 'object') && data[2].type === ValueType.array) {
|
|
53
|
+
const flat = Util.Flatten(data[2].value);
|
|
54
|
+
series.y.data = flat.map(item => typeof item.value.value === 'number' ? item.value.value : undefined);
|
|
55
|
+
if (flat[0].value?.format) {
|
|
56
|
+
series.y.format = flat[0].value?.format as string;
|
|
57
|
+
const format = NumberFormatCache.Get(series.y.format);
|
|
58
|
+
series.y.labels = series.y.data.map(value => (value === undefined) ? undefined : format.Format(value));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!!data[1] && (typeof data[1] === 'object') && data[1].type === ValueType.array) {
|
|
63
|
+
const flat = Util.Flatten(data[1].value);
|
|
64
|
+
series.x.data = flat.map(item => typeof item.value.value === 'number' ? item.value.value : undefined);
|
|
65
|
+
if (flat[0].value.format) {
|
|
66
|
+
series.x.format = flat[0].value.format;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const subseries of [series.x, series.y]) {
|
|
71
|
+
|
|
72
|
+
// in case of no values
|
|
73
|
+
if (subseries.data.length) {
|
|
74
|
+
const values = subseries.data.filter(value => value || value === 0) as number[];
|
|
75
|
+
subseries.range = {
|
|
76
|
+
min: Math.min.apply(0, values),
|
|
77
|
+
max: Math.max.apply(0, values),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return series;
|
|
83
|
+
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const ArrayToSeries = (array_data: ArrayUnion): SeriesType => {
|
|
87
|
+
|
|
88
|
+
// this is an array of Y, X not provided
|
|
89
|
+
|
|
90
|
+
const series: SeriesType = { x: { data: [] }, y: { data: [] }, };
|
|
91
|
+
const flat = Util.Flatten(array_data.value);
|
|
92
|
+
|
|
93
|
+
// series.y.data = flat.map(item => typeof item.value === 'number' ? item.value : undefined);
|
|
94
|
+
|
|
95
|
+
series.y.data = flat.map((item, index) => {
|
|
96
|
+
|
|
97
|
+
// if the data is passed in from the output of a function, it will not
|
|
98
|
+
// be inside a metadata structure
|
|
99
|
+
|
|
100
|
+
if (typeof item.value === 'number') { return item.value; }
|
|
101
|
+
|
|
102
|
+
// ... ok, it's metadata (why not just test?) ...
|
|
103
|
+
|
|
104
|
+
// experimenting with complex... put real in X axis and imaginary in Y axis
|
|
105
|
+
// note should also function w/ complex not in a metadata structure
|
|
106
|
+
|
|
107
|
+
if (typeof item.value.value?.real === 'number') {
|
|
108
|
+
series.x.data[index] = item.value.value.real;
|
|
109
|
+
return item.value.value.imaginary;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return typeof item.value.value === 'number' ? item.value.value : undefined;
|
|
113
|
+
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (flat[0].value.format) {
|
|
117
|
+
series.y.format = flat[0].value.format || '';
|
|
118
|
+
const format = NumberFormatCache.Get(series.y.format || '');
|
|
119
|
+
series.y.labels = series.y.data.map(value => (value === undefined) ? undefined : format.Format(value));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const values = series.y.data.filter(value => value || value === 0) as number[];
|
|
123
|
+
series.y.range = {
|
|
124
|
+
min: Math.min.apply(0, values),
|
|
125
|
+
max: Math.max.apply(0, values),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// experimenting with complex... this should only be set if we populated
|
|
129
|
+
// it from complex values
|
|
130
|
+
|
|
131
|
+
if (series.x.data.length) {
|
|
132
|
+
|
|
133
|
+
const filtered: number[] = series.x.data.filter(test => typeof test === 'number') as number[];
|
|
134
|
+
series.x.range = {
|
|
135
|
+
min: Math.min.apply(0, filtered),
|
|
136
|
+
max: Math.max.apply(0, filtered),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (flat[0].value.format) {
|
|
140
|
+
series.x.format = flat[0].value.format || '';
|
|
141
|
+
const format = NumberFormatCache.Get(series.x.format || '');
|
|
142
|
+
series.x.labels = series.x.data.map(value => (value === undefined) ? undefined : format.Format(value));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return series;
|
|
148
|
+
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* composite data -> series. composite data can be
|
|
153
|
+
*
|
|
154
|
+
* (1) set of Y values, with X not provided;
|
|
155
|
+
* (2) SERIES(label, X, Y) with Y required, others optional
|
|
156
|
+
* (3) GROUP(a, b, ...), where entries are either arrays as (1) or SERIES as (2)
|
|
157
|
+
*
|
|
158
|
+
* FIXME: consider supporting GROUP(SERIES, [y], ...)
|
|
159
|
+
*
|
|
160
|
+
* NOTE: (1) could be an array of boxed (union) values...
|
|
161
|
+
*
|
|
162
|
+
*/
|
|
163
|
+
export const TransformSeriesData = (raw_data?: UnionValue, default_x?: UnionValue): SeriesType[] => {
|
|
164
|
+
|
|
165
|
+
if (!raw_data) { return []; }
|
|
166
|
+
|
|
167
|
+
const list: SeriesType[] = [];
|
|
168
|
+
|
|
169
|
+
if (raw_data.type === ValueType.object) {
|
|
170
|
+
if (raw_data.key === 'group') {
|
|
171
|
+
if (Array.isArray(raw_data.value)) {
|
|
172
|
+
for (const entry of raw_data.value) {
|
|
173
|
+
if (!!entry && (typeof entry === 'object')) {
|
|
174
|
+
if (entry.key === 'series') {
|
|
175
|
+
const series = ReadSeries(entry.value);
|
|
176
|
+
list.push(series);
|
|
177
|
+
}
|
|
178
|
+
else if (entry.type === ValueType.array) {
|
|
179
|
+
list.push(ArrayToSeries(entry));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else if (raw_data.key === 'series') {
|
|
186
|
+
const series = ReadSeries(raw_data.value);
|
|
187
|
+
list.push(series);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (raw_data.type === ValueType.array) {
|
|
191
|
+
list.push(ArrayToSeries(raw_data));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// now we may or may not have X for each series, so we need
|
|
195
|
+
// to patch. it's also possible (as with older chart functions)
|
|
196
|
+
// that there's a common X -- not sure if we want to continue
|
|
197
|
+
// to support that or not...
|
|
198
|
+
|
|
199
|
+
let baseline_x: SubSeries | undefined;
|
|
200
|
+
let max_y_length = 0;
|
|
201
|
+
|
|
202
|
+
// if we have a default, use that (and range it)
|
|
203
|
+
|
|
204
|
+
if (default_x?.type === ValueType.array) {
|
|
205
|
+
|
|
206
|
+
const values = Util.Flatten(default_x.value);
|
|
207
|
+
|
|
208
|
+
let format = '0.00###';
|
|
209
|
+
|
|
210
|
+
if (values[0] && values[0].type === ValueType.object) { // UnionIs.Extended(values[0])) {
|
|
211
|
+
format = values[0].value.format;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const data = values.map(x => {
|
|
215
|
+
if (x.type === ValueType.number) { return x.value; }
|
|
216
|
+
if (x.type === ValueType.object) { // ??
|
|
217
|
+
// if (UnionIs.Extended(x)) { // ?
|
|
218
|
+
return x.value.value;
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
}) as Array<number | undefined>;
|
|
222
|
+
|
|
223
|
+
const filtered = data.filter(x => typeof x === 'number') as number[];
|
|
224
|
+
|
|
225
|
+
baseline_x = {
|
|
226
|
+
data,
|
|
227
|
+
format,
|
|
228
|
+
range: {
|
|
229
|
+
min: Math.min.apply(0, filtered),
|
|
230
|
+
max: Math.max.apply(0, filtered),
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// look for the first set that has values. at the same time, get max len
|
|
236
|
+
|
|
237
|
+
for (const entry of list) {
|
|
238
|
+
max_y_length = Math.max(max_y_length, entry.y.data.length);
|
|
239
|
+
if (entry.x.data.length) {
|
|
240
|
+
if (!baseline_x) {
|
|
241
|
+
baseline_x = entry.x;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// now default for any series missing X
|
|
247
|
+
|
|
248
|
+
if (!baseline_x) {
|
|
249
|
+
baseline_x = {
|
|
250
|
+
data: [],
|
|
251
|
+
range: {
|
|
252
|
+
min: 0,
|
|
253
|
+
max: Math.max(0, max_y_length - 1),
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
for (let i = 0; i < max_y_length; i++) { baseline_x.data.push(i); }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
for (const entry of list) {
|
|
260
|
+
if (!entry.x.data.length) {
|
|
261
|
+
entry.x = baseline_x;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return list;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/** get a unified scale, and formats */
|
|
269
|
+
export const CommonData = (series: SeriesType[], y_floor?: number, y_ceiling?: number) => {
|
|
270
|
+
|
|
271
|
+
let x_format = '';
|
|
272
|
+
let y_format = '';
|
|
273
|
+
|
|
274
|
+
for (const entry of series) {
|
|
275
|
+
if (entry.y.format && !y_format) { y_format = entry.y.format; }
|
|
276
|
+
if (entry.x.format && !x_format) { x_format = entry.x.format; }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let legend: Array<{ label: string, index?: number }> | undefined; // string[]|undefined;
|
|
280
|
+
if (series.some(test => test.label && (test.label.length > 0))) {
|
|
281
|
+
legend = series.map((entry, i) => ({
|
|
282
|
+
label: entry.label || `Series ${i + 1}`,
|
|
283
|
+
index: typeof entry.index === 'number' ? entry.index : i + 1,
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const x = series.filter(test => test.x.range);
|
|
288
|
+
const x_min = Math.min.apply(0, x.map(test => test.x.range?.min || 0));
|
|
289
|
+
const x_max = Math.max.apply(0, x.map(test => test.x.range?.max || 0));
|
|
290
|
+
|
|
291
|
+
const y = series.filter(test => test.y.range);
|
|
292
|
+
let y_min = Math.min.apply(0, x.map(test => test.y.range?.min || 0));
|
|
293
|
+
let y_max = Math.max.apply(0, x.map(test => test.y.range?.max || 0));
|
|
294
|
+
|
|
295
|
+
if (typeof y_floor !== 'undefined') {
|
|
296
|
+
y_min = Math.min(y_min, y_floor);
|
|
297
|
+
}
|
|
298
|
+
if (typeof y_ceiling !== 'undefined') {
|
|
299
|
+
y_max = Math.max(y_max, y_ceiling);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const x_scale = Util.Scale(x_min, x_max, 7);
|
|
303
|
+
const y_scale = Util.Scale(y_min, y_max, 7);
|
|
304
|
+
|
|
305
|
+
let x_labels: string[] | undefined;
|
|
306
|
+
let y_labels: string[] | undefined;
|
|
307
|
+
|
|
308
|
+
if (x_format) {
|
|
309
|
+
x_labels = [];
|
|
310
|
+
const format = NumberFormatCache.Get(x_format);
|
|
311
|
+
for (let i = 0; i <= x_scale.count; i++) {
|
|
312
|
+
x_labels.push(format.Format(x_scale.min + i * x_scale.step));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (y_format) {
|
|
317
|
+
y_labels = [];
|
|
318
|
+
const format = NumberFormatCache.Get(y_format);
|
|
319
|
+
for (let i = 0; i <= y_scale.count; i++) {
|
|
320
|
+
y_labels.push(format.Format(y_scale.min + i * y_scale.step));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
x: {
|
|
326
|
+
format: x_format,
|
|
327
|
+
scale: x_scale,
|
|
328
|
+
labels: x_labels,
|
|
329
|
+
},
|
|
330
|
+
y: {
|
|
331
|
+
format: y_format,
|
|
332
|
+
scale: y_scale,
|
|
333
|
+
labels: y_labels,
|
|
334
|
+
},
|
|
335
|
+
legend,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const ApplyLabels = (series_list: SeriesType[], pattern: string, category_labels?: string[]): void => {
|
|
341
|
+
|
|
342
|
+
for (const series of series_list) {
|
|
343
|
+
|
|
344
|
+
const format = {
|
|
345
|
+
x: NumberFormatCache.Get(series.x.format || ''),
|
|
346
|
+
y: NumberFormatCache.Get(series.y.format || ''),
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
series.y.labels = [];
|
|
350
|
+
|
|
351
|
+
for (let i = 0; i < series.y.data.length; i++) {
|
|
352
|
+
|
|
353
|
+
const x = category_labels ? category_labels[i] :
|
|
354
|
+
(typeof series.x.data[i] === 'number' ? format.x.Format(series.x.data[i]) : '');
|
|
355
|
+
const y = typeof series.y.data[i] === 'number' ? format.y.Format(series.y.data[i]) : '';
|
|
356
|
+
|
|
357
|
+
series.y.labels[i] = pattern.replace(/\bx\b/g, x).replace(/\by\b/g, y);
|
|
358
|
+
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
//------------------------------------------------------------------------------
|
|
366
|
+
|
|
367
|
+
export const CreateBubbleChart = (args: UnionValue[]): ChartData => {
|
|
368
|
+
|
|
369
|
+
const [x, y, z] = [0,1,2].map(index => {
|
|
370
|
+
const arg = args[index];
|
|
371
|
+
if (arg.type === ValueType.array) {
|
|
372
|
+
return ArrayToSeries(arg).y;
|
|
373
|
+
}
|
|
374
|
+
return undefined;
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
let c: string[]|undefined = undefined;
|
|
378
|
+
if (Array.isArray(args[3])) {
|
|
379
|
+
c = Util.Flatten(args[3]).map(value => (value||'').toString());
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const title = args[4]?.toString() || undefined;
|
|
383
|
+
|
|
384
|
+
// FIXME: need to pad out the axes by the values at the edges,
|
|
385
|
+
// so the whole circle is included in the chart area.
|
|
386
|
+
|
|
387
|
+
const [x_scale, y_scale] = [x, y].map(subseries => {
|
|
388
|
+
|
|
389
|
+
let series_min = 0;
|
|
390
|
+
let series_max = 1;
|
|
391
|
+
let first = false;
|
|
392
|
+
|
|
393
|
+
if (subseries?.data) {
|
|
394
|
+
|
|
395
|
+
for (const [index, value] of subseries.data.entries()) {
|
|
396
|
+
if (typeof value === 'number') {
|
|
397
|
+
|
|
398
|
+
if (!first) {
|
|
399
|
+
first = true;
|
|
400
|
+
series_min = value;
|
|
401
|
+
series_max = value;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const size = (z?.data?.[index]) || 0;
|
|
405
|
+
series_min = Math.min(series_min, value - size / 2);
|
|
406
|
+
series_max = Math.max(series_max, value + size / 2);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return Util.Scale(series_min, series_max, 7);
|
|
412
|
+
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
let x_labels: string[] | undefined;
|
|
416
|
+
let y_labels: string[] | undefined;
|
|
417
|
+
|
|
418
|
+
if (x?.format) {
|
|
419
|
+
x_labels = [];
|
|
420
|
+
const format = NumberFormatCache.Get(x.format);
|
|
421
|
+
for (let i = 0; i <= x_scale.count; i++) {
|
|
422
|
+
x_labels.push(format.Format(x_scale.min + i * x_scale.step));
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (y?.format) {
|
|
427
|
+
y_labels = [];
|
|
428
|
+
const format = NumberFormatCache.Get(y.format);
|
|
429
|
+
for (let i = 0; i <= y_scale.count; i++) {
|
|
430
|
+
y_labels.push(format.Format(y_scale.min + i * y_scale.step));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
|
|
436
|
+
type: 'bubble',
|
|
437
|
+
|
|
438
|
+
title,
|
|
439
|
+
|
|
440
|
+
x,
|
|
441
|
+
y,
|
|
442
|
+
z,
|
|
443
|
+
c,
|
|
444
|
+
|
|
445
|
+
x_scale,
|
|
446
|
+
y_scale,
|
|
447
|
+
|
|
448
|
+
x_labels,
|
|
449
|
+
y_labels,
|
|
450
|
+
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* args is [data, title, options]
|
|
457
|
+
*
|
|
458
|
+
* args[0] is the scatter data. this can be
|
|
459
|
+
*
|
|
460
|
+
* (1) set of Y values, with X not provided;
|
|
461
|
+
* (2) SERIES(label, X, Y) with Y required, others optional
|
|
462
|
+
* (3) GROUP(SERIES(label, X, Y), SERIES(label, X, Y), ...), with same rule for each series
|
|
463
|
+
*
|
|
464
|
+
* @param args
|
|
465
|
+
*/
|
|
466
|
+
export const CreateScatterChart = (args: any[], style: 'plot' | 'line' = 'plot'): ChartData => {
|
|
467
|
+
|
|
468
|
+
// FIXME: transform the data, then have this function
|
|
469
|
+
// operate on clean data. that way the transform can
|
|
470
|
+
// be reused (and the function can be reused without the
|
|
471
|
+
// transform).
|
|
472
|
+
|
|
473
|
+
const series: SeriesType[] = TransformSeriesData(args[0]);
|
|
474
|
+
|
|
475
|
+
const common = CommonData(series);
|
|
476
|
+
|
|
477
|
+
const title = args[1]?.toString() || undefined;
|
|
478
|
+
const options = args[2]?.toString() || undefined;
|
|
479
|
+
|
|
480
|
+
const chart_data: ScatterData2 = {
|
|
481
|
+
legend: common.legend,
|
|
482
|
+
style,
|
|
483
|
+
type: 'scatter2',
|
|
484
|
+
series, // : [{x, y}],
|
|
485
|
+
title,
|
|
486
|
+
|
|
487
|
+
x_scale: common.x.scale,
|
|
488
|
+
x_labels: common.x.labels,
|
|
489
|
+
|
|
490
|
+
y_scale: common.y.scale,
|
|
491
|
+
y_labels: common.y.labels,
|
|
492
|
+
|
|
493
|
+
lines: style === 'line', // true,
|
|
494
|
+
points: style === 'plot',
|
|
495
|
+
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
if (options) {
|
|
499
|
+
|
|
500
|
+
chart_data.markers = /marker/i.test(options);
|
|
501
|
+
chart_data.smooth = /smooth/i.test(options);
|
|
502
|
+
chart_data.data_labels = /labels/i.test(options);
|
|
503
|
+
|
|
504
|
+
let match = options.match(/labels="(.*?)"/);
|
|
505
|
+
if (match && chart_data.series) {
|
|
506
|
+
ApplyLabels(chart_data.series, match[1]);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
match = options.match(/labels=([^\s\r\n,]+)(?:\W|$)/);
|
|
510
|
+
if (match && chart_data.series) {
|
|
511
|
+
ApplyLabels(chart_data.series, match[1]);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
match = options.match(/class=([\w_-]+)(?:\W|$)/);
|
|
516
|
+
if (match) {
|
|
517
|
+
chart_data.class_name = match[1];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return chart_data;
|
|
523
|
+
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* column/bar chart, now using common Series data and routines
|
|
529
|
+
*
|
|
530
|
+
* @param args arguments: data, categories, title, options
|
|
531
|
+
* @param type
|
|
532
|
+
*/
|
|
533
|
+
export const CreateColumnChart = (args: [UnionValue?, UnionValue?, string?, string?], type: 'bar' | 'column'): ChartData => {
|
|
534
|
+
|
|
535
|
+
const series: SeriesType[] = TransformSeriesData(args[0]);
|
|
536
|
+
const common = CommonData(series);
|
|
537
|
+
|
|
538
|
+
let category_labels: string[] | undefined;
|
|
539
|
+
|
|
540
|
+
if (args[1]) {
|
|
541
|
+
|
|
542
|
+
const values = args[1].type === ValueType.array ? Util.Flatten(args[1].value) : Util.Flatten(args[1]);
|
|
543
|
+
category_labels = values.map((cell) => {
|
|
544
|
+
if (!cell) { return ''; }
|
|
545
|
+
|
|
546
|
+
if (cell.type === ValueType.object && cell.value.type === 'metadata') {
|
|
547
|
+
if (typeof cell.value.value === 'number') {
|
|
548
|
+
const format = NumberFormatCache.Get(cell.value.format || DEFAULT_FORMAT);
|
|
549
|
+
return format.Format(cell.value.value);
|
|
550
|
+
}
|
|
551
|
+
return cell.value.value;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (typeof cell.value === 'number') {
|
|
555
|
+
const format = NumberFormatCache.Get(cell.format || DEFAULT_FORMAT);
|
|
556
|
+
return format.Format(cell.value);
|
|
557
|
+
}
|
|
558
|
+
return cell.value;
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
const count = series.reduce((a, entry) => Math.max(a, entry.y.data.length), 0);
|
|
562
|
+
|
|
563
|
+
if (count < category_labels.length) {
|
|
564
|
+
category_labels = category_labels.slice(0, count);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
while (count > category_labels.length) { category_labels.push(''); }
|
|
568
|
+
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const title = args[2]?.toString() || undefined;
|
|
572
|
+
const options = args[3]?.toString() || undefined;
|
|
573
|
+
|
|
574
|
+
const chart_data = {
|
|
575
|
+
type,
|
|
576
|
+
legend: common.legend,
|
|
577
|
+
// legend_position: LegendPosition.right,
|
|
578
|
+
legend_style: LegendStyle.marker,
|
|
579
|
+
series2: series,
|
|
580
|
+
scale: common.y.scale,
|
|
581
|
+
title,
|
|
582
|
+
y_labels: type === 'bar' ? category_labels : common.y.labels, // swapped
|
|
583
|
+
x_labels: type === 'bar' ? common.y.labels : category_labels, // swapped
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
if (options) {
|
|
587
|
+
(chart_data as BarData).round = /round/i.test(options);
|
|
588
|
+
(chart_data as ChartDataBaseType).data_labels = /labels/i.test(options);
|
|
589
|
+
|
|
590
|
+
let match = options.match(/labels="(.*?)"/);
|
|
591
|
+
if (match && series) {
|
|
592
|
+
ApplyLabels(series, match[1], category_labels);
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
match = options.match(/labels=([^\s\r\n,]+)(?:\W|$)/);
|
|
596
|
+
if (match && series) {
|
|
597
|
+
ApplyLabels(series, match[1], category_labels);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
match = options.match(/class=([\w_-]+)(?:\W|$)/);
|
|
603
|
+
if (match) {
|
|
604
|
+
(chart_data as ChartDataBaseType).class_name = match[1];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
return chart_data;
|
|
610
|
+
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* args: data, labels, title, callouts, "smooth"
|
|
616
|
+
*/
|
|
617
|
+
export const CreateLineChart = (args: any[], type: 'line' | 'area'): ChartData => {
|
|
618
|
+
|
|
619
|
+
const series: SeriesType[] = TransformSeriesData(args[0], args[1]);
|
|
620
|
+
const common = CommonData(series, 0, 0);
|
|
621
|
+
|
|
622
|
+
const title = args[2]?.toString() || undefined;
|
|
623
|
+
const options = args[3]?.toString() || undefined;
|
|
624
|
+
|
|
625
|
+
const chart_data: ChartData = {
|
|
626
|
+
legend: common.legend,
|
|
627
|
+
// style: type, // 'line',
|
|
628
|
+
type: 'scatter2',
|
|
629
|
+
series, // : [{x, y}],
|
|
630
|
+
title,
|
|
631
|
+
|
|
632
|
+
x_scale: common.x.scale,
|
|
633
|
+
x_labels: common.x.labels,
|
|
634
|
+
|
|
635
|
+
y_scale: common.y.scale,
|
|
636
|
+
y_labels: common.y.labels,
|
|
637
|
+
|
|
638
|
+
lines: true,
|
|
639
|
+
filled: type === 'area',
|
|
640
|
+
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
if (options) {
|
|
644
|
+
// this.chart_data.markers = /marker/i.test(options);
|
|
645
|
+
chart_data.smooth = /smooth/i.test(options);
|
|
646
|
+
// this.chart_data.data_labels = /labels/i.test(options);
|
|
647
|
+
|
|
648
|
+
const match = options.match(/class=([\w_-]+)(?:\W|$)/);
|
|
649
|
+
if (match) {
|
|
650
|
+
chart_data.class_name = match[1];
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return chart_data;
|
|
656
|
+
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* arguments are values, labels, title, sort, label option, ...
|
|
661
|
+
*/
|
|
662
|
+
export const CreateDonut = (args: [UnionValue?, UnionValue?, string?, string?, string?], pie_chart = false): ChartData => {
|
|
663
|
+
|
|
664
|
+
const raw_data = args[0]?.type === ValueType.array ? args[0].value : args[0];
|
|
665
|
+
|
|
666
|
+
// we're now expecting this to be metadata (including value).
|
|
667
|
+
// so we need to unpack. could be an array... could be deep...
|
|
668
|
+
const flat = Util.Flatten(raw_data);
|
|
669
|
+
|
|
670
|
+
// we still need the aggregate for range, scale
|
|
671
|
+
let data = flat.map((x) => (typeof x.value.value === 'number') ? x.value.value : undefined) as number[];
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
// if labels are strings, just pass them in. if they're numbers then
|
|
675
|
+
// use the format (we're collecting metadata for this field now)
|
|
676
|
+
|
|
677
|
+
const raw_labels = args[1]?.type === ValueType.array ? args[1].value : args[1];
|
|
678
|
+
|
|
679
|
+
const labels = Util.Flatten(raw_labels).map((label) => {
|
|
680
|
+
if (label && typeof label === 'object') {
|
|
681
|
+
const value = label.value?.value;
|
|
682
|
+
if (typeof value === 'number' && label.value?.format) {
|
|
683
|
+
return NumberFormatCache.Get(label.value?.format).Format(value);
|
|
684
|
+
}
|
|
685
|
+
else return value ? value.toString() : '';
|
|
686
|
+
}
|
|
687
|
+
else return label ? label.toString() : '';
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// no negative numbers
|
|
691
|
+
|
|
692
|
+
data = data.map((check) => {
|
|
693
|
+
if (check < 0) {
|
|
694
|
+
console.warn('pie/donut chart does not support negative values (omitted)');
|
|
695
|
+
return 0;
|
|
696
|
+
}
|
|
697
|
+
return check;
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
const title = args[2] || '';
|
|
701
|
+
|
|
702
|
+
let sum = 0;
|
|
703
|
+
|
|
704
|
+
const slices: DonutSlice[] = data.map((value, i) => {
|
|
705
|
+
if (typeof value !== 'undefined') sum += value;
|
|
706
|
+
return { value, label: labels[i] || '', index: i + 1, percent: 0 };
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
if (sum) {
|
|
710
|
+
for (const slice of slices) {
|
|
711
|
+
slice.percent = (slice.value || 0) / sum;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// titles? label/value/percent
|
|
716
|
+
// FIXME: number format(s)
|
|
717
|
+
|
|
718
|
+
const format_pattern = (flat.length && flat[0].value?.format) ? flat[0].value.format : '';
|
|
719
|
+
const format = NumberFormatCache.Get(format_pattern || DEFAULT_FORMAT);
|
|
720
|
+
const percent_format = NumberFormatCache.Get('percent');
|
|
721
|
+
|
|
722
|
+
// ensure label if we have labels array but no label format string
|
|
723
|
+
|
|
724
|
+
if (typeof args[4] === 'undefined' && args[1]) {
|
|
725
|
+
args[4] = 'label';
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const slice_title = (args[4] || '');
|
|
729
|
+
if (slice_title) {
|
|
730
|
+
for (const slice of slices) {
|
|
731
|
+
const value = /*NumberFormatCache.Get('general')*/ format.Format(slice.value || 0);
|
|
732
|
+
const percent = percent_format.Format(slice.percent);
|
|
733
|
+
slice.title = slice_title
|
|
734
|
+
.replace(/value%/ig, percent_format.Format(slice.value || 0))
|
|
735
|
+
.replace(/value/ig, value)
|
|
736
|
+
.replace(/percent/ig, percent)
|
|
737
|
+
.replace(/label/ig, slice.label || '')
|
|
738
|
+
.trim();
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// optionally sort...
|
|
743
|
+
|
|
744
|
+
const options = (args[3] || '').toString().trim();
|
|
745
|
+
|
|
746
|
+
// old-style...
|
|
747
|
+
|
|
748
|
+
let sort = options.toUpperCase();
|
|
749
|
+
if (sort === 'ASC' || sort === 'ASCENDING' || sort === 'INC') {
|
|
750
|
+
slices.sort((a, b) => { return (a.value || 0) - (b.value || 0); });
|
|
751
|
+
}
|
|
752
|
+
else if (sort === 'DESC' || sort === 'DESCENDING' || sort === 'DEC') {
|
|
753
|
+
slices.sort((a, b) => { return (b.value || 0) - (a.value || 0); });
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
const match = options.match(/sort=([\w]+)(?:\W|$)/i);
|
|
757
|
+
if (match) {
|
|
758
|
+
sort = match[1];
|
|
759
|
+
if (/^(asc|inc)/i.test(sort)) {
|
|
760
|
+
slices.sort((a, b) => { return (a.value || 0) - (b.value || 0); });
|
|
761
|
+
}
|
|
762
|
+
else if (/^(desc|dec)/i.test(sort)) {
|
|
763
|
+
slices.sort((a, b) => { return (b.value || 0) - (a.value || 0); });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const chart_data: ChartData = {
|
|
769
|
+
type: pie_chart ? 'pie' : 'donut',
|
|
770
|
+
slices,
|
|
771
|
+
title,
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
if (options) {
|
|
775
|
+
const match = options.match(/class=([_-\w]+)(?:\W|$)/);
|
|
776
|
+
if (match) {
|
|
777
|
+
chart_data.class_name = match[1];
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
return chart_data;
|
|
782
|
+
|
|
783
|
+
};
|