@trebco/treb 28.5.1 → 28.7.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.
Files changed (162) hide show
  1. package/dist/treb-spreadsheet-light.mjs +15 -15
  2. package/dist/treb-spreadsheet.mjs +15 -15
  3. package/dist/treb.d.ts +1 -1
  4. package/package.json +1 -1
  5. package/treb-base-types/src/api_types.ts +1 -1
  6. package/treb-base-types/src/area.ts +1 -1
  7. package/treb-base-types/src/basic_types.ts +21 -21
  8. package/treb-base-types/src/cell.ts +1 -1
  9. package/treb-base-types/src/cells.ts +1 -1
  10. package/treb-base-types/src/color.ts +1 -1
  11. package/treb-base-types/src/dom-utilities.ts +1 -1
  12. package/treb-base-types/src/import.ts +1 -1
  13. package/treb-base-types/src/index-standalone.ts +9 -9
  14. package/treb-base-types/src/index.ts +1 -1
  15. package/treb-base-types/src/layout.ts +21 -21
  16. package/treb-base-types/src/localization.ts +21 -21
  17. package/treb-base-types/src/rectangle.ts +1 -1
  18. package/treb-base-types/src/render_text.ts +1 -1
  19. package/treb-base-types/src/style.ts +1 -1
  20. package/treb-base-types/src/table.ts +1 -1
  21. package/treb-base-types/src/text_part.ts +21 -21
  22. package/treb-base-types/src/theme.ts +1 -1
  23. package/treb-base-types/src/union.ts +1 -1
  24. package/treb-base-types/src/value-type.ts +1 -1
  25. package/treb-base-types/style/resizable.css +21 -21
  26. package/treb-calculator/src/calculator.ts +1 -1
  27. package/treb-calculator/src/complex-math.ts +1 -1
  28. package/treb-calculator/src/dag/array-vertex.ts +1 -1
  29. package/treb-calculator/src/dag/calculation_leaf_vertex.ts +1 -1
  30. package/treb-calculator/src/dag/graph.ts +1 -1
  31. package/treb-calculator/src/dag/spreadsheet_vertex.ts +1 -1
  32. package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +21 -21
  33. package/treb-calculator/src/dag/state_leaf_vertex.ts +1 -1
  34. package/treb-calculator/src/dag/vertex.ts +21 -21
  35. package/treb-calculator/src/descriptors.ts +1 -1
  36. package/treb-calculator/src/expression-calculator.ts +1 -1
  37. package/treb-calculator/src/function-error.ts +1 -1
  38. package/treb-calculator/src/function-library.ts +1 -1
  39. package/treb-calculator/src/functions/base-functions.ts +1 -1
  40. package/treb-calculator/src/functions/checkbox.ts +1 -1
  41. package/treb-calculator/src/functions/complex-functions.ts +1 -1
  42. package/treb-calculator/src/functions/finance-functions.ts +1 -1
  43. package/treb-calculator/src/functions/information-functions.ts +1 -1
  44. package/treb-calculator/src/functions/matrix-functions.ts +1 -1
  45. package/treb-calculator/src/functions/sparkline.ts +1 -1
  46. package/treb-calculator/src/functions/statistics-functions.ts +1 -1
  47. package/treb-calculator/src/functions/text-functions.ts +1 -1
  48. package/treb-calculator/src/index.ts +1 -1
  49. package/treb-calculator/src/notifier-types.ts +1 -1
  50. package/treb-calculator/src/primitives.ts +1 -1
  51. package/treb-calculator/src/utilities.ts +1 -1
  52. package/treb-charts/src/chart-functions.ts +1 -1
  53. package/treb-charts/src/chart-types.ts +21 -21
  54. package/treb-charts/src/chart-utils.ts +696 -0
  55. package/treb-charts/src/chart.ts +92 -1291
  56. package/treb-charts/src/default-chart-renderer.ts +535 -0
  57. package/treb-charts/src/index.ts +5 -4
  58. package/treb-charts/src/main.ts +17 -17
  59. package/treb-charts/src/rectangle.ts +21 -21
  60. package/treb-charts/src/renderer-type.ts +32 -0
  61. package/treb-charts/src/renderer.ts +1 -1
  62. package/treb-charts/src/util.ts +1 -1
  63. package/treb-charts/style/charts.scss +1 -1
  64. package/treb-charts/style/old-charts.scss +21 -21
  65. package/treb-embed/src/embedded-spreadsheet.ts +15 -12
  66. package/treb-embed/src/language-model.ts +1 -1
  67. package/treb-embed/src/options.ts +30 -1
  68. package/treb-embed/src/progress-dialog.ts +1 -1
  69. package/treb-embed/src/spinner.ts +1 -1
  70. package/treb-embed/src/types.ts +1 -1
  71. package/treb-embed/style/autocomplete.scss +1 -1
  72. package/treb-embed/style/dark-theme.scss +1 -1
  73. package/treb-embed/style/defaults.scss +1 -1
  74. package/treb-embed/style/dialog.scss +1 -1
  75. package/treb-embed/style/dropdown-select.scss +1 -1
  76. package/treb-embed/style/formula-bar.scss +1 -1
  77. package/treb-embed/style/grid.scss +1 -1
  78. package/treb-embed/style/mouse-mask.scss +1 -1
  79. package/treb-embed/style/note.scss +1 -1
  80. package/treb-embed/style/overlay-editor.scss +1 -1
  81. package/treb-embed/style/spinner.scss +1 -1
  82. package/treb-embed/style/tab-bar.scss +1 -1
  83. package/treb-embed/style/table.scss +1 -1
  84. package/treb-embed/style/theme-defaults.scss +1 -1
  85. package/treb-embed/style/tooltip.scss +1 -1
  86. package/treb-embed/style/z-index.scss +1 -1
  87. package/treb-export/src/address-type.ts +21 -21
  88. package/treb-export/src/base-template.ts +1 -1
  89. package/treb-export/src/column-width.ts +1 -1
  90. package/treb-export/src/drawing2/chart-template-components2.ts +1 -1
  91. package/treb-export/src/drawing2/chart2.ts +1 -1
  92. package/treb-export/src/drawing2/column-chart-template2.ts +1 -1
  93. package/treb-export/src/drawing2/donut-chart-template2.ts +1 -1
  94. package/treb-export/src/drawing2/drawing2.ts +1 -1
  95. package/treb-export/src/drawing2/embedded-image.ts +1 -1
  96. package/treb-export/src/drawing2/scatter-chart-template2.ts +1 -1
  97. package/treb-export/src/export-worker/export-worker.ts +1 -1
  98. package/treb-export/src/export-worker/index.worker.ts +1 -1
  99. package/treb-export/src/export2.ts +1 -1
  100. package/treb-export/src/import2.ts +1 -1
  101. package/treb-export/src/relationship.ts +1 -1
  102. package/treb-export/src/shared-strings2.ts +1 -1
  103. package/treb-export/src/template-2.ts +2 -2
  104. package/treb-export/src/workbook-sheet2.ts +1 -1
  105. package/treb-export/src/workbook-style2.ts +1 -1
  106. package/treb-export/src/workbook-theme2.ts +1 -1
  107. package/treb-export/src/workbook2.ts +1 -1
  108. package/treb-export/src/xml-utils.ts +1 -1
  109. package/treb-format/src/format.test.ts +21 -21
  110. package/treb-format/src/format.ts +1 -1
  111. package/treb-format/src/format_cache.ts +21 -21
  112. package/treb-format/src/format_parser.ts +1 -1
  113. package/treb-format/src/index.ts +4 -4
  114. package/treb-format/src/number_format_section.ts +21 -21
  115. package/treb-format/src/value_parser.ts +1 -1
  116. package/treb-grid/src/editors/autocomplete.ts +1 -1
  117. package/treb-grid/src/editors/autocomplete_matcher.ts +21 -21
  118. package/treb-grid/src/editors/editor.ts +1 -1
  119. package/treb-grid/src/editors/formula_bar.ts +1 -1
  120. package/treb-grid/src/editors/overlay_editor.ts +1 -1
  121. package/treb-grid/src/index.ts +1 -1
  122. package/treb-grid/src/layout/base_layout.ts +1 -1
  123. package/treb-grid/src/layout/grid_layout.ts +1 -1
  124. package/treb-grid/src/layout/rectangle_cache.ts +21 -21
  125. package/treb-grid/src/render/selection-renderer.ts +1 -1
  126. package/treb-grid/src/render/svg_header_overlay.ts +1 -1
  127. package/treb-grid/src/render/svg_selection_block.ts +1 -1
  128. package/treb-grid/src/render/tile_renderer.ts +1 -1
  129. package/treb-grid/src/types/annotation.ts +1 -1
  130. package/treb-grid/src/types/border_constants.ts +1 -1
  131. package/treb-grid/src/types/clipboard_data.ts +1 -1
  132. package/treb-grid/src/types/data_model.ts +1 -1
  133. package/treb-grid/src/types/drag_mask.ts +21 -21
  134. package/treb-grid/src/types/grid.ts +1 -1
  135. package/treb-grid/src/types/grid_base.ts +1 -1
  136. package/treb-grid/src/types/grid_command.ts +1 -1
  137. package/treb-grid/src/types/grid_events.ts +1 -1
  138. package/treb-grid/src/types/grid_options.ts +1 -1
  139. package/treb-grid/src/types/grid_selection.ts +1 -1
  140. package/treb-grid/src/types/named_range.ts +1 -1
  141. package/treb-grid/src/types/scale-control.ts +1 -1
  142. package/treb-grid/src/types/serialize_options.ts +1 -1
  143. package/treb-grid/src/types/set_range_options.ts +1 -1
  144. package/treb-grid/src/types/sheet.ts +1 -1
  145. package/treb-grid/src/types/sheet_types.ts +1 -1
  146. package/treb-grid/src/types/tab_bar.ts +1 -1
  147. package/treb-grid/src/types/tile.ts +21 -21
  148. package/treb-grid/src/types/update_flags.ts +1 -1
  149. package/treb-grid/src/util/fontmetrics2.ts +1 -1
  150. package/treb-grid/src/util/ua.ts +21 -21
  151. package/treb-parser/src/csv-parser.ts +21 -21
  152. package/treb-parser/src/index.ts +5 -5
  153. package/treb-parser/src/md-parser.ts +1 -1
  154. package/treb-parser/src/parser-types.ts +1 -1
  155. package/treb-parser/src/parser.test.ts +21 -21
  156. package/treb-parser/src/parser.ts +1 -1
  157. package/treb-utils/src/event_source.ts +1 -1
  158. package/treb-utils/src/ievent_source.ts +13 -13
  159. package/treb-utils/src/index.ts +1 -1
  160. package/treb-utils/src/measurement.ts +1 -1
  161. package/treb-utils/src/scale.ts +21 -21
  162. package/treb-utils/src/serialize_html.ts +1 -1
@@ -1,1291 +1,92 @@
1
- /*
2
- * This file is part of TREB.
3
- *
4
- * TREB is free software: you can redistribute it and/or modify it under the
5
- * terms of the GNU General Public License as published by the Free Software
6
- * Foundation, either version 3 of the License, or (at your option) any
7
- * later version.
8
- *
9
- * TREB is distributed in the hope that it will be useful, but WITHOUT ANY
10
- * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
- * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
- * details.
13
- *
14
- * You should have received a copy of the GNU General Public License along
15
- * with TREB. If not, see <https://www.gnu.org/licenses/>.
16
- *
17
- * Copyright 2022-2023 trebco, llc.
18
- * info@treb.app
19
- *
20
- */
21
-
22
- import { NumberFormatCache } from 'treb-format';
23
- import type { Metrics } from './renderer';
24
- import { ChartRenderer } from './renderer';
25
- import { Area } from './rectangle';
26
- import { Util } from './util';
27
- import type { BarData, CellData, ChartData, DonutSlice, SeriesType, SubSeries } from './chart-types';
28
- import { LegendLayout, LegendPosition, LegendStyle } from './chart-types';
29
- import type { ArrayUnion, ExtendedUnion, UnionValue} from 'treb-base-types';
30
- import { ValueType } from 'treb-base-types';
31
-
32
- // require('../style/charts.scss');
33
-
34
- const DEFAULT_FORMAT = '#,##0.00'; // why not use "general", or whatever the usual default is?
35
-
36
- export class Chart {
37
-
38
- /** flag indicating we've registered at least once */
39
- public static functions_registered = false;
40
-
41
- // always exists; default null type, no title
42
-
43
- protected chart_data: ChartData = {type: 'null'};
44
-
45
- // not chart-specific, so leave outside (FIXME: layout options?)
46
-
47
- // FIXME: change depending on whether there are y-axis labels
48
- // FIXME: different for donut charts...
49
-
50
- private margin = { top: 0.025, left: 0.05, bottom: 0.025, right: 0.075 };
51
-
52
- constructor(
53
- public renderer: ChartRenderer = new ChartRenderer()) {
54
- }
55
-
56
- public Initialize(node: HTMLElement) {
57
- this.renderer.Initialize(node);
58
- }
59
-
60
- public Exec(func: string, union: ExtendedUnion) {
61
-
62
- const args: any[] = union?.value || [];
63
-
64
- switch (func.toLowerCase()) {
65
-
66
- case 'column.chart':
67
- this.CreateColumnChart(args as [UnionValue?, UnionValue?, string?, string?], 'column');
68
- break;
69
-
70
- case 'bar.chart':
71
- this.CreateColumnChart(args as [UnionValue?, UnionValue?, string?, string?], 'bar');
72
- break;
73
-
74
- case 'line.chart':
75
- this.CreateLineChart(args, 'line');
76
- break;
77
-
78
- case 'area.chart':
79
- this.CreateLineChart(args, 'area');
80
- break;
81
-
82
- case 'donut.chart':
83
- case 'pie.chart':
84
- this.CreateDonut(args as [UnionValue?, UnionValue?, string?, string?, string?], func.toLowerCase() === 'pie.chart');
85
- break;
86
-
87
- case 'scatter.plot':
88
- this.CreateScatterChart(args, 'plot');
89
- break;
90
-
91
- case 'scatter.line':
92
- this.CreateScatterChart(args, 'line');
93
- break;
94
-
95
- default:
96
- this.Clear();
97
- break;
98
- }
99
-
100
- }
101
-
102
- public Clear() {
103
- this.chart_data = { type: 'null' };
104
- }
105
-
106
- /**
107
- * column/bar chart, now using common Series data and routines
108
- *
109
- * @param args arguments: data, categories, title, options
110
- * @param type
111
- */
112
- public CreateColumnChart(args: [UnionValue?, UnionValue?, string?, string?], type: 'bar'|'column'): void {
113
-
114
- const series: SeriesType[] = this.TransformSeriesData(args[0]);
115
-
116
- const common = this.CommonData(series);
117
-
118
- let category_labels: string[] | undefined;
119
-
120
- if (args[1]) {
121
-
122
- const values = args[1].type === ValueType.array ? Util.Flatten(args[1].value) : Util.Flatten(args[1]);
123
- category_labels = values.map((cell) => {
124
- if (!cell) { return ''; }
125
-
126
- if (cell.type === ValueType.object && cell.value.type === 'metadata') {
127
- if (typeof cell.value.value === 'number') {
128
- const format = NumberFormatCache.Get(cell.value.format || DEFAULT_FORMAT);
129
- return format.Format(cell.value.value);
130
- }
131
- return cell.value.value;
132
- }
133
-
134
- if (typeof cell.value === 'number') {
135
- const format = NumberFormatCache.Get(cell.format || DEFAULT_FORMAT);
136
- return format.Format(cell.value);
137
- }
138
- return cell.value;
139
- });
140
-
141
- const count = series.reduce((a, entry) => Math.max(a, entry.y.data.length), 0);
142
-
143
- if(count < category_labels.length) {
144
- category_labels = category_labels.slice(0, count);
145
- }
146
-
147
- while (count > category_labels.length) { category_labels.push(''); }
148
-
149
- }
150
-
151
- const title = args[2]?.toString() || undefined;
152
- const options = args[3]?.toString() || undefined;
153
-
154
- this.chart_data = {
155
- type,
156
- legend: common.legend,
157
- // legend_position: LegendPosition.right,
158
- legend_style: LegendStyle.marker,
159
- series2: series,
160
- scale: common.y.scale,
161
- title,
162
- y_labels: type === 'bar' ? category_labels : common.y.labels, // swapped
163
- x_labels: type === 'bar' ? common.y.labels : category_labels, // swapped
164
- };
165
-
166
- if (options) {
167
- (this.chart_data as BarData).round = /round/i.test(options);
168
- this.chart_data.data_labels = /labels/i.test(options);
169
-
170
- let match = options.match(/labels="(.*?)"/);
171
- if (match && series) {
172
- this.ApplyLabels(series, match[1], category_labels);
173
- }
174
- else {
175
- match = options.match(/labels=([^\s\r\n,]+)(?:\W|$)/);
176
- if (match && series) {
177
- this.ApplyLabels(series, match[1], category_labels);
178
- }
179
-
180
- }
181
-
182
- match = options.match(/class=([\w_-]+)(?:\W|$)/);
183
- if (match) {
184
- this.chart_data.class_name = match[1];
185
- }
186
-
187
- }
188
-
189
- }
190
-
191
- public ReadSeries(data: Array<any>): SeriesType {
192
-
193
- // in this case it's (label, X, Y)
194
- const series: SeriesType = {
195
- x: { data: [] },
196
- y: { data: [] },
197
- };
198
-
199
- if (data[3] && typeof data[3] === 'number') {
200
- series.index = data[3];
201
- }
202
- if (data[4]) {
203
- series.subtype = data[4].toString();
204
- }
205
-
206
- if (data[0]) {
207
-
208
- const flat = Util.Flatten(data[0]);
209
-
210
- // this could be a string, if it's a literal, or metadata
211
- // [why would we want metadata?]
212
- //
213
- // OK, check that, should be a string (or other literal)
214
-
215
- if (typeof flat[0] === 'object') {
216
- series.label = flat[0]?.value?.toString() || '';
217
- }
218
- else {
219
- series.label = flat[0].toString();
220
- }
221
- }
222
-
223
- // read [2] first, so we can default for [1] if necessary
224
-
225
- if (!!data[2] && (typeof data[2] === 'object') && data[2].type === ValueType.array) {
226
- const flat = Util.Flatten(data[2].value);
227
- series.y.data = flat.map(item => typeof item.value.value === 'number' ? item.value.value : undefined);
228
- if (flat[0].value?.format) {
229
- series.y.format = flat[0].value?.format as string;
230
- const format = NumberFormatCache.Get(series.y.format);
231
- series.y.labels = series.y.data.map(value => (value === undefined) ? undefined : format.Format(value));
232
- }
233
- }
234
-
235
- if (!!data[1] && (typeof data[1] === 'object') && data[1].type === ValueType.array) {
236
- const flat = Util.Flatten(data[1].value);
237
- series.x.data = flat.map(item => typeof item.value.value === 'number' ? item.value.value : undefined);
238
- if (flat[0].value.format) {
239
- series.x.format = flat[0].value.format;
240
- }
241
- }
242
-
243
- for (const subseries of [series.x, series.y]) {
244
-
245
- // in case of no values
246
- if (subseries.data.length) {
247
- const values = subseries.data.filter(value => value || value === 0) as number[];
248
- subseries.range = {
249
- min: Math.min.apply(0, values),
250
- max: Math.max.apply(0, values),
251
- };
252
- }
253
- }
254
-
255
- return series;
256
-
257
- }
258
-
259
- public ArrayToSeries(array_data: ArrayUnion): SeriesType {
260
-
261
- // this is an array of Y, X not provided
262
-
263
- const series: SeriesType = { x: { data: [] }, y: { data: [] }, };
264
- const flat = Util.Flatten(array_data.value);
265
-
266
- // series.y.data = flat.map(item => typeof item.value === 'number' ? item.value : undefined);
267
-
268
- series.y.data = flat.map((item, index) => {
269
-
270
- // if the data is passed in from the output of a function, it will not
271
- // be inside a metadata structure
272
-
273
- if (typeof item.value === 'number') { return item.value; }
274
-
275
- // ... ok, it's metadata (why not just test?) ...
276
-
277
- // experimenting with complex... put real in X axis and imaginary in Y axis
278
- // note should also function w/ complex not in a metadata structure
279
-
280
- if (typeof item.value.value?.real === 'number') {
281
- series.x.data[index] = item.value.value.real;
282
- return item.value.value.imaginary;
283
- }
284
-
285
- return typeof item.value.value === 'number' ? item.value.value : undefined;
286
-
287
- });
288
-
289
- if (flat[0].value.format) {
290
- series.y.format = flat[0].value.format || '';
291
- const format = NumberFormatCache.Get(series.y.format || '');
292
- series.y.labels = series.y.data.map(value => (value === undefined) ? undefined : format.Format(value));
293
- }
294
-
295
- const values = series.y.data.filter(value => value || value === 0) as number[];
296
- series.y.range = {
297
- min: Math.min.apply(0, values),
298
- max: Math.max.apply(0, values),
299
- };
300
-
301
- // experimenting with complex... this should only be set if we populated
302
- // it from complex values
303
-
304
- if (series.x.data.length) {
305
-
306
- const filtered: number[] = series.x.data.filter(test => typeof test === 'number') as number[];
307
- series.x.range = {
308
- min: Math.min.apply(0, filtered),
309
- max: Math.max.apply(0, filtered),
310
- }
311
-
312
- if (flat[0].value.format) {
313
- series.x.format = flat[0].value.format || '';
314
- const format = NumberFormatCache.Get(series.x.format || '');
315
- series.x.labels = series.x.data.map(value => (value === undefined) ? undefined : format.Format(value));
316
- }
317
-
318
- }
319
-
320
- return series;
321
-
322
- }
323
-
324
- /**
325
- * composite data -> series. composite data can be
326
- *
327
- * (1) set of Y values, with X not provided;
328
- * (2) SERIES(label, X, Y) with Y required, others optional
329
- * (3) GROUP(a, b, ...), where entries are either arrays as (1) or SERIES as (2)
330
- *
331
- * FIXME: consider supporting GROUP(SERIES, [y], ...)
332
- *
333
- * NOTE: (1) could be an array of boxed (union) values...
334
- *
335
- */
336
- public TransformSeriesData(raw_data?: UnionValue, default_x?: UnionValue): SeriesType[] {
337
-
338
- if (!raw_data) { return []; }
339
-
340
- const list: SeriesType[] = [];
341
-
342
- if (raw_data.type === ValueType.object) {
343
- if (raw_data.key === 'group') {
344
- if (Array.isArray(raw_data.value)) {
345
- for (const entry of raw_data.value) {
346
- if (!!entry && (typeof entry === 'object')) {
347
- if (entry.key === 'series') {
348
- const series = this.ReadSeries(entry.value);
349
- list.push(series);
350
- }
351
- else if (entry.type === ValueType.array) {
352
- list.push(this.ArrayToSeries(entry));
353
- }
354
- }
355
- }
356
- }
357
- }
358
- else if (raw_data.key === 'series') {
359
- const series = this.ReadSeries(raw_data.value);
360
- list.push(series);
361
- }
362
- }
363
- else if (raw_data.type === ValueType.array) {
364
- list.push(this.ArrayToSeries(raw_data));
365
- }
366
-
367
- // now we may or may not have X for each series, so we need
368
- // to patch. it's also possible (as with older chart functions)
369
- // that there's a common X -- not sure if we want to continue
370
- // to support that or not...
371
-
372
- let baseline_x: SubSeries|undefined;
373
- let max_y_length = 0;
374
-
375
- // if we have a default, use that (and range it)
376
-
377
- if (default_x?.type === ValueType.array) {
378
-
379
- const values = Util.Flatten(default_x.value);
380
-
381
- let format = '0.00###';
382
-
383
- if (values[0] && values[0].type === ValueType.object) { // UnionIs.Extended(values[0])) {
384
- format = values[0].value.format;
385
- }
386
-
387
- const data = values.map(x => {
388
- if (x.type === ValueType.number) { return x.value; }
389
- if (x.type === ValueType.object) { // ??
390
- // if (UnionIs.Extended(x)) { // ?
391
- return x.value.value;
392
- }
393
- return undefined;
394
- }) as Array<number|undefined>;
395
-
396
- const filtered = data.filter(x => typeof x === 'number') as number[];
397
-
398
- baseline_x = {
399
- data,
400
- format,
401
- range: {
402
- min: Math.min.apply(0, filtered),
403
- max: Math.max.apply(0, filtered),
404
- }
405
- }
406
- }
407
-
408
- // look for the first set that has values. at the same time, get max len
409
-
410
- for (const entry of list) {
411
- max_y_length = Math.max(max_y_length, entry.y.data.length);
412
- if (entry.x.data.length) {
413
- if (!baseline_x) {
414
- baseline_x = entry.x;
415
- }
416
- }
417
- }
418
-
419
- // now default for any series missing X
420
-
421
- if (!baseline_x) {
422
- baseline_x = {
423
- data: [],
424
- range: {
425
- min: 0,
426
- max: Math.max(0, max_y_length - 1),
427
- }
428
- }
429
- for (let i = 0; i < max_y_length; i++) { baseline_x.data.push(i); }
430
- }
431
-
432
- for (const entry of list) {
433
- if (!entry.x.data.length) {
434
- entry.x = baseline_x;
435
- }
436
- }
437
-
438
- return list;
439
- }
440
-
441
- /** get a unified scale, and formats */
442
- public CommonData(series: SeriesType[], y_floor?: number, y_ceiling?: number) {
443
-
444
- let x_format = '';
445
- let y_format = '';
446
-
447
- for (const entry of series) {
448
- if (entry.y.format && !y_format) { y_format = entry.y.format; }
449
- if (entry.x.format && !x_format) { x_format = entry.x.format; }
450
- }
451
-
452
- let legend: Array<{label: string, index?: number}>|undefined; // string[]|undefined;
453
- if (series.some(test => test.label && (test.label.length > 0))) {
454
- legend = series.map((entry, i) => ({
455
- label: entry.label || `Series ${i + 1}`,
456
- index: typeof entry.index === 'number' ? entry.index : i + 1,
457
- }));
458
- }
459
-
460
- const x = series.filter(test => test.x.range);
461
- const x_min = Math.min.apply(0, x.map(test => test.x.range?.min || 0));
462
- const x_max = Math.max.apply(0, x.map(test => test.x.range?.max || 0));
463
-
464
- const y = series.filter(test => test.y.range);
465
- let y_min = Math.min.apply(0, x.map(test => test.y.range?.min || 0));
466
- let y_max = Math.max.apply(0, x.map(test => test.y.range?.max || 0));
467
-
468
- if (typeof y_floor !== 'undefined') {
469
- y_min = Math.min(y_min, y_floor);
470
- }
471
- if (typeof y_ceiling !== 'undefined') {
472
- y_max = Math.max(y_max, y_ceiling);
473
- }
474
-
475
- const x_scale = Util.Scale(x_min, x_max, 7);
476
- const y_scale = Util.Scale(y_min, y_max, 7);
477
-
478
- let x_labels: string[]|undefined;
479
- let y_labels: string[]|undefined;
480
-
481
- if (x_format) {
482
- x_labels = [];
483
- const format = NumberFormatCache.Get(x_format);
484
- for (let i = 0; i <= x_scale.count; i++) {
485
- x_labels.push(format.Format(x_scale.min + i * x_scale.step));
486
- }
487
- }
488
-
489
- if (y_format) {
490
- y_labels = [];
491
- const format = NumberFormatCache.Get(y_format);
492
- for (let i = 0; i <= y_scale.count; i++) {
493
- y_labels.push(format.Format(y_scale.min + i * y_scale.step));
494
- }
495
- }
496
-
497
- return {
498
- x: {
499
- format: x_format,
500
- scale: x_scale,
501
- labels: x_labels,
502
- },
503
- y: {
504
- format: y_format,
505
- scale: y_scale,
506
- labels: y_labels,
507
- },
508
- legend,
509
- };
510
-
511
- }
512
-
513
- /**
514
- * args is [data, title, options]
515
- *
516
- * args[0] is the scatter data. this can be
517
- *
518
- * (1) set of Y values, with X not provided;
519
- * (2) SERIES(label, X, Y) with Y required, others optional
520
- * (3) GROUP(SERIES(label, X, Y), SERIES(label, X, Y), ...), with same rule for each series
521
- *
522
- * @param args
523
- */
524
- public CreateScatterChart(args: any[], style: 'plot'|'line' = 'plot'): void {
525
-
526
- // FIXME: transform the data, then have this function
527
- // operate on clean data. that way the transform can
528
- // be reused (and the function can be reused without the
529
- // transform).
530
-
531
- const series: SeriesType[] = this.TransformSeriesData(args[0]);
532
-
533
- const common = this.CommonData(series);
534
-
535
- const title = args[1]?.toString() || undefined;
536
- const options = args[2]?.toString() || undefined;
537
-
538
- this.chart_data = {
539
- legend: common.legend,
540
- style,
541
- type: 'scatter2',
542
- series, // : [{x, y}],
543
- title,
544
-
545
- x_scale: common.x.scale,
546
- x_labels: common.x.labels,
547
-
548
- y_scale: common.y.scale,
549
- y_labels: common.y.labels,
550
-
551
- lines: style === 'line', // true,
552
- points: style === 'plot',
553
-
554
- };
555
-
556
- if (options) {
557
-
558
- this.chart_data.markers = /marker/i.test(options);
559
- this.chart_data.smooth = /smooth/i.test(options);
560
- this.chart_data.data_labels = /labels/i.test(options);
561
-
562
- let match = options.match(/labels="(.*?)"/);
563
- if (match && this.chart_data.series) {
564
- this.ApplyLabels(this.chart_data.series, match[1]);
565
- }
566
- else {
567
- match = options.match(/labels=([^\s\r\n,]+)(?:\W|$)/);
568
- if (match && this.chart_data.series) {
569
- this.ApplyLabels(this.chart_data.series, match[1]);
570
- }
571
- }
572
-
573
- match = options.match(/class=([\w_-]+)(?:\W|$)/);
574
- if (match) {
575
- this.chart_data.class_name = match[1];
576
- }
577
-
578
- }
579
-
580
- }
581
-
582
- public ApplyLabels(series_list: SeriesType[], pattern: string, category_labels?: string[]): void {
583
-
584
- for (const series of series_list) {
585
-
586
- const format = {
587
- x: NumberFormatCache.Get(series.x.format || ''),
588
- y: NumberFormatCache.Get(series.y.format || ''),
589
- };
590
-
591
- series.y.labels = [];
592
-
593
- for (let i = 0; i < series.y.data.length; i++) {
594
-
595
- const x = category_labels ? category_labels[i] :
596
- (typeof series.x.data[i] === 'number' ? format.x.Format(series.x.data[i]) : '');
597
- const y = typeof series.y.data[i] === 'number' ? format.y.Format(series.y.data[i]) : '';
598
-
599
- series.y.labels[i] = pattern.replace(/\bx\b/g, x).replace(/\by\b/g, y);
600
-
601
- }
602
-
603
- }
604
-
605
- }
606
-
607
- /**
608
- * args: data, labels, title, callouts, "smooth"
609
- */
610
- public CreateLineChart(args: any[], type: 'line'|'area'): void { // |'bar'|'column') {
611
-
612
- const series: SeriesType[] = this.TransformSeriesData(args[0], args[1]);
613
-
614
- const common = this.CommonData(series, 0, 0);
615
-
616
- const title = args[2]?.toString() || undefined;
617
- const options = args[3]?.toString() || undefined;
618
-
619
- this.chart_data = {
620
- legend: common.legend,
621
- // style: type, // 'line',
622
- type: 'scatter2',
623
- series, // : [{x, y}],
624
- title,
625
-
626
- x_scale: common.x.scale,
627
- x_labels: common.x.labels,
628
-
629
- y_scale: common.y.scale,
630
- y_labels: common.y.labels,
631
-
632
- lines: true,
633
- filled: type === 'area',
634
-
635
- };
636
-
637
- if (options) {
638
- // this.chart_data.markers = /marker/i.test(options);
639
- this.chart_data.smooth = /smooth/i.test(options);
640
- // this.chart_data.data_labels = /labels/i.test(options);
641
-
642
- const match = options.match(/class=([\w_-]+)(?:\W|$)/);
643
- if (match) {
644
- this.chart_data.class_name = match[1];
645
- }
646
-
647
- }
648
-
649
- }
650
-
651
- /**
652
- * arguments are values, labels, title, sort, label option, ...
653
- */
654
- public CreateDonut(args: [UnionValue?, UnionValue?, string?, string?, string?], pie_chart = false): void {
655
-
656
- const raw_data = args[0]?.type === ValueType.array ? args[0].value : args[0];
657
-
658
- // we're now expecting this to be metadata (including value).
659
- // so we need to unpack. could be an array... could be deep...
660
- const flat = Util.Flatten(raw_data);
661
-
662
- // we still need the aggregate for range, scale
663
- let data = flat.map((x) => (typeof x.value.value === 'number') ? x.value.value : undefined) as number[];
664
-
665
-
666
- // if labels are strings, just pass them in. if they're numbers then
667
- // use the format (we're collecting metadata for this field now)
668
-
669
- const raw_labels = args[1]?.type === ValueType.array ? args[1].value : args[1];
670
-
671
- const labels = Util.Flatten(raw_labels).map((label) => {
672
- if (label && typeof label === 'object') {
673
- const value = label.value?.value;
674
- if (typeof value === 'number' && label.value?.format) {
675
- return NumberFormatCache.Get(label.value?.format).Format(value);
676
- }
677
- else return value ? value.toString() : '';
678
- }
679
- else return label ? label.toString() : '';
680
- });
681
-
682
- // no negative numbers
683
-
684
- data = data.map((check) => {
685
- if (check < 0) {
686
- console.warn('pie/donut chart does not support negative values (omitted)');
687
- return 0;
688
- }
689
- return check;
690
- });
691
-
692
- const title = args[2] || '';
693
-
694
- let sum = 0;
695
-
696
- const slices: DonutSlice[] = data.map((value, i) => {
697
- if (typeof value !== 'undefined') sum += value;
698
- return { value, label: labels[i] || '', index: i + 1, percent: 0 };
699
- });
700
-
701
- if (sum) {
702
- for (const slice of slices) {
703
- slice.percent = (slice.value || 0) / sum;
704
- }
705
- }
706
-
707
- // titles? label/value/percent
708
- // FIXME: number format(s)
709
-
710
- const format_pattern = (flat.length && flat[0].value?.format) ? flat[0].value.format : '';
711
- const format = NumberFormatCache.Get(format_pattern || DEFAULT_FORMAT);
712
- const percent_format = NumberFormatCache.Get('percent');
713
-
714
- // ensure label if we have labels array but no label format string
715
-
716
- if (typeof args[4] === 'undefined' && args[1]) {
717
- args[4] = 'label';
718
- }
719
-
720
- const slice_title = (args[4] || '');
721
- if (slice_title) {
722
- for (const slice of slices) {
723
- const value = /*NumberFormatCache.Get('general')*/ format.Format(slice.value || 0);
724
- const percent = percent_format.Format(slice.percent);
725
- slice.title = slice_title
726
- .replace(/value%/ig, percent_format.Format(slice.value || 0))
727
- .replace(/value/ig, value)
728
- .replace(/percent/ig, percent)
729
- .replace(/label/ig, slice.label || '')
730
- .trim();
731
- }
732
- }
733
-
734
- // optionally sort...
735
-
736
- const options = (args[3] || '').toString().trim();
737
-
738
- // old-style...
739
-
740
- let sort = options.toUpperCase();
741
- if (sort === 'ASC' || sort === 'ASCENDING' || sort === 'INC') {
742
- slices.sort((a, b) => { return (a.value || 0) - (b.value || 0); });
743
- }
744
- else if (sort === 'DESC' || sort === 'DESCENDING' || sort === 'DEC') {
745
- slices.sort((a, b) => { return (b.value || 0) - (a.value || 0); });
746
- }
747
- else {
748
- const match = options.match(/sort=([\w]+)(?:\W|$)/i);
749
- if (match) {
750
- sort = match[1];
751
- if (/^(asc|inc)/i.test(sort)) {
752
- slices.sort((a, b) => { return (a.value || 0) - (b.value || 0); });
753
- }
754
- else if (/^(desc|dec)/i.test(sort)) {
755
- slices.sort((a, b) => { return (b.value || 0) - (a.value || 0); });
756
- }
757
- }
758
- }
759
-
760
- this.chart_data = {
761
- type: pie_chart ? 'pie' : 'donut',
762
- slices,
763
- title,
764
- };
765
-
766
- if (options) {
767
- const match = options.match(/class=([_-\w]+)(?:\W|$)/);
768
- if (match) {
769
- this.chart_data.class_name = match[1];
770
- }
771
- }
772
-
773
- }
774
-
775
-
776
- /** pass-through */
777
- public Resize() {
778
- this.renderer.Resize();
779
- }
780
-
781
- /**
782
- * redraw
783
- */
784
- public Update() {
785
-
786
- // reset
787
- this.renderer.Resize(); // just too many problems
788
- this.renderer.Prerender();
789
- this.renderer.Clear(this.chart_data.class_name);
790
-
791
- // get usable area [FIXME: method]
792
- const area = new Area(0, 0, this.renderer.size.width, this.renderer.size.height);
793
-
794
- // chart margin
795
- const chart_margin = {
796
- top: Math.round(area.height) * this.margin.top,
797
- bottom: Math.round(area.height) * this.margin.bottom,
798
- left: Math.round(area.width) * this.margin.left,
799
- right: Math.round(area.width) * this.margin.right,
800
- };
801
-
802
- // title, top or bottom
803
- const title = this.chart_data.title;
804
-
805
- if (title) {
806
- this.renderer.RenderTitle(title, area, chart_margin.top,
807
- this.chart_data.title_layout||'top');
808
- }
809
-
810
- // pad
811
- area.top += chart_margin.top;
812
- area.left += chart_margin.left;
813
- area.bottom -= chart_margin.bottom;
814
- area.right -= chart_margin.right;
815
-
816
- if (this.chart_data.legend && this.chart_data.legend.length) {
817
-
818
- let default_position = LegendPosition.top;
819
- if (this.chart_data.title) {
820
- if (!this.chart_data.title_layout || this.chart_data.title_layout === 'top') {
821
- default_position = LegendPosition.bottom;
822
- }
823
- }
824
-
825
- const position = this.chart_data.legend_position || default_position;
826
-
827
- this.renderer.Legend({
828
- labels: this.chart_data.legend,
829
- position,
830
- style: this.chart_data.legend_style,
831
- layout: (position === LegendPosition.top || position === LegendPosition.bottom) ?
832
- LegendLayout.horizontal : LegendLayout.vertical,
833
- area,
834
- });
835
-
836
- }
837
-
838
- if (this.chart_data.type === 'histogram'
839
- || this.chart_data.type === 'line'
840
- || this.chart_data.type === 'area'
841
- || this.chart_data.type === 'column'
842
- || this.chart_data.type === 'histogram2'
843
- || this.chart_data.type === 'bar'
844
- || this.chart_data.type === 'scatter2'
845
- ) {
846
-
847
- // we need to measure first, then lay out the other axis, then we
848
- // can come back and render. it doesn't really matter which one you
849
- // do first.
850
-
851
- // measure x axis (height)
852
-
853
- let x_metrics: Metrics[] = [];
854
- let max_x_height = 0;
855
-
856
- if (this.chart_data.x_labels && this.chart_data.x_labels.length) {
857
- x_metrics = this.chart_data.x_labels.map((text) => {
858
- const metrics = this.renderer.MeasureText(text, ['axis-label', 'x-axis-label'], true);
859
- max_x_height = Math.max(max_x_height, metrics.height);
860
- return metrics;
861
- });
862
- }
863
-
864
- // measure & render y axis
865
-
866
- if (this.chart_data.y_labels && this.chart_data.y_labels.length) {
867
-
868
- const y_labels: Array<{label: string; metrics: Metrics}> = [];
869
- let max_width = 0;
870
- let max_height = 0;
871
-
872
- const scale = (this.chart_data.type === 'scatter2') ? this.chart_data.y_scale : this.chart_data.scale;
873
-
874
- const count = (this.chart_data.type === 'bar') ?
875
- this.chart_data.y_labels.length :
876
- /* this.chart_data. */
877
- scale.count + 1;
878
-
879
- for (let i = 0; i < count; i++ ){
880
- const metrics = this.renderer.MeasureText(this.chart_data.y_labels[i], ['axis-label', 'y-axis-label']);
881
- y_labels.push({ label: this.chart_data.y_labels[i], metrics });
882
- max_width = Math.max(max_width, metrics.width);
883
- max_height = Math.max(max_height, metrics.height);
884
- }
885
-
886
- area.bottom = Math.round(area.bottom - max_height / 2);
887
- area.top = Math.round(area.top + max_height / 2);
888
-
889
- if (x_metrics.length) {
890
- area.bottom -= (max_x_height + chart_margin.bottom);
891
- }
892
-
893
- if (this.chart_data.type === 'bar') {
894
- this.renderer.RenderYAxisBar(area, area.left + max_width, y_labels, ['axis-label', 'y-axis-label']);
895
- }
896
- else {
897
- this.renderer.RenderYAxis(area, area.left + max_width, y_labels, ['axis-label', 'y-axis-label']);
898
- }
899
- area.left += (max_width + chart_margin.left);
900
-
901
- }
902
-
903
- // now render x axis
904
-
905
- if (x_metrics.length && this.chart_data.x_labels && this.chart_data.x_labels.length) {
906
-
907
- const tick = (this.chart_data.type === 'histogram2');
908
- const offset_tick = (
909
- this.chart_data.type !== 'line' &&
910
- this.chart_data.type !== 'area' &&
911
- this.chart_data.type !== 'bar' &&
912
- this.chart_data.type !== 'scatter2' &&
913
- this.chart_data.type !== 'histogram2'
914
- );
915
-
916
- // do this before you fix the offset
917
-
918
- if (tick) {
919
- this.renderer.RenderXAxisTicks(area, offset_tick, this.chart_data.x_labels.length);
920
- }
921
-
922
-
923
- if (this.chart_data.y_labels) {
924
- // undo, temp
925
- area.bottom += (max_x_height + chart_margin.bottom);
926
- }
927
-
928
- // render
929
- this.renderer.RenderXAxis(area,
930
- offset_tick,
931
- this.chart_data.x_labels,
932
- x_metrics,
933
- ['axis-label', 'x-axis-label']);
934
-
935
- // update bottom (either we unwound for labels, or we need to do it the first time)
936
- area.bottom -= (max_x_height + chart_margin.bottom);
937
-
938
- }
939
-
940
- }
941
-
942
- // now do type-specific rendering
943
-
944
- switch (this.chart_data.type) {
945
- case 'scatter':
946
- this.renderer.RenderPoints(area, this.chart_data.x, this.chart_data.y, 'mc mc-correlation series-1');
947
- break;
948
-
949
- case 'scatter2':
950
-
951
- this.renderer.RenderGrid(area,
952
- this.chart_data.y_scale.count,
953
- this.chart_data.x_scale.count + 1, // (sigh)
954
- 'chart-grid');
955
-
956
- if (this.chart_data.series) {
957
- for (let i = 0; i < this.chart_data.series.length; i++) {
958
- const series = this.chart_data.series[i];
959
-
960
- let lines = !!this.chart_data.lines;
961
- let points = !!this.chart_data.points;
962
-
963
- if (series.subtype === 'plot') {
964
- points = true;
965
- lines = false;
966
- }
967
- else if (series.subtype === 'line') {
968
- points = false;
969
- lines = true;
970
- }
971
-
972
- const index = typeof series.index === 'number' ? series.index : i + 1;
973
- this.renderer.RenderScatterSeries(area,
974
- series.x.data,
975
- series.y.data,
976
- this.chart_data.x_scale,
977
- this.chart_data.y_scale,
978
- lines,
979
- points,
980
- !!this.chart_data.filled,
981
- !!this.chart_data.markers,
982
- !!this.chart_data.smooth,
983
- `scatter-plot series-${index}`);
984
- }
985
- if (this.chart_data.data_labels) {
986
- for (let i = 0; i < this.chart_data.series.length; i++) {
987
- const series = this.chart_data.series[i];
988
- if (series.y.labels) {
989
- this.renderer.RenderDataLabels(
990
- area,
991
- series.x.data,
992
- series.y.data,
993
- this.chart_data.x_scale,
994
- this.chart_data.y_scale,
995
- series.y.labels,
996
- i + 1);
997
- }
998
- }
999
- }
1000
- }
1001
- break;
1002
-
1003
- case 'pie':
1004
- case 'donut':
1005
- {
1006
- const outer = (Math.min(area.height, area.width) / 2) * .9;
1007
- const inner = this.chart_data.type === 'pie' ? 0 : outer * .8;
1008
- this.renderer.RenderDonut(this.chart_data.slices, area.center, outer, inner, area,
1009
- true, 'donut');
1010
- }
1011
- break;
1012
-
1013
- case 'line':
1014
- case 'area':
1015
- {
1016
- const scale = this.chart_data.scale;
1017
- if (this.chart_data.series) {
1018
-
1019
- const points = this.chart_data.x_scale ?
1020
- this.chart_data.x_scale.max :
1021
- Math.max.apply(0, this.chart_data.series.map(x => x.length));
1022
-
1023
- const func = this.chart_data.smooth ?
1024
- this.renderer.RenderSmoothLine : this.renderer.RenderLine;
1025
-
1026
- // gridlines
1027
- this.renderer.RenderGrid(area,
1028
- this.chart_data.scale.count,
1029
- this.chart_data.x_scale ? this.chart_data.x_scale.count + 1 : points,
1030
- 'chart-grid');
1031
-
1032
- // series
1033
- let series_index = 0;
1034
- for (const series of this.chart_data.series) {
1035
-
1036
-
1037
-
1038
- const y = series.map((point) => {
1039
- if (typeof point === 'undefined') { return undefined; }
1040
- return Util.ApplyScale(point, area.height, scale);
1041
- });
1042
-
1043
- if (y.length < points) {
1044
- for (let i = y.length; i < points; i++) {
1045
- y.push(undefined);
1046
- }
1047
- }
1048
-
1049
- const styles = [
1050
- this.chart_data.type === 'area' ? 'chart-area' : 'chart-line',
1051
- `series-${series_index + 1}`]
1052
-
1053
- func.call(this.renderer, area, y, (this.chart_data.type === 'area'), this.chart_data.titles, styles);
1054
- series_index++;
1055
- }
1056
- }
1057
-
1058
- // TODO: callouts
1059
-
1060
- }
1061
- break;
1062
-
1063
- case 'bar':
1064
- {
1065
- let corners: number[]|undefined;
1066
-
1067
- // gridlines
1068
- this.renderer.RenderBarGrid(area, this.chart_data.scale.count, 'chart-grid');
1069
- if (this.chart_data.series2) {
1070
-
1071
- let count = 0;
1072
- const series_count = this.chart_data.series2.length;
1073
-
1074
- for (const series of this.chart_data.series2) {
1075
- count = Math.max(count, series.y.data.length);
1076
- }
1077
-
1078
- const row_height = area.height / count;
1079
- let row_pct = .7;
1080
- if (typeof this.chart_data.space === 'number') {
1081
- row_pct = Math.max(0, Math.min(1, 1 - (this.chart_data.space)));
1082
- }
1083
-
1084
- const space = row_height * (1 - row_pct) / 2;
1085
- const height = (row_height - space * 2) / series_count;
1086
-
1087
- let zero = 0;
1088
- if (this.chart_data.scale.min < 0) { // && this.chart_data.scale.max >= 0) {
1089
- zero = Util.ApplyScale(0, area.width, this.chart_data.scale);
1090
- }
1091
-
1092
- if (this.chart_data.round) {
1093
- const half_height = Math.floor(height / 2);
1094
- corners = [0, half_height, half_height, 0];
1095
- }
1096
-
1097
- for (let s = 0; s < series_count; s++) {
1098
- const series = this.chart_data.series2[s];
1099
- const color_index = typeof series.index === 'number' ? series.index : s + 1;
1100
-
1101
- for (let i = 0; i < series.y.data.length; i++ ){
1102
- const value = series.y.data[i];
1103
- if (typeof value === 'number') {
1104
-
1105
- const y = Math.round(area.top + i * row_height + space) + s * height;
1106
-
1107
- let x = 0;
1108
- let width = 0;
1109
- let negative = false;
1110
-
1111
- if (zero) {
1112
- if (value > 0) {
1113
- width = Util.ApplyScale(value + this.chart_data.scale.min, area.width, this.chart_data.scale);
1114
- x = area.left + zero;
1115
- }
1116
- else {
1117
- width = Util.ApplyScale(this.chart_data.scale.min - value, area.width, this.chart_data.scale);
1118
- x = area.left + zero - width;
1119
- negative = true;
1120
- }
1121
- }
1122
- else {
1123
- width = Util.ApplyScale(value, area.width, this.chart_data.scale);
1124
- x = area.left;
1125
- }
1126
-
1127
- // const bar_title = this.chart_data.titles ? this.chart_data.titles[i] : undefined;
1128
- const bar_title = undefined;
1129
-
1130
- if (width) {
1131
- this.renderer.RenderRectangle(new Area(
1132
- x, y, x + width, y + height,
1133
- ), corners, ['chart-column', `series-${color_index}`], bar_title || undefined);
1134
- }
1135
- }
1136
- }
1137
- }
1138
-
1139
- }
1140
-
1141
- }
1142
- break;
1143
-
1144
- case 'column':
1145
- case 'histogram2':
1146
- {
1147
-
1148
- // gridlines
1149
- this.renderer.RenderGrid(area, this.chart_data.scale.count, 0, 'chart-grid');
1150
-
1151
- if (this.chart_data.series2) {
1152
-
1153
- let count = 0;
1154
- const series_count = this.chart_data.series2.length;
1155
-
1156
- for (const series of this.chart_data.series2) {
1157
- count = Math.max(count, series.y.data.length);
1158
- }
1159
-
1160
- // columns
1161
- const column_width = area.width / count;
1162
- let column_pct = .7;
1163
- if (typeof this.chart_data.space === 'number') {
1164
- column_pct = Math.max(0, Math.min(1, 1 - (this.chart_data.space)));
1165
- }
1166
-
1167
- const space = column_width * (1 - column_pct) / 2;
1168
- const width = (column_width - space * 2) / series_count;
1169
-
1170
- let zero = 0;
1171
- if (this.chart_data.scale.min < 0) { // && this.chart_data.scale.max >= 0) {
1172
- zero = Util.ApplyScale(0, area.height, this.chart_data.scale);
1173
- }
1174
-
1175
- if (this.chart_data.callouts && this.chart_data.x_scale) {
1176
- const scale = this.chart_data.x_scale;
1177
- const lines = this.chart_data.callouts.map((callout, index) => {
1178
- const x = Math.round(area.left + Util.ApplyScale(callout.value, area.width, scale)) + .5;
1179
- return {
1180
- x1: x, y1: area.bottom - area.height, x2: x, y2: area.bottom,
1181
- classes: `callout-${index + 1}`,
1182
- }
1183
- });
1184
- this.renderer.RenderCalloutLines(lines);
1185
- }
1186
-
1187
- let corners: number[]|undefined;
1188
-
1189
- if (this.chart_data.round) {
1190
- const half_width = Math.floor(width/2);
1191
- corners = [half_width, half_width, 0, 0];
1192
- }
1193
-
1194
- for (let s = 0; s < series_count; s++) {
1195
- const series = this.chart_data.series2[s];
1196
- const color_index = typeof series.index === 'number' ? series.index : s + 1;
1197
-
1198
- for (let i = 0; i < series.y.data.length; i++ ){
1199
- const value = series.y.data[i];
1200
- // const format = NumberFormatCache.Get(series.y.format || '0.00');
1201
-
1202
- if (typeof value === 'number') {
1203
-
1204
- // const x = Math.round(area.left + i * column_width + space) + s * width;
1205
- const x = (area.left + i * column_width + space) + s * width;
1206
-
1207
- let height = 0;
1208
- let y = 0;
1209
- let negative = false;
1210
-
1211
- if (zero) {
1212
- if (value > 0) {
1213
- height = Util.ApplyScale(value + this.chart_data.scale.min, area.height, this.chart_data.scale);
1214
- y = area.bottom - height - zero;
1215
- }
1216
- else {
1217
- height = Util.ApplyScale(this.chart_data.scale.min - value, area.height, this.chart_data.scale);
1218
- y = area.bottom - zero; // // area.bottom - height - zero;
1219
- negative = true;
1220
- }
1221
- }
1222
- else {
1223
- height = Util.ApplyScale(value, area.height, this.chart_data.scale);
1224
- y = area.bottom - height;
1225
- }
1226
-
1227
- // const bar_title = this.chart_data.titles ? this.chart_data.titles[i] : undefined;
1228
- const bar_title = undefined;
1229
-
1230
- if (height) {
1231
-
1232
- const label = (this.chart_data.data_labels && !!series.y.labels) ? series.y.labels[i] : '';
1233
- const label_point = {
1234
- x: Math.round(x + width / 2),
1235
- y: Math.round(y - 10),
1236
- };
1237
-
1238
- this.renderer.RenderRectangle(new Area(
1239
- x, y, x + width, y + height,
1240
- ), corners, ['chart-column', `series-${color_index}`], bar_title || undefined, label, label_point);
1241
- }
1242
- }
1243
- }
1244
-
1245
- }
1246
-
1247
- }
1248
-
1249
- }
1250
- break;
1251
-
1252
- case 'histogram':
1253
- {
1254
- // gridlines
1255
- this.renderer.RenderGrid(area, this.chart_data.scale.count, 0, 'chart-grid');
1256
-
1257
- // columns
1258
- const column_width = area.width / this.chart_data.count;
1259
- const column_pct = this.chart_data.column_width;
1260
-
1261
- const space = column_width * (1 - column_pct) / 2;
1262
-
1263
- for (let i = 0; i < this.chart_data.count; i++ ){
1264
- const x = Math.round(area.left + i * column_width + space);
1265
- const width = column_width - space * 2;
1266
- const height = Util.ApplyScale(this.chart_data.bins[i], area.height, this.chart_data.scale);
1267
- const y = area.bottom - height;
1268
- const bar_title = this.chart_data.titles ? this.chart_data.titles[i] : undefined;
1269
-
1270
- this.renderer.RenderRectangle(new Area(
1271
- x, y, x + width, y + height,
1272
- ), undefined, 'chart-column series-1', bar_title || undefined);
1273
- }
1274
-
1275
- }
1276
- break;
1277
- }
1278
-
1279
- }
1280
-
1281
- /** type guard */
1282
- protected IsCellData(candidate: any): candidate is CellData {
1283
- return (
1284
- typeof candidate === 'object' &&
1285
- typeof candidate.address === 'object' &&
1286
- typeof candidate.address.row === 'number' &&
1287
- typeof candidate.address.column === 'number');
1288
- }
1289
-
1290
-
1291
- }
1
+
2
+ import type { ChartRenderer } from './renderer-type';
3
+ import type { ChartData } from './chart-types';
4
+ import type { ExtendedUnion, UnionValue } from 'treb-base-types';
5
+ import * as ChartUtils from './chart-utils';
6
+ import { DefaultChartRenderer } from './default-chart-renderer';
7
+
8
+ /**
9
+ * transitioning to new structure, this should mirror the old chart
10
+ * interface (at least the public interface)
11
+ */
12
+ export class Chart {
13
+
14
+ /** flag indicating we've registered at least once */
15
+ public static functions_registered = false;
16
+
17
+ // always exists; default null type, no title
18
+
19
+ protected chart_data: ChartData = {type: 'null'};
20
+
21
+ protected node?: HTMLElement;
22
+
23
+ constructor(
24
+ public renderer: ChartRenderer = new DefaultChartRenderer()) {
25
+ }
26
+
27
+ public Initialize(node: HTMLElement) {
28
+ this.node = node;
29
+ this.renderer.Initialize(node);
30
+ }
31
+
32
+ public Exec(func: string, union: ExtendedUnion) {
33
+
34
+ const args: any[] = union?.value || [];
35
+
36
+ switch (func.toLowerCase()) {
37
+
38
+ case 'column.chart':
39
+ this.chart_data = ChartUtils.CreateColumnChart(args as [UnionValue?, UnionValue?, string?, string?], 'column');
40
+ break;
41
+
42
+ case 'bar.chart':
43
+ this.chart_data = ChartUtils.CreateColumnChart(args as [UnionValue?, UnionValue?, string?, string?], 'bar');
44
+ break;
45
+
46
+ case 'line.chart':
47
+ this.chart_data = ChartUtils.CreateLineChart(args, 'line');
48
+ break;
49
+
50
+ case 'area.chart':
51
+ this.chart_data = ChartUtils.CreateLineChart(args, 'area');
52
+ break;
53
+
54
+ case 'donut.chart':
55
+ case 'pie.chart':
56
+ this.chart_data = ChartUtils.CreateDonut(args as [UnionValue?, UnionValue?, string?, string?, string?], func.toLowerCase() === 'pie.chart');
57
+ break;
58
+
59
+ case 'scatter.plot':
60
+ this.chart_data = ChartUtils.CreateScatterChart(args, 'plot');
61
+ break;
62
+
63
+ case 'scatter.line':
64
+ this.chart_data = ChartUtils.CreateScatterChart(args, 'line');
65
+ break;
66
+
67
+ default:
68
+ this.Clear();
69
+ break;
70
+ }
71
+
72
+ }
73
+
74
+ public Clear() {
75
+ this.chart_data = { type: 'null' };
76
+ }
77
+
78
+ /** pass through */
79
+ public Resize() {
80
+ if (this.node) {
81
+ this.renderer.Resize(this.node, this.chart_data);
82
+ }
83
+ }
84
+
85
+ /** pass through */
86
+ public Update() {
87
+ if (this.node) {
88
+ this.renderer.Update(this.node, this.chart_data);
89
+ }
90
+ }
91
+
92
+ }