@trebco/treb 23.6.5 → 25.0.0-rc1

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 (217) hide show
  1. package/.eslintignore +8 -0
  2. package/.eslintrc.js +164 -0
  3. package/README-shadow-DOM.md +88 -0
  4. package/README.md +37 -130
  5. package/api-config.json +29 -0
  6. package/api-generator/api-generator-types.ts +82 -0
  7. package/api-generator/api-generator.ts +1172 -0
  8. package/api-generator/package.json +3 -0
  9. package/build/treb-spreadsheet.mjs +14 -0
  10. package/{treb.d.ts → build/treb.d.ts} +285 -269
  11. package/esbuild-custom-element.mjs +336 -0
  12. package/esbuild.js +305 -0
  13. package/package.json +43 -14
  14. package/treb-base-types/package.json +5 -0
  15. package/treb-base-types/src/api_types.ts +36 -0
  16. package/treb-base-types/src/area.ts +583 -0
  17. package/treb-base-types/src/basic_types.ts +45 -0
  18. package/treb-base-types/src/cell.ts +612 -0
  19. package/treb-base-types/src/cells.ts +1066 -0
  20. package/treb-base-types/src/color.ts +124 -0
  21. package/treb-base-types/src/import.ts +71 -0
  22. package/treb-base-types/src/index-standalone.ts +29 -0
  23. package/treb-base-types/src/index.ts +42 -0
  24. package/treb-base-types/src/layout.ts +47 -0
  25. package/treb-base-types/src/localization.ts +187 -0
  26. package/treb-base-types/src/rectangle.ts +145 -0
  27. package/treb-base-types/src/render_text.ts +72 -0
  28. package/treb-base-types/src/style.ts +545 -0
  29. package/treb-base-types/src/table.ts +109 -0
  30. package/treb-base-types/src/text_part.ts +54 -0
  31. package/treb-base-types/src/theme.ts +608 -0
  32. package/treb-base-types/src/union.ts +152 -0
  33. package/treb-base-types/src/value-type.ts +164 -0
  34. package/treb-base-types/style/resizable.css +59 -0
  35. package/treb-calculator/modern.tsconfig.json +11 -0
  36. package/treb-calculator/package.json +5 -0
  37. package/treb-calculator/src/calculator.ts +2546 -0
  38. package/treb-calculator/src/complex-math.ts +558 -0
  39. package/treb-calculator/src/dag/array-vertex.ts +198 -0
  40. package/treb-calculator/src/dag/graph.ts +951 -0
  41. package/treb-calculator/src/dag/leaf_vertex.ts +118 -0
  42. package/treb-calculator/src/dag/spreadsheet_vertex.ts +327 -0
  43. package/treb-calculator/src/dag/spreadsheet_vertex_base.ts +44 -0
  44. package/treb-calculator/src/dag/vertex.ts +352 -0
  45. package/treb-calculator/src/descriptors.ts +162 -0
  46. package/treb-calculator/src/expression-calculator.ts +1069 -0
  47. package/treb-calculator/src/function-error.ts +103 -0
  48. package/treb-calculator/src/function-library.ts +103 -0
  49. package/treb-calculator/src/functions/base-functions.ts +1214 -0
  50. package/treb-calculator/src/functions/checkbox.ts +164 -0
  51. package/treb-calculator/src/functions/complex-functions.ts +253 -0
  52. package/treb-calculator/src/functions/finance-functions.ts +399 -0
  53. package/treb-calculator/src/functions/information-functions.ts +102 -0
  54. package/treb-calculator/src/functions/matrix-functions.ts +182 -0
  55. package/treb-calculator/src/functions/sparkline.ts +335 -0
  56. package/treb-calculator/src/functions/statistics-functions.ts +350 -0
  57. package/treb-calculator/src/functions/text-functions.ts +298 -0
  58. package/treb-calculator/src/index.ts +27 -0
  59. package/treb-calculator/src/notifier-types.ts +59 -0
  60. package/treb-calculator/src/primitives.ts +428 -0
  61. package/treb-calculator/src/utilities.ts +305 -0
  62. package/treb-charts/package.json +5 -0
  63. package/treb-charts/src/chart-functions.ts +156 -0
  64. package/treb-charts/src/chart-types.ts +230 -0
  65. package/treb-charts/src/chart.ts +1288 -0
  66. package/treb-charts/src/index.ts +24 -0
  67. package/treb-charts/src/main.ts +37 -0
  68. package/treb-charts/src/rectangle.ts +52 -0
  69. package/treb-charts/src/renderer.ts +1841 -0
  70. package/treb-charts/src/util.ts +122 -0
  71. package/treb-charts/style/charts.scss +221 -0
  72. package/treb-charts/style/old-charts.scss +250 -0
  73. package/treb-embed/markup/layout.html +137 -0
  74. package/treb-embed/markup/toolbar.html +175 -0
  75. package/treb-embed/modern.tsconfig.json +25 -0
  76. package/treb-embed/src/custom-element/content-types.d.ts +18 -0
  77. package/treb-embed/src/custom-element/global.d.ts +11 -0
  78. package/treb-embed/src/custom-element/spreadsheet-constructor.ts +1227 -0
  79. package/treb-embed/src/custom-element/treb-global.ts +44 -0
  80. package/treb-embed/src/custom-element/treb-spreadsheet-element.ts +52 -0
  81. package/treb-embed/src/embedded-spreadsheet.ts +5362 -0
  82. package/treb-embed/src/index.ts +16 -0
  83. package/treb-embed/src/language-model.ts +41 -0
  84. package/treb-embed/src/options.ts +320 -0
  85. package/treb-embed/src/progress-dialog.ts +228 -0
  86. package/treb-embed/src/selection-state.ts +16 -0
  87. package/treb-embed/src/spinner.ts +42 -0
  88. package/treb-embed/src/toolbar-message.ts +96 -0
  89. package/treb-embed/src/types.ts +167 -0
  90. package/treb-embed/style/autocomplete.scss +103 -0
  91. package/treb-embed/style/dark-theme.scss +114 -0
  92. package/treb-embed/style/defaults.scss +36 -0
  93. package/treb-embed/style/dialog.scss +181 -0
  94. package/treb-embed/style/dropdown-select.scss +101 -0
  95. package/treb-embed/style/formula-bar.scss +193 -0
  96. package/treb-embed/style/grid.scss +374 -0
  97. package/treb-embed/style/layout.scss +424 -0
  98. package/treb-embed/style/mouse-mask.scss +67 -0
  99. package/treb-embed/style/note.scss +92 -0
  100. package/treb-embed/style/overlay-editor.scss +102 -0
  101. package/treb-embed/style/spinner.scss +92 -0
  102. package/treb-embed/style/tab-bar.scss +228 -0
  103. package/treb-embed/style/table.scss +80 -0
  104. package/treb-embed/style/theme-defaults.scss +444 -0
  105. package/treb-embed/style/toolbar.scss +416 -0
  106. package/treb-embed/style/tooltip.scss +68 -0
  107. package/treb-embed/style/treb-icons.scss +130 -0
  108. package/treb-embed/style/treb-spreadsheet-element.scss +20 -0
  109. package/treb-embed/style/z-index.scss +43 -0
  110. package/treb-export/docs/charts.md +68 -0
  111. package/treb-export/modern.tsconfig.json +19 -0
  112. package/treb-export/package.json +4 -0
  113. package/treb-export/src/address-type.ts +77 -0
  114. package/treb-export/src/base-template.ts +22 -0
  115. package/treb-export/src/column-width.ts +85 -0
  116. package/treb-export/src/drawing2/chart-template-components2.ts +389 -0
  117. package/treb-export/src/drawing2/chart2.ts +282 -0
  118. package/treb-export/src/drawing2/column-chart-template2.ts +521 -0
  119. package/treb-export/src/drawing2/donut-chart-template2.ts +296 -0
  120. package/treb-export/src/drawing2/drawing2.ts +355 -0
  121. package/treb-export/src/drawing2/embedded-image.ts +71 -0
  122. package/treb-export/src/drawing2/scatter-chart-template2.ts +555 -0
  123. package/treb-export/src/export-worker/export-worker.ts +99 -0
  124. package/treb-export/src/export-worker/index-modern.ts +22 -0
  125. package/treb-export/src/export2.ts +2204 -0
  126. package/treb-export/src/import2.ts +882 -0
  127. package/treb-export/src/relationship.ts +36 -0
  128. package/treb-export/src/shared-strings2.ts +128 -0
  129. package/treb-export/src/template-2.ts +22 -0
  130. package/treb-export/src/unescape_xml.ts +47 -0
  131. package/treb-export/src/workbook-sheet2.ts +182 -0
  132. package/treb-export/src/workbook-style2.ts +1285 -0
  133. package/treb-export/src/workbook-theme2.ts +88 -0
  134. package/treb-export/src/workbook2.ts +491 -0
  135. package/treb-export/src/xml-utils.ts +201 -0
  136. package/treb-export/template/base/[Content_Types].xml +2 -0
  137. package/treb-export/template/base/_rels/.rels +2 -0
  138. package/treb-export/template/base/docProps/app.xml +2 -0
  139. package/treb-export/template/base/docProps/core.xml +12 -0
  140. package/treb-export/template/base/xl/_rels/workbook.xml.rels +2 -0
  141. package/treb-export/template/base/xl/sharedStrings.xml +2 -0
  142. package/treb-export/template/base/xl/styles.xml +2 -0
  143. package/treb-export/template/base/xl/theme/theme1.xml +2 -0
  144. package/treb-export/template/base/xl/workbook.xml +2 -0
  145. package/treb-export/template/base/xl/worksheets/sheet1.xml +2 -0
  146. package/treb-export/template/base.xlsx +0 -0
  147. package/treb-format/package.json +8 -0
  148. package/treb-format/src/format.test.ts +213 -0
  149. package/treb-format/src/format.ts +942 -0
  150. package/treb-format/src/format_cache.ts +199 -0
  151. package/treb-format/src/format_parser.ts +723 -0
  152. package/treb-format/src/index.ts +25 -0
  153. package/treb-format/src/number_format_section.ts +100 -0
  154. package/treb-format/src/value_parser.ts +337 -0
  155. package/treb-grid/package.json +5 -0
  156. package/treb-grid/src/editors/autocomplete.ts +394 -0
  157. package/treb-grid/src/editors/autocomplete_matcher.ts +260 -0
  158. package/treb-grid/src/editors/formula_bar.ts +473 -0
  159. package/treb-grid/src/editors/formula_editor_base.ts +910 -0
  160. package/treb-grid/src/editors/overlay_editor.ts +511 -0
  161. package/treb-grid/src/index.ts +37 -0
  162. package/treb-grid/src/layout/base_layout.ts +2618 -0
  163. package/treb-grid/src/layout/grid_layout.ts +299 -0
  164. package/treb-grid/src/layout/rectangle_cache.ts +86 -0
  165. package/treb-grid/src/render/selection-renderer.ts +414 -0
  166. package/treb-grid/src/render/svg_header_overlay.ts +93 -0
  167. package/treb-grid/src/render/svg_selection_block.ts +187 -0
  168. package/treb-grid/src/render/tile_renderer.ts +2122 -0
  169. package/treb-grid/src/types/annotation.ts +216 -0
  170. package/treb-grid/src/types/border_constants.ts +34 -0
  171. package/treb-grid/src/types/clipboard_data.ts +31 -0
  172. package/treb-grid/src/types/data_model.ts +334 -0
  173. package/treb-grid/src/types/drag_mask.ts +81 -0
  174. package/treb-grid/src/types/grid.ts +7743 -0
  175. package/treb-grid/src/types/grid_base.ts +3644 -0
  176. package/treb-grid/src/types/grid_command.ts +470 -0
  177. package/treb-grid/src/types/grid_events.ts +124 -0
  178. package/treb-grid/src/types/grid_options.ts +97 -0
  179. package/treb-grid/src/types/grid_selection.ts +60 -0
  180. package/treb-grid/src/types/named_range.ts +369 -0
  181. package/treb-grid/src/types/scale-control.ts +202 -0
  182. package/treb-grid/src/types/serialize_options.ts +72 -0
  183. package/treb-grid/src/types/set_range_options.ts +52 -0
  184. package/treb-grid/src/types/sheet.ts +3099 -0
  185. package/treb-grid/src/types/sheet_types.ts +95 -0
  186. package/treb-grid/src/types/tab_bar.ts +464 -0
  187. package/treb-grid/src/types/tile.ts +59 -0
  188. package/treb-grid/src/types/update_flags.ts +75 -0
  189. package/treb-grid/src/util/dom_utilities.ts +44 -0
  190. package/treb-grid/src/util/fontmetrics2.ts +179 -0
  191. package/treb-grid/src/util/ua.ts +104 -0
  192. package/treb-logo.svg +18 -0
  193. package/treb-parser/package.json +5 -0
  194. package/treb-parser/src/csv-parser.ts +122 -0
  195. package/treb-parser/src/index.ts +25 -0
  196. package/treb-parser/src/md-parser.ts +526 -0
  197. package/treb-parser/src/parser-types.ts +397 -0
  198. package/treb-parser/src/parser.test.ts +298 -0
  199. package/treb-parser/src/parser.ts +2673 -0
  200. package/treb-utils/package.json +5 -0
  201. package/treb-utils/src/dispatch.ts +57 -0
  202. package/treb-utils/src/event_source.ts +147 -0
  203. package/treb-utils/src/ievent_source.ts +33 -0
  204. package/treb-utils/src/index.ts +31 -0
  205. package/treb-utils/src/measurement.ts +174 -0
  206. package/treb-utils/src/resizable.ts +160 -0
  207. package/treb-utils/src/scale.ts +137 -0
  208. package/treb-utils/src/serialize_html.ts +124 -0
  209. package/treb-utils/src/template.ts +70 -0
  210. package/treb-utils/src/validate_uri.ts +61 -0
  211. package/tsconfig.json +10 -0
  212. package/tsproject.json +30 -0
  213. package/util/license-plugin-esbuild.js +86 -0
  214. package/util/list-css-vars.sh +46 -0
  215. package/README-esm.md +0 -37
  216. package/treb-bundle.css +0 -2
  217. package/treb-bundle.mjs +0 -15
@@ -0,0 +1,1841 @@
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 { Area, Size, Point } from './rectangle';
23
+ import { DonutSlice, LegendLayout, LegendOptions, LegendPosition, LegendStyle } from './chart-types';
24
+ import type { RangeScale } from 'treb-utils';
25
+
26
+ const SVGNS = 'http://www.w3.org/2000/svg';
27
+
28
+ export interface Metrics {
29
+ width: number;
30
+ height: number;
31
+ y_offset: number;
32
+ }
33
+
34
+ // const trident = /trident/i.test(navigator?.userAgent || '');
35
+
36
+ /*
37
+ let dom_parser: DOMParser | undefined;
38
+ const SetSVG = trident ? (node: SVGElement, svg: string) => {
39
+
40
+ if (!dom_parser) {
41
+ dom_parser = new DOMParser();
42
+ (dom_parser as any).async = false;
43
+ }
44
+
45
+ const element = dom_parser.parseFromString(
46
+ '<svg xmlns=\'http://www.w3.org/2000/svg\' xmlns:xlink=\'http://www.w3.org/1999/xlink\'>' + svg + '</svg>',
47
+ 'text/xml').documentElement;
48
+
49
+ node.textContent = '';
50
+
51
+ let child = element.firstChild;
52
+
53
+ while (child) {
54
+ node.appendChild(document.importNode(child, true));
55
+ child = child.nextSibling;
56
+ }
57
+
58
+ } : (node: SVGElement, svg: string) => node.innerHTML = svg;
59
+ */
60
+
61
+ const SVGNode = (tag: string, attribute_map: {[index: string]: any} = {}, text?: string): SVGElement => {
62
+ const node = document.createElementNS(SVGNS, tag);
63
+ for (const key of Object.keys(attribute_map)) {
64
+ if (attribute_map[key] !== undefined) {
65
+ const value = attribute_map[key];
66
+ node.setAttribute(key, Array.isArray(value) ? value.join(' ') : value.toString());
67
+ }
68
+ }
69
+ if (text) { node.textContent = text; }
70
+ return node;
71
+ };
72
+
73
+ /**
74
+ * FIXME: normalize API, make canvas version
75
+ */
76
+ export class ChartRenderer {
77
+
78
+ public parent!: HTMLElement;
79
+ public svg_node!: SVGElement;
80
+ public text_measurement_node?: SVGTextElement;
81
+
82
+ public container_group: SVGGElement;
83
+ public group: SVGGElement;
84
+ public axis_group: SVGGElement;
85
+ public label_group: SVGGElement;
86
+
87
+ public size: Size = { width: 0, height: 0 };
88
+ public bounds: Area = new Area();
89
+
90
+ // public smoothing_factor = 0.2;
91
+
92
+ constructor() {
93
+ this.container_group = SVGNode('g') as SVGGElement;
94
+
95
+ this.group = SVGNode('g') as SVGGElement;
96
+ this.axis_group = SVGNode('g', {class: 'axis-group'}) as SVGGElement;
97
+ this.label_group = SVGNode('g', {class: 'label-group'}) as SVGGElement;
98
+
99
+ this.container_group.appendChild(this.axis_group);
100
+ this.container_group.appendChild(this.group);
101
+ this.container_group.appendChild(this.label_group);
102
+ }
103
+
104
+ public Initialize(node: HTMLElement): void {
105
+ this.parent = node;
106
+
107
+ this.svg_node = SVGNode('svg', {
108
+ class: 'treb-chart',
109
+ // style: 'overflow: hidden; position: relative; width: 100%; height: 100%;'
110
+ });
111
+ this.svg_node.style.overflow = 'hidden';
112
+ this.svg_node.style.position = 'relative';
113
+ this.svg_node.style.width = '100%';
114
+ this.svg_node.style.height = '100%';
115
+
116
+ // this.group = document.createElementNS(SVGNS, 'g');
117
+ this.svg_node.appendChild(this.container_group);
118
+
119
+ // FIXME: validate parent is relative/absolute
120
+
121
+ this.parent.appendChild(this.svg_node);
122
+ this.Resize();
123
+ }
124
+
125
+ public Legend(options: LegendOptions): void {
126
+ const group = SVGNode('g');
127
+ this.group.appendChild(group);
128
+
129
+ const measure = SVGNode('text');
130
+ group.appendChild(measure);
131
+
132
+ // IE says no
133
+ // group.classList.add('legend');
134
+ group.setAttribute('class', 'legend');
135
+
136
+ const rows: number[][] = [[]];
137
+ const padding = 10;
138
+ let space = options.area.width;
139
+ let row = 0;
140
+ let max_height = 0;
141
+ const width = options.area.width;
142
+
143
+ const marker_width = (options.style === LegendStyle.marker) ? 14 : 26;
144
+
145
+ const metrics = options.labels.map((label, index) => {
146
+ measure.textContent = label.label;
147
+
148
+ const text_rect = measure.getBoundingClientRect();
149
+ const text_metrics = { width: text_rect.width, height: text_rect.height };
150
+ const composite = text_metrics.width + marker_width + padding;
151
+
152
+ max_height = Math.max(max_height, text_metrics.height);
153
+
154
+ if (options.layout === LegendLayout.vertical) {
155
+ rows[index] = [index];
156
+ }
157
+ else {
158
+ if (composite > space) {
159
+ if (rows[row].length === 0) {
160
+
161
+ // there's nothing in this row, so moving to the next
162
+ // row will not help; stick it in here regardless
163
+
164
+ rows[row].push(index);
165
+ row++;
166
+ rows[row] = [];
167
+ space = width;
168
+ }
169
+ else {
170
+ row++;
171
+ rows[row] = [index];
172
+ space = width - composite;
173
+ }
174
+ }
175
+ else {
176
+ rows[row].push(index);
177
+ space -= composite;
178
+ }
179
+ }
180
+
181
+ return text_metrics;
182
+ });
183
+
184
+ // IE11: SVG element doesn't have parent element? (...)
185
+
186
+ // measure.parentElement?.removeChild(measure);
187
+ group.removeChild(measure);
188
+
189
+ let y = max_height;
190
+
191
+ let layout = options.layout || LegendLayout.horizontal;
192
+ if (layout === LegendLayout.horizontal && rows.every(row => row.length <= 1)) {
193
+ layout = LegendLayout.horizontal;
194
+ }
195
+
196
+ for (let row = 0; row < rows.length; row++) {
197
+
198
+ const row_width = rows[row].reduce((a, x) => a + metrics[x].width + marker_width, (rows[row].length - 1) * padding);
199
+
200
+ let h = 0;
201
+ let x = layout === LegendLayout.horizontal ?
202
+ Math.round((width - row_width) / 2) :
203
+ Math.round(padding / 2);
204
+
205
+ for (let col = 0; col < rows[row].length; col++) {
206
+
207
+ const index = rows[row][col];
208
+ const text_metrrics = metrics[index];
209
+ const label = options.labels[index];
210
+
211
+ const marker_y = y - 1; // Math.round(y + text_metrrics.height / 2);
212
+
213
+ // NOTE: trident offset is inlined here
214
+
215
+ let trident = false;
216
+ if (typeof navigator !== 'undefined') {
217
+ trident = /trident/i.test(navigator?.userAgent || '');
218
+ }
219
+
220
+ const color = typeof label.index === 'number' ? label.index : index + 1;
221
+
222
+ group.appendChild(SVGNode('text', {
223
+ 'dominant-baseline': 'middle', x: x + marker_width, y, dy: (trident ? '.3em' : undefined) }, label.label));
224
+
225
+ if (options.style === LegendStyle.marker) {
226
+ group.appendChild(SVGNode('rect', {
227
+ class: `series-${color}`, x, y: marker_y - 4, width: 8, height: 8 }));
228
+ }
229
+ else {
230
+ group.appendChild(SVGNode('rect', {
231
+ class: `series-${color}`, x, y: marker_y - 1, width: marker_width - 3, height: 2}));
232
+ }
233
+
234
+ h = Math.max(h, text_metrrics.height);
235
+ x += text_metrrics.width + marker_width + padding;
236
+
237
+ }
238
+
239
+ y = Math.round(y + h * 1.1);
240
+ }
241
+
242
+ const rect = group.getBoundingClientRect();
243
+ const legend_size = { width: rect.width, height: rect.height + max_height };
244
+
245
+ switch (options.position) {
246
+ case LegendPosition.bottom:
247
+ group.setAttribute('transform', `translate(${options.area.left}, ${options.area.bottom - legend_size.height})`);
248
+ break;
249
+
250
+ case LegendPosition.left:
251
+ group.setAttribute('transform', `translate(${options.area.left}, ${options.area.top})`);
252
+ break;
253
+
254
+ case LegendPosition.right:
255
+ group.setAttribute('transform', `translate(${options.area.right - legend_size.width}, ${options.area.top})`);
256
+ break;
257
+
258
+ case LegendPosition.top:
259
+ default:
260
+ group.setAttribute('transform', `translate(${options.area.left}, ${options.area.top})`);
261
+ }
262
+
263
+ if (options.position === LegendPosition.top) {
264
+ options.area.top += legend_size.height || 0;
265
+ }
266
+ else if (options.position === LegendPosition.right) {
267
+ options.area.right -= ((legend_size.width || 0) + 8); // 8?
268
+ }
269
+ else if (options.position === LegendPosition.left) {
270
+ options.area.left += ((legend_size.width || 0) + 8);
271
+ }
272
+ else {
273
+ options.area.bottom -= legend_size.height || 0;
274
+ }
275
+
276
+ // return legend_size;
277
+
278
+ }
279
+
280
+ public Clear(class_name?: string): void {
281
+ this.group.textContent = '';
282
+ this.axis_group.textContent = '';
283
+ this.label_group.textContent = '';
284
+ class_name = 'treb-chart' + (class_name ? ' ' + class_name: '');
285
+ this.svg_node.setAttribute('class', class_name);
286
+ }
287
+
288
+ public Resize(): void {
289
+ const bounds = this.parent.getBoundingClientRect();
290
+ this.svg_node.setAttribute('width', bounds.width.toString());
291
+ this.svg_node.setAttribute('height', bounds.height.toString());
292
+ this.size = {
293
+ width: bounds.width,
294
+ height: bounds.height,
295
+ };
296
+ }
297
+
298
+ /**
299
+ * initialize before render. this assumes that document layout/scroll
300
+ * won't change during the render pass, so we can cache some values.
301
+ */
302
+ public Prerender(): void {
303
+ const bounds = this.svg_node.getBoundingClientRect();
304
+ this.bounds.top = bounds.top;
305
+ this.bounds.left = bounds.left;
306
+ this.bounds.right = bounds.right;
307
+ this.bounds.bottom = bounds.bottom;
308
+ }
309
+
310
+ /**
311
+ * render title. this method modifies "area" in place -- that's
312
+ * the style we want to use going forward.
313
+ *
314
+ * @param title
315
+ * @param area
316
+ * @param margin
317
+ * @param layout
318
+ */
319
+ public RenderTitle(
320
+ title: string,
321
+ area: Area,
322
+ margin: number,
323
+ layout: 'top'|'bottom'): void {
324
+
325
+ const text = SVGNode('text', {
326
+ class: 'chart-title',
327
+ x: Math.round(area.width / 2),
328
+ // style: 'text-anchor: middle',
329
+ }, title);
330
+ text.style.textAnchor = 'middle';
331
+
332
+ this.group.appendChild(text);
333
+ const bounds = text.getBoundingClientRect();
334
+
335
+ switch (layout) {
336
+ case 'bottom':
337
+ text.setAttribute('y', Math.round(area.bottom - bounds.height).toString());
338
+ area.bottom -= (bounds.height + margin);
339
+ break;
340
+
341
+ default:
342
+ text.setAttribute('y', Math.round(area.top + margin + bounds.height).toString());
343
+ area.top += (bounds.height + margin);
344
+ break;
345
+ }
346
+
347
+ }
348
+
349
+ /**
350
+ * measure a label, optionally with class name(s)
351
+ *
352
+ * this is silly. you are doing the measurement on a random node and
353
+ * trying to match classes, while you could just do the measurement on
354
+ * the actual node, get actual classes right, and not bother with junk
355
+ * nodes.
356
+ *
357
+ * FIXME: decprecate
358
+ *
359
+ */
360
+ public MeasureText(label: string, classes?: string | string[], ceil = false): Metrics {
361
+
362
+ if (!this.text_measurement_node) {
363
+ this.text_measurement_node = SVGNode('text', { x: '-100px', y: '-100px' }) as SVGTextElement;
364
+ this.svg_node.appendChild(this.text_measurement_node);
365
+ }
366
+
367
+ if (typeof classes !== 'undefined') {
368
+ if (typeof classes === 'string') {
369
+ classes = [classes];
370
+ }
371
+ this.text_measurement_node.setAttribute('class', classes.join(' '));
372
+ }
373
+ else {
374
+ this.text_measurement_node.setAttribute('class', '');
375
+ }
376
+
377
+ this.text_measurement_node.textContent = label;
378
+
379
+ const bounds = this.text_measurement_node.getBoundingClientRect();
380
+
381
+ const metrics = {
382
+ width: bounds.width,
383
+ height: bounds.height,
384
+
385
+ // wtf is this?
386
+ y_offset: bounds.height - ((this.bounds.top - bounds.top) - 100),
387
+ };
388
+
389
+ if (ceil) {
390
+ metrics.width = Math.ceil(metrics.width);
391
+ metrics.height = Math.ceil(metrics.height);
392
+ metrics.y_offset = Math.ceil(metrics.y_offset);
393
+ }
394
+
395
+ return metrics;
396
+ }
397
+
398
+ public RenderTicks(area: Area,
399
+ top: number, bottom: number, count: number, classes?: string | string[]) {
400
+
401
+ const d: string[] = [];
402
+
403
+ const step = area.width / (count);
404
+ for (let i = 0; i < count; i++) {
405
+ const center = Math.round(area.left + step / 2 + step * i) - 0.5;
406
+ d.push(`M${center} ${top} L${center} ${bottom}`);
407
+ }
408
+
409
+ this.group.appendChild(SVGNode('path', {d, class: classes}));
410
+
411
+ }
412
+
413
+ /*
414
+ public GetAxisNode(): SVGElement {
415
+ if (!this.axis_group) {
416
+ this.axis_group = SVGNode('g', {class: 'axis-group'});
417
+ this.group.appendChild(this.axis_group);
418
+ }
419
+ return this.axis_group;
420
+ }
421
+ */
422
+
423
+ /** specialization for bar; it's different enough that we want special treatment */
424
+ public RenderXAxisBar(
425
+ area: Area,
426
+ offset: boolean,
427
+ labels: string[],
428
+ metrics: Metrics[],
429
+ classes?: string | string[]): void {
430
+
431
+ const count = labels.length;
432
+ if (!count) return;
433
+
434
+ // FIXME: base on font, ' ' character
435
+ const label_buffer = 4;
436
+
437
+ const step = offset ? area.width / count : area.width / (count - 1);
438
+ const initial_offset = offset ? (step / 2) : 0;
439
+
440
+ // calculate increment (skip_count)
441
+ let increment = 1;
442
+ let repeat = true;
443
+
444
+ while (repeat) {
445
+ repeat = false;
446
+ let extent = 0;
447
+ for (let i = 0; i < count; i += increment) {
448
+ const center = Math.round(area.left + initial_offset + step * i);
449
+ const left = center - metrics[i].width / 2;
450
+ if (extent && (left <= extent)) {
451
+ increment++;
452
+ repeat = true;
453
+ break;
454
+ }
455
+
456
+ // FIXME: buffer? they get pretty tight sometimes
457
+
458
+ extent = center + (metrics[i].width / 2) + label_buffer;
459
+ }
460
+ }
461
+
462
+ // const axis = this.GetAxisNode();
463
+
464
+ for (let i = 0; i < count; i += increment) {
465
+ const x = Math.round(area.left + initial_offset + step * i);
466
+ // if (x + metrics[i].width / 2 >= area.right) { break; }
467
+ this.RenderText(this.axis_group, labels[i], 'center', { x, y: area.bottom }, classes);
468
+ }
469
+
470
+ }
471
+
472
+ public RenderXAxisTicks(area: Area, offset: boolean, count: number): void {
473
+
474
+ const step = offset ? area.width / count : area.width / (count - 1);
475
+ const initial_offset = offset ? (step / 2) : 0;
476
+
477
+ const d: string[] = [];
478
+ for (let i = 0; i < count; i++) {
479
+ const center = Math.round(area.left + initial_offset + step * i) + .5;
480
+ d.push(`M${center},${area.bottom + .5} v${6}`)
481
+ }
482
+
483
+ this.axis_group.appendChild(SVGNode('path', {d: d.join(' '), class: 'x-axis-tick axis-tick'}));
484
+
485
+ }
486
+
487
+ /**
488
+ * render x axis labels; skips over labels to prevent overlap
489
+ *
490
+ * @param offset - move label by 1/2 step width, to center it under columns.
491
+ */
492
+ public RenderXAxis(
493
+ area: Area,
494
+ offset: boolean,
495
+ labels: string[],
496
+ metrics: Metrics[],
497
+ classes?: string | string[]): void {
498
+
499
+ const count = labels.length;
500
+ if (!count) return;
501
+
502
+ // FIXME: base on font, ' ' character
503
+ const label_buffer = 4;
504
+
505
+ const step = offset ? area.width / count : area.width / (count - 1);
506
+ // const initial_offset = shift ? (step / 2) : 0;
507
+ const initial_offset = offset ? (step / 2) : 0;
508
+
509
+ // calculate increment (skip_count)
510
+ let increment = 1;
511
+ let repeat = true;
512
+
513
+ const f2 = (labels.length - 1) % 2 === 0;
514
+ const f3 = (labels.length - 1) % 3 === 0;
515
+ // const f5 = (labels.length - 1) % 5 === 0;
516
+
517
+ while (repeat) {
518
+ repeat = false;
519
+ let extent = 0;
520
+ for (let i = 0; i < count; i += increment) {
521
+ const center = Math.round(area.left + initial_offset + step * i);
522
+ const left = center - metrics[i].width / 2;
523
+ if (extent && (left <= extent)) {
524
+ increment++;
525
+ repeat = true;
526
+ break;
527
+ }
528
+
529
+ // FIXME: buffer? they get pretty tight sometimes
530
+
531
+ extent = center + (metrics[i].width / 2) + label_buffer;
532
+ }
533
+ }
534
+
535
+ // special patch for 0% - 100% range...
536
+
537
+ if (increment === 3 && !f3 && f2) {
538
+ increment++;
539
+ }
540
+
541
+ // const axis = this.GetAxisNode();
542
+
543
+ for (let i = 0; i < count; i += increment) {
544
+ const x = Math.round(area.left + initial_offset + step * i);
545
+ // if (x + metrics[i].width / 2 >= area.right) { break; }
546
+ this.RenderText(this.axis_group, labels[i], 'center', { x, y: area.bottom }, classes);
547
+ }
548
+
549
+ }
550
+
551
+ /** specialization for bar; it's different enough that we want special treatment */
552
+ public RenderYAxisBar(area: Area, left: number,
553
+ labels: Array<{
554
+ label: string;
555
+ metrics: Metrics;
556
+ }>, classes?: string | string[]) {
557
+
558
+ labels = labels.slice(0);
559
+ labels.reverse();
560
+
561
+ const count = labels.length;
562
+ if (!count) return;
563
+
564
+ const step = area.height / count;
565
+
566
+ // calculate increment (skip count)
567
+ let increment = 1;
568
+ let repeat = true;
569
+
570
+ while (repeat) {
571
+ repeat = false;
572
+ let extent = 0;
573
+ for (let i = 0; i < count; i += increment) {
574
+ const label = labels[i];
575
+ const y = Math.round(area.bottom - step * (i + .5) + label.metrics.height / 4);
576
+ if (extent && y >= extent) {
577
+ increment++;
578
+ repeat = true;
579
+ break;
580
+ }
581
+ extent = y - label.metrics.height;
582
+ }
583
+ }
584
+
585
+ // const axis = this.GetAxisNode();
586
+
587
+ for (let i = 0; i < count; i += increment) {
588
+ const label = labels[i];
589
+ const y = Math.round(area.bottom - step * (i + .5) + label.metrics.height / 4);
590
+ this.RenderText(this.axis_group, label.label, 'right', { x: left, y }, classes);
591
+ }
592
+
593
+ }
594
+
595
+ /**
596
+ * render y axis labels; skips over labels to prevent overlap
597
+ */
598
+ public RenderYAxis(area: Area, left: number,
599
+ labels: Array<{
600
+ label: string;
601
+ metrics: Metrics;
602
+ }>, classes?: string | string[]) {
603
+
604
+ const count = labels.length;
605
+ if (!count) return;
606
+
607
+ const step = area.height / (count - 1);
608
+
609
+ // calculate increment (skip count)
610
+ let increment = 1;
611
+ let repeat = true;
612
+
613
+ while (repeat) {
614
+ repeat = false;
615
+ let extent = 0;
616
+ for (let i = 0; i < count; i += increment) {
617
+ const label = labels[i];
618
+ const y = Math.round(area.bottom - step * i + label.metrics.height / 4);
619
+ if (extent && y >= extent) {
620
+ increment++;
621
+ repeat = true;
622
+ break;
623
+ }
624
+ extent = y - label.metrics.height;
625
+ }
626
+ }
627
+
628
+ // const axis = this.GetAxisNode();
629
+
630
+ for (let i = 0; i < count; i += increment) {
631
+ const label = labels[i];
632
+ const y = Math.round(area.bottom - step * i + label.metrics.height / 4);
633
+ this.RenderText(this.axis_group, label.label, 'right', { x: left, y }, classes);
634
+ }
635
+
636
+ }
637
+
638
+ /*
639
+ public ControlPoint(current: Point, previous?: Point, next?: Point, reverse = false): Point {
640
+
641
+ previous = previous || current;
642
+ next = next || current;
643
+
644
+ const o = this.LineProperties(previous, next);
645
+ const factor = Math.pow(1 - Math.abs(o.angle) / Math.PI, 2) * this.smoothing_factor;
646
+
647
+ const angle = o.angle + (reverse ? Math.PI : 0);
648
+ const length = o.length * factor;
649
+
650
+ const x = current.x + Math.cos(angle) * length;
651
+ const y = current.y + Math.sin(angle) * length;
652
+
653
+ return { x, y };
654
+
655
+ }
656
+ */
657
+
658
+ public LineProperties(a: Point, b: Point) {
659
+
660
+ const x = b.x - a.x;
661
+ const y = b.y - a.y;
662
+
663
+ return {
664
+ length: Math.sqrt((x * x) + (y * y)),
665
+ angle: Math.atan2(y, x),
666
+ };
667
+
668
+ }
669
+
670
+ public RenderSmoothLine(
671
+ area: Area,
672
+ data: Array<number | undefined>,
673
+ fill = false,
674
+ titles?: string[],
675
+ classes?: string | string[]): void {
676
+
677
+
678
+ // const node = document.createElementNS(SVGNS, 'path');
679
+ const group = SVGNode('g');
680
+
681
+ const d1: string[] = [];
682
+ const d2: string[] = [];
683
+
684
+ const count = data.length;
685
+ const steps = count - 1;
686
+ const step = (area.width / count) / 2;
687
+
688
+ const circles: Array<{
689
+ x: number;
690
+ y: number;
691
+ i: number;
692
+ }> = [];
693
+
694
+ const points: Array<Point | undefined> = data.map((value, i) => {
695
+ if (typeof value === 'undefined') {
696
+ return undefined;
697
+ }
698
+ return {
699
+ x: Math.round(area.left + area.width / steps * i),
700
+ y: area.bottom - value,
701
+ };
702
+ });
703
+
704
+ ///
705
+
706
+
707
+ // we need to split into segments in the event of missing data
708
+
709
+ let segment: Point[] = [];
710
+ const render_segment = () => {
711
+
712
+ if (segment.length < 2){ return; }
713
+
714
+ let line = '';
715
+ const first = segment[0];
716
+ const last = segment[segment.length-1];
717
+
718
+ // note here we're not adding the leading M because for area,
719
+ // we want to use an L instead (or it won't be contiguous)
720
+
721
+ if (segment.length === 2) {
722
+ line = `${segment[0].x},${segment[0].y} L${segment[1].x},${segment[1].y}`;
723
+ }
724
+ else if (segment.length > 2) {
725
+ const curve = this.CatmullRomChain(segment);
726
+ line = '' + curve.map(point => `${point.x},${point.y}`).join(' L');
727
+ }
728
+
729
+ if (line) {
730
+ d1.push('M' + line);
731
+ if (fill) {
732
+ d2.push(`M ${first.x},${area.bottom} L ${first.x},${first.y}`);
733
+ d2.push('L' + line);
734
+ d2.push(`L ${last.x},${area.bottom}`);
735
+ }
736
+ }
737
+
738
+ };
739
+
740
+ for (const point of points) {
741
+ if (!point) {
742
+ render_segment();
743
+ segment = [];
744
+ }
745
+ else {
746
+ segment.push(point);
747
+ }
748
+ }
749
+ // render?
750
+ if (segment.length) {
751
+ render_segment();
752
+ }
753
+
754
+
755
+ ///
756
+
757
+ /*
758
+
759
+ for (let i = 0; i < points.length; i++) {
760
+
761
+ const point = points[i];
762
+
763
+ if (point) {
764
+ if (move) {
765
+ d1.push(`M ${[point.x]},${point.y}`);
766
+ if (fill) {
767
+ d2.push(`M ${point.x} ${area.bottom} L ${[point.x]},${point.y}`);
768
+ }
769
+ }
770
+ else {
771
+ const cp_start = this.ControlPoint(points[i - 1] as Point, points[i - 2], point);
772
+ const cp_end = this.ControlPoint(point, points[i - 1], points[i + 1], true);
773
+ d1.push(`C ${cp_start.x},${cp_start.y} ${cp_end.x},${cp_end.y} ${point.x},${point.y}`);
774
+ d2.push(`C ${cp_start.x},${cp_start.y} ${cp_end.x},${cp_end.y} ${point.x},${point.y}`);
775
+ }
776
+ move = false;
777
+ last_point = point;
778
+
779
+ }
780
+ else {
781
+ move = true;
782
+ if (fill && last_point) {
783
+ d2.push(`L ${last_point.x},${area.bottom} Z`);
784
+ }
785
+ last_point = undefined;
786
+ }
787
+
788
+ }
789
+
790
+ if (fill && last_point) {
791
+ d2.push(`L ${last_point.x},${area.bottom} Z`);
792
+ }
793
+
794
+ */
795
+
796
+ /*
797
+
798
+ for (; i < count; i++ ){
799
+ const point = data[i];
800
+ if (typeof point === 'undefined') {
801
+ move = true;
802
+ if (fill && (typeof last_x !== 'undefined')) {
803
+ d2.push(`L${last_x} ${area.bottom}Z`);
804
+ }
805
+ last_x = undefined;
806
+ continue;
807
+ }
808
+ const x = Math.round(area.left + area.width / steps * i);
809
+ if (move) {
810
+ if (fill) {
811
+ d2.push(`M${x} ${area.bottom} L${x} ${area.bottom - point}`);
812
+ }
813
+ d1.push(`M${x} ${area.bottom - point}`);
814
+ }
815
+ else {
816
+ d1.push(`L${x} ${area.bottom - point}`);
817
+ d2.push(`L${x} ${area.bottom - point}`);
818
+ }
819
+
820
+ circles.push({x, y: area.bottom - point, i});
821
+
822
+ last_x = x;
823
+ move = false;
824
+ }
825
+
826
+ */
827
+
828
+ /*
829
+ if (fill && (typeof last_x !== 'undefined')) {
830
+ d2.push(`L${last_x} ${area.bottom}Z`);
831
+ }
832
+ */
833
+
834
+ // fill first, underneath
835
+ if (fill) {
836
+ group.appendChild(SVGNode('path', { class: 'fill', d: d2 }));
837
+ }
838
+
839
+ // then line
840
+ group.appendChild(SVGNode('path', { class: 'line', d: d1 }));
841
+
842
+ if (typeof classes !== 'undefined') {
843
+ if (typeof classes === 'string') {
844
+ classes = [classes];
845
+ }
846
+ group.setAttribute('class', classes.join(' '));
847
+ }
848
+
849
+ this.group.appendChild(group);
850
+
851
+ // circles...
852
+
853
+ if (titles && circles.length) {
854
+ const circle_group = document.createElementNS(SVGNS, 'g');
855
+ for (const circle of circles) {
856
+
857
+ const shape = SVGNode('circle', {cx: circle.x, cy: circle.y, r: step});
858
+
859
+ shape.addEventListener('mouseenter', (event) => {
860
+ this.parent.setAttribute('title', titles[circle.i] || '');
861
+ });
862
+ shape.addEventListener('mouseleave', (event) => {
863
+ this.parent.setAttribute('title', '');
864
+ });
865
+
866
+ circle_group.appendChild(shape);
867
+ }
868
+ circle_group.setAttribute('class', 'mouse-layer');
869
+ this.group.appendChild(circle_group);
870
+
871
+ }
872
+ }
873
+
874
+ public RenderLine(
875
+ area: Area,
876
+ data: Array<number | undefined>,
877
+ fill = false,
878
+ titles?: string[],
879
+ classes?: string | string[]) {
880
+
881
+ // const node = document.createElementNS(SVGNS, 'path');
882
+ const group = document.createElementNS(SVGNS, 'g');
883
+
884
+ const d1: string[] = [];
885
+ const d2: string[] = [];
886
+
887
+ const count = data.length;
888
+ const steps = count - 1;
889
+ const step = (area.width / count) / 2;
890
+
891
+ const circles: Array<{
892
+ x: number;
893
+ y: number;
894
+ i: number;
895
+ }> = [];
896
+
897
+ let i = 0;
898
+ let move = true;
899
+ let last_x: number | undefined;
900
+
901
+ for (; i < count; i++) {
902
+ const point = data[i];
903
+ if (typeof point === 'undefined') {
904
+ move = true;
905
+ if (fill && (typeof last_x !== 'undefined')) {
906
+ d2.push(`L${last_x} ${area.bottom}Z`);
907
+ }
908
+ last_x = undefined;
909
+ continue;
910
+ }
911
+ const x = Math.round(/*step*/ + area.left + area.width / steps * i);
912
+ if (move) {
913
+ if (fill) {
914
+ d2.push(`M${x} ${area.bottom} L${x} ${area.bottom - point}`);
915
+ }
916
+ d1.push(`M${x} ${area.bottom - point}`);
917
+ }
918
+ else {
919
+ d1.push(`L${x} ${area.bottom - point}`);
920
+ d2.push(`L${x} ${area.bottom - point}`);
921
+ }
922
+
923
+ circles.push({ x, y: area.bottom - point, i });
924
+
925
+ last_x = x;
926
+ move = false;
927
+ }
928
+
929
+ if (fill && (typeof last_x !== 'undefined')) {
930
+ d2.push(`L${last_x} ${area.bottom}Z`);
931
+ }
932
+
933
+ // fill first, underneath
934
+ if (fill) {
935
+ group.appendChild(SVGNode('path', { class: 'fill', d: d2 }));
936
+ }
937
+
938
+ // then line
939
+ group.appendChild(SVGNode('path', { class: 'line', d: d1 }));
940
+
941
+ if (typeof classes !== 'undefined') {
942
+ if (typeof classes === 'string') {
943
+ classes = [classes];
944
+ }
945
+ group.setAttribute('class', classes.join(' '));
946
+ }
947
+
948
+ this.group.appendChild(group);
949
+
950
+ // circles...
951
+
952
+ if (titles && circles.length) {
953
+ const circle_group = document.createElementNS(SVGNS, 'g');
954
+ for (const circle of circles) {
955
+
956
+ const shape = SVGNode('circle', { cx: circle.x, cy: circle.y, r: step });
957
+
958
+ /*
959
+ const shape = document.createElementNS(SVGNS, 'circle');
960
+ shape.setAttribute('cx', circle.x.toString());
961
+ shape.setAttribute('cy', circle.y.toString());
962
+ shape.setAttribute('r', (step).toString());
963
+ */
964
+
965
+ shape.addEventListener('mouseenter', (event) => {
966
+ this.parent.setAttribute('title', titles[circle.i] || '');
967
+ });
968
+ shape.addEventListener('mouseleave', (event) => {
969
+ this.parent.setAttribute('title', '');
970
+ });
971
+
972
+ circle_group.appendChild(shape);
973
+ }
974
+ circle_group.setAttribute('class', 'mouse-layer');
975
+ this.group.appendChild(circle_group);
976
+
977
+ }
978
+
979
+ }
980
+
981
+ /**
982
+ * the other RenderGrid function has semantics specifically for area/line.
983
+ * rather than try to shoehorn this in we'll use a different method.
984
+ */
985
+ public RenderBarGrid(area: Area, x_count: number, classes?: string | string[]): void {
986
+
987
+ const d: string[] = [];
988
+
989
+ const step = area.width / (x_count);
990
+ for (let i = 0; i <= x_count; i++) {
991
+ const x = Math.round(area.left + step * i) - 0.5;
992
+ d.push(`M${x} ${area.top} L${x} ${area.bottom}`);
993
+ }
994
+
995
+ this.group.appendChild(SVGNode('path', { d, class: classes }));
996
+
997
+ }
998
+
999
+ public RenderGrid(area: Area, y_count: number, x_count = 0, classes?: string | string[]): void {
1000
+
1001
+ const d: string[] = [];
1002
+
1003
+ let step = area.height / y_count;
1004
+ for (let i = 0; i <= y_count; i++) {
1005
+ const y = Math.round(area.top + step * i) - 0.5;
1006
+ d.push(`M${area.left} ${y} L${area.right} ${y}`);
1007
+ }
1008
+
1009
+ step = area.width / (x_count - 1);
1010
+ for (let i = 0; i < x_count; i++) {
1011
+ const x = Math.round(area.left + step * i) - 0.5;
1012
+ d.push(`M${x} ${area.top} L${x} ${area.bottom}`);
1013
+ }
1014
+
1015
+ this.group.appendChild(SVGNode('path', {d, class: classes}));
1016
+
1017
+ }
1018
+
1019
+ /* *
1020
+ * return the intersection point of two lines (assuming
1021
+ * infinite projection) or undefined if they are parallel
1022
+ * /
1023
+ public LineIntersection(a1: Point, a2: Point, b1: Point, b2: Point): Point|undefined {
1024
+
1025
+ const det = ((a1.x - a2.x) * (b1.y - b2.y) - (a1.y - a2.y) * (b1.x - b2.x));
1026
+
1027
+ if (!det) {
1028
+ return undefined; // parallel
1029
+ }
1030
+
1031
+ const t = ((a1.x - b1.x) * (b1.y - b2.y) - (a1.y - b1.y) * (b1.x - b2.x)) / det;
1032
+
1033
+ return { x: a1.x + t * (a2.x - a1.x), y: a1.y + t * (a2.y - a1.y) };
1034
+
1035
+ }
1036
+ */
1037
+
1038
+ public MultiplyPoint(point: Point, scalar: number): Point {
1039
+ return {
1040
+ x: point.x * scalar,
1041
+ y: point.y * scalar,
1042
+ };
1043
+ }
1044
+
1045
+ public AddPoints(a: Point, b: Point): Point {
1046
+ return {
1047
+ x: a.x + b.x,
1048
+ y: a.y + b.y,
1049
+ };
1050
+ }
1051
+
1052
+ /**
1053
+ * algo from
1054
+ * https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline
1055
+ */
1056
+
1057
+ public CatmullRomSpline(P: Point[], n: number): Point[] {
1058
+
1059
+ // Parametric constant: 0.5 for the centripetal spline,
1060
+ // 0.0 for the uniform spline, 1.0 for the chordal spline.
1061
+ let alpha = .5;
1062
+
1063
+ // Premultiplied power constant for the following tj() function.
1064
+ alpha = alpha/2;
1065
+ const tj = (ti: number, Pi: Point, Pj: Point) => {
1066
+ const {x: xi, y: yi} = Pi
1067
+ const {x: xj, y: yj} = Pj
1068
+ return ((xj-xi)**2 + (yj-yi)**2)**alpha + ti;
1069
+ };
1070
+
1071
+ const t0 = 0
1072
+ const t1 = tj(t0, P[0], P[1]);
1073
+ const t2 = tj(t1, P[1], P[2]);
1074
+ const t3 = tj(t2, P[2], P[3]);
1075
+
1076
+ const step = (t2-t1) / n;
1077
+
1078
+ const points: Point[] = [];
1079
+
1080
+ for (let i = 0; i < n; i++){
1081
+ const t = t1 + step * i;
1082
+
1083
+ const A1 = this.AddPoints(
1084
+ this.MultiplyPoint(P[0], (t1-t)/(t1-t0)),
1085
+ this.MultiplyPoint(P[1], (t-t0)/(t1-t0)),
1086
+ );
1087
+
1088
+ const A2 = this.AddPoints(
1089
+ this.MultiplyPoint(P[1], (t2-t)/(t2-t1)),
1090
+ this.MultiplyPoint(P[2], (t-t1)/(t2-t1)),
1091
+ );
1092
+
1093
+ const A3 = this.AddPoints(
1094
+ this.MultiplyPoint(P[2], (t3-t)/(t3-t2)),
1095
+ this.MultiplyPoint(P[3], (t-t2)/(t3-t2)),
1096
+ );
1097
+
1098
+ const B1 = this.AddPoints(
1099
+ this.MultiplyPoint(A1, (t2-t)/(t2-t0)),
1100
+ this.MultiplyPoint(A2, (t-t0)/(t2-t0)),
1101
+ );
1102
+
1103
+ const B2 = this.AddPoints(
1104
+ this.MultiplyPoint(A2, (t3-t)/(t3-t1)),
1105
+ this.MultiplyPoint(A3, (t-t1)/(t3-t1)),
1106
+ );
1107
+
1108
+ const C = this.AddPoints(
1109
+ this.MultiplyPoint(B1, (t2-t)/(t2-t1)),
1110
+ this.MultiplyPoint(B2, (t-t1)/(t2-t1)),
1111
+ );
1112
+
1113
+ points.push(C);
1114
+
1115
+ }
1116
+
1117
+ return points;
1118
+
1119
+ }
1120
+
1121
+ /**
1122
+ * NOTE: we are munging the point list here, so don't use it after
1123
+ * calling this function or pass in a temp copy
1124
+ *
1125
+ * OK so that was rude, we will not munge the list
1126
+ */
1127
+ public CatmullRomChain(original: Point[], n = 30): Point[] {
1128
+
1129
+ const points = original.slice(0);
1130
+
1131
+ const result: Point[] = [];
1132
+ const len = points.length;
1133
+
1134
+ if (len) {
1135
+
1136
+ // add two trailing points, extended linearly from existing segmnet
1137
+
1138
+ let dx = points[len-1].x - points[len-2].x;
1139
+ let dy = points[len-1].y - points[len-2].y;
1140
+
1141
+ points.push({
1142
+ x: points[len-1].x + dx,
1143
+ y: points[len-1].y + dy,
1144
+ });
1145
+
1146
+ points.push({
1147
+ x: points[len-1].x + dx,
1148
+ y: points[len-1].y + dy,
1149
+ });
1150
+
1151
+ // some for the first point, in the other direction
1152
+
1153
+ dx = points[1].x - points[0].x;
1154
+ dy = points[1].y - points[0].y;
1155
+
1156
+ points.unshift({
1157
+ x: points[0].x - dx,
1158
+ y: points[0].y - dy,
1159
+ });
1160
+
1161
+ for (let i = 0; i < points.length - 4; i++) {
1162
+ const subset = points.slice(i, i + 4);
1163
+ const step = this.CatmullRomSpline(subset, n);
1164
+ result.push(...step);
1165
+ }
1166
+
1167
+ }
1168
+
1169
+ return result;
1170
+
1171
+ }
1172
+
1173
+ public RenderDataLabels(
1174
+ area: Area,
1175
+ x: Array<number | undefined>,
1176
+ y: Array<number | undefined>,
1177
+ x_scale: RangeScale,
1178
+ y_scale: RangeScale,
1179
+ data_labels: Array<string|undefined>,
1180
+ series_index: number ): void {
1181
+
1182
+ // const label_group = SVGNode('g');
1183
+ // this.group.appendChild(label_group);
1184
+
1185
+ const count = Math.max(x.length, y.length);
1186
+ const xrange = (x_scale.max - x_scale.min) || 1;
1187
+ const yrange = (y_scale.max - y_scale.min) || 1;
1188
+
1189
+ for (let i = 0; i < count; i++) {
1190
+
1191
+ const a = x[i];
1192
+ const b = y[i];
1193
+
1194
+ if (a !== undefined && b !== undefined) {
1195
+ const point ={
1196
+ x: area.left + ((a - x_scale.min) / xrange) * area.width,
1197
+ y: area.bottom - ((b - y_scale.min) / yrange) * area.height,
1198
+ };
1199
+ const label = data_labels[i];
1200
+ if (label) {
1201
+
1202
+ this.label_group.appendChild(SVGNode('circle', {class: 'label-target', cx: point.x, cy: point.y, r: 10 }));
1203
+
1204
+ const g = SVGNode('g', {class: 'data-label', transform: `translate(${point.x + 10},${point.y})`});
1205
+ this.label_group.appendChild(g);
1206
+
1207
+ const circle = SVGNode('circle', {
1208
+ cx: -10, y: 0, r: 5, class: `marker-highlight series-${series_index}`
1209
+ });
1210
+ g.appendChild(circle);
1211
+
1212
+ const text = SVGNode('text', {x: 4, y: 0}, label);
1213
+ g.appendChild(text);
1214
+ const bounds = text.getBoundingClientRect();
1215
+ const h = bounds.height;
1216
+ const w = bounds.width + 8;
1217
+
1218
+ if (w + 15 + point.x >= area.right) {
1219
+ g.setAttribute('transform', `translate(${point.x - w - 15},${point.y})`)
1220
+ circle.setAttribute('cx', (w + 15).toString());
1221
+ }
1222
+
1223
+ const rect = SVGNode('path', {d:`M0,5 h${w} v-${h} h-${w} Z`});
1224
+ g.insertBefore(rect, text);
1225
+
1226
+ }
1227
+ }
1228
+ }
1229
+
1230
+ }
1231
+
1232
+ public RenderScatterSeries(area: Area,
1233
+ x: Array<number | undefined>,
1234
+ y: Array<number | undefined>,
1235
+ x_scale: RangeScale,
1236
+ y_scale: RangeScale,
1237
+ lines = true,
1238
+ plot_points = false,
1239
+ filled = false,
1240
+ markers = false,
1241
+ smooth = false,
1242
+ classes?: string | string[]): void {
1243
+
1244
+ // ...
1245
+
1246
+ const count = Math.max(x.length, y.length);
1247
+ const xrange = (x_scale.max - x_scale.min) || 1;
1248
+ const yrange = (y_scale.max - y_scale.min) || 1;
1249
+
1250
+ // const marker_elements: string[] = [];
1251
+ const points: Array<Point | undefined> = [];
1252
+
1253
+ const d: string[] = [];
1254
+ const areas: string[] = [];
1255
+
1256
+ /*
1257
+ const group = document.createElementNS(SVGNS, 'g');
1258
+ if (typeof classes !== 'undefined') {
1259
+ if (typeof classes === 'string') {
1260
+ classes = [classes];
1261
+ }
1262
+ group.setAttribute('class', classes.join(' '));
1263
+ }
1264
+ */
1265
+ const group = SVGNode('g', {class: classes});
1266
+
1267
+ // if (title) node.setAttribute('title', title);
1268
+ this.group.appendChild(group);
1269
+
1270
+ for (let i = 0; i < count; i++) {
1271
+
1272
+ const a = x[i];
1273
+ const b = y[i];
1274
+
1275
+ if (typeof a === 'undefined' || typeof b === 'undefined') {
1276
+ points.push(undefined);
1277
+ }
1278
+ else {
1279
+ points.push({
1280
+ x: area.left + ((a - x_scale.min) / xrange) * area.width,
1281
+ y: area.bottom - ((b - y_scale.min) / yrange) * area.height,
1282
+ });
1283
+ }
1284
+
1285
+ }
1286
+
1287
+ // FIXME: merge loops, if possible
1288
+
1289
+ /*
1290
+ if (markers) {
1291
+ for (const point of points) {
1292
+ if (point) {
1293
+
1294
+ // if we can't use CSS to update the path (except in chrome)
1295
+ // then it's probably not worth it... leave it for now
1296
+
1297
+ // marker_elements.push(`<path d='M0,-1.5 a1.5,1.5,0,1,1,0,3 a1.5,1.5,0,1,1,0,-3' transform='translate(${point.x},${point.y})' class='marker'/>`);
1298
+
1299
+ }
1300
+ }
1301
+ }
1302
+ */
1303
+
1304
+ if (lines) {
1305
+
1306
+ // we need to split into segments in the event of missing data
1307
+
1308
+ let segment: Point[] = [];
1309
+ const render_segment = smooth ? () => {
1310
+
1311
+ // segments < 3 should be straight lines (or points)
1312
+ if (segment.length === 2) {
1313
+ return `${segment[0].x},${segment[0].y} L${segment[1].x},${segment[1].y}`;
1314
+ }
1315
+ else if (segment.length > 2) {
1316
+ const curve = this.CatmullRomChain(segment);
1317
+ return curve.map(point => `${point.x},${point.y}`).join(' L');
1318
+ }
1319
+ return '';
1320
+
1321
+ } : () => {
1322
+ return segment.map(point => `${point.x},${point.y}`).join(' L');
1323
+ };
1324
+
1325
+ for (const point of points) {
1326
+ if (!point) {
1327
+ if (segment.length >= 2) {
1328
+ const line = render_segment();
1329
+ d.push('M' + line);
1330
+ areas.push(`M${segment[0].x},${area.bottom}L` + line + `L${segment[segment.length - 1].x},${area.bottom}Z`);
1331
+ }
1332
+ segment = [];
1333
+ }
1334
+ else {
1335
+ segment.push(point);
1336
+ }
1337
+ }
1338
+
1339
+ if (segment.length >= 2) {
1340
+ const line = render_segment();
1341
+ d.push('M' + line);
1342
+ areas.push(`M${segment[0].x},${area.bottom}L` + line + `L${segment[segment.length - 1].x},${area.bottom}Z`);
1343
+ }
1344
+
1345
+
1346
+ }
1347
+
1348
+ if (filled) {
1349
+ group.appendChild(SVGNode('path', {d: areas, class: 'fill'}));
1350
+ }
1351
+
1352
+ if (lines) {
1353
+ group.appendChild(SVGNode('path', {d, class: 'line'}));
1354
+ }
1355
+
1356
+ if (plot_points) {
1357
+ for (const point of points) {
1358
+ if (point) {
1359
+ group.appendChild(SVGNode('circle', {cx: point.x, cy: point.y, r: 1, class: 'point'}));
1360
+
1361
+ // if we can't use CSS to update the path (except in chrome)
1362
+ // then it's probably not worth it... leave it for now
1363
+
1364
+ // marker_elements.push(`<path d='M0,-1.5 a1.5,1.5,0,1,1,0,3 a1.5,1.5,0,1,1,0,-3' transform='translate(${point.x},${point.y})' class='marker'/>`);
1365
+
1366
+ }
1367
+ }
1368
+
1369
+ }
1370
+
1371
+ if (markers) {
1372
+ for (const point of points) {
1373
+ if (point) {
1374
+ group.appendChild(SVGNode('circle', {cx: point.x, cy: point.y, r: 3, class: 'marker'}));
1375
+
1376
+ // if we can't use CSS to update the path (except in chrome)
1377
+ // then it's probably not worth it... leave it for now
1378
+
1379
+ // marker_elements.push(`<path d='M0,-1.5 a1.5,1.5,0,1,1,0,3 a1.5,1.5,0,1,1,0,-3' transform='translate(${point.x},${point.y})' class='marker'/>`);
1380
+
1381
+ }
1382
+ }
1383
+ }
1384
+
1385
+
1386
+ // SetSVG(group, `<path d='${d.join(' ')}' class='line' />${marker_elements.join('')}`);
1387
+
1388
+ }
1389
+
1390
+
1391
+ public RenderPoints(area: Area, x: number[], y: number[], classes?: string | string[]) {
1392
+
1393
+ // const node = document.createElementNS(SVGNS, 'path');
1394
+ const d: string[] = [];
1395
+
1396
+ for (let i = 0; i < x.length; i++) {
1397
+ const px = x[i] * area.width + area.left;
1398
+ const py = area.bottom - y[i] * area.height;
1399
+ d.push(`M${px - 1},${py - 1} L${px + 1},${py + 1}`);
1400
+ d.push(`M${px - 1},${py + 1} L${px + 1},${py - 1}`);
1401
+ }
1402
+
1403
+ this.group.appendChild(SVGNode('path', {d, class: classes}));
1404
+
1405
+ }
1406
+
1407
+ public RenderPoint(cx: number, cy: number, classes?: string | string[]): void {
1408
+ this.group.appendChild(SVGNode('circle', {cx, cy, r: 1, class: classes}));
1409
+ }
1410
+
1411
+ public RenderCalloutLines(lines: Array<{x1: number, y1: number, x2: number, y2:number, label?: string, classes?: string }>): void {
1412
+
1413
+ const g = SVGNode('g', {class: 'callouts'});
1414
+ this.label_group.appendChild(g);
1415
+
1416
+ for (const line of lines) {
1417
+ g.appendChild(SVGNode('path', {
1418
+ d: `M${line.x1},${line.y1} L${line.x2},${line.y2}`,
1419
+ class: 'callout ' + (line.classes || '').trim(),
1420
+ }));
1421
+ }
1422
+
1423
+ }
1424
+
1425
+ public RenderRectangle(
1426
+ area: Area,
1427
+ corner_radius?: number[],
1428
+ classes?: string | string[],
1429
+ title?: string,
1430
+ label?: string,
1431
+ label_point?: Point): void {
1432
+
1433
+ let d = '';
1434
+
1435
+ if (corner_radius) {
1436
+
1437
+ // two cases we have to worry about: top L/R corner radius > height,
1438
+ // and top/bottom L radius > width
1439
+
1440
+ if (corner_radius[0] &&
1441
+ corner_radius[0] === corner_radius[1] &&
1442
+ corner_radius[0] >= area.height) {
1443
+
1444
+ const c = corner_radius[0];
1445
+ const b = corner_radius[0] - area.height;
1446
+ const a = Math.sqrt(c * c - b * b);
1447
+
1448
+ d = `M${area.left + area.width / 2 - a},${area.bottom} a${c},${c} 0 0 1 ${a * 2},0 z`;
1449
+
1450
+ }
1451
+ else if (corner_radius[1] &&
1452
+ corner_radius[1] === corner_radius[2] &&
1453
+ corner_radius[1] >= area.width) {
1454
+
1455
+ const c = corner_radius[1];
1456
+ const b = corner_radius[1] - area.width;
1457
+ const a = Math.sqrt(c * c - b * b);
1458
+
1459
+ d = `M${area.left},${area.top + area.height / 2 - a} a${c},${c} 0 0 1 0,${a * 2} z`;
1460
+
1461
+ }
1462
+ else {
1463
+ d = `M${area.left},${area.top + corner_radius[0]} `
1464
+ + `a${corner_radius[0]},${corner_radius[0]} 0 0 1 ${corner_radius[0]},${-corner_radius[0]} `
1465
+ + `h${area.width - corner_radius[0] - corner_radius[1]} `
1466
+ + `a${corner_radius[1]},${corner_radius[1]} 0 0 1 ${corner_radius[1]},${corner_radius[1]} `
1467
+ + `v${area.height - corner_radius[1] - corner_radius[2]} `
1468
+ + `a${corner_radius[2]},${corner_radius[2]} 0 0 1 ${-corner_radius[2]},${corner_radius[2]} `
1469
+ + `h${-area.width + corner_radius[2] + corner_radius[3]} `
1470
+ + `a${corner_radius[3]},${corner_radius[3]} 0 0 1 ${-corner_radius[3]},${-corner_radius[3]} `
1471
+ + `v${-area.height + corner_radius[3] + corner_radius[0]} `;
1472
+ }
1473
+ }
1474
+ else {
1475
+ /*
1476
+ node = SVGNode('rect', {
1477
+ x: area.left,
1478
+ y: area.top,
1479
+ width: area.width,
1480
+ height: area.height,
1481
+ class: classes });
1482
+ */
1483
+
1484
+ d = `M${area.left},${area.top} `
1485
+ + `h${area.width} `
1486
+ + `v${area.height} `
1487
+ + `h${-area.width} `
1488
+ + `v${-area.height} `;
1489
+
1490
+ }
1491
+
1492
+ const node = SVGNode('path', {
1493
+ d, class: classes,
1494
+ });
1495
+
1496
+ if (title) {
1497
+ node.addEventListener('mouseenter', (event) => {
1498
+ this.parent.setAttribute('title', title);
1499
+ });
1500
+ node.addEventListener('mouseleave', (event) => {
1501
+ this.parent.setAttribute('title', '');
1502
+ });
1503
+ }
1504
+
1505
+ this.group.appendChild(node);
1506
+
1507
+ if (label) {
1508
+
1509
+ this.label_group.appendChild(SVGNode('path', {class: 'label-target', d }));
1510
+
1511
+ const point = label_point || {
1512
+ x: Math.round(area.left + area.width / 2),
1513
+ y: Math.round(area.top - 10),
1514
+ };
1515
+
1516
+ const g = SVGNode('g', {class: 'data-label', transform: `translate(${point.x},${point.y})`});
1517
+ this.label_group.appendChild(g);
1518
+
1519
+
1520
+ const text = SVGNode('text', {x: 0, y: 0}, label);
1521
+ g.appendChild(text);
1522
+ const bounds = text.getBoundingClientRect();
1523
+ const h = bounds.height;
1524
+ const w = bounds.width + 8;
1525
+
1526
+ if (point.y - bounds.height < 4) {
1527
+ point.y -= (point.y - bounds.height - 4);
1528
+ g.setAttribute('transform', `translate(${point.x},${point.y})`);
1529
+ }
1530
+
1531
+ text.setAttribute('x', Math.floor(-bounds.width/2).toString());
1532
+
1533
+ /*
1534
+ if (w + 15 + point.x >= area.right) {
1535
+ g.setAttribute('transform', `translate(${point.x - w - 15},${point.y})`)
1536
+ // circle.setAttribute('cx', (w + 15).toString());
1537
+ }
1538
+ */
1539
+
1540
+ const vertical_padding = Math.ceil(h * .125);
1541
+
1542
+ // const rect = SVGNode('path', {d:`M${-w/2},${vertical_padding} h${w} v-${h + vertical_padding / 2} h-${w} Z`});
1543
+ const rect = SVGNode('rect', {rx: 3, x: -w/2, y: Math.round(-h + vertical_padding * 2/3), width: w, height: h + vertical_padding});
1544
+ g.insertBefore(rect, text);
1545
+
1546
+ }
1547
+
1548
+ }
1549
+
1550
+ /**
1551
+ * render text at point
1552
+ */
1553
+ public RenderText(
1554
+ target: SVGElement|undefined,
1555
+ text: string,
1556
+ align: 'center' | 'left' | 'right',
1557
+ point: Point,
1558
+ classes?: string | string[]): void {
1559
+
1560
+ const node = SVGNode('text', {x: point.x, y: point.y, class: classes}, text);
1561
+
1562
+ switch (align) {
1563
+ case 'right':
1564
+ node.style.textAnchor = 'end';
1565
+ break;
1566
+
1567
+ case 'center':
1568
+ node.style.textAnchor = 'middle';
1569
+ break;
1570
+
1571
+ default:
1572
+ node.style.textAnchor = 'start';
1573
+ break;
1574
+ }
1575
+
1576
+ (target||this.group).appendChild(node);
1577
+
1578
+ }
1579
+
1580
+ /**
1581
+ * render a donut, given a list of slices (as %)
1582
+ * @param values
1583
+ */
1584
+ public RenderDonut(
1585
+ slices: DonutSlice[],
1586
+ center: Point,
1587
+ outer_radius: number,
1588
+ inner_radius: number,
1589
+ bounds_area: Area,
1590
+ callouts: boolean,
1591
+ classes?: string | string[]): void {
1592
+
1593
+ let start_angle = -Math.PI / 2; // start at 12:00
1594
+ let end_angle = 0;
1595
+
1596
+ if (callouts) {
1597
+ outer_radius *= .8;
1598
+ inner_radius *= .7;
1599
+ }
1600
+
1601
+ const PointOnCircle = (center: Point, radius: number, angle: number) => {
1602
+ return [
1603
+ Math.cos(angle) * radius + center.x,
1604
+ Math.sin(angle) * radius + center.y,
1605
+ ];
1606
+ };
1607
+
1608
+ for (const slice of slices) {
1609
+
1610
+ const title = slice.title || '';
1611
+
1612
+ const value = slice.percent;
1613
+ const index = slice.index;
1614
+
1615
+ let d: string[] = [];
1616
+
1617
+ let half_angle = 0;
1618
+
1619
+ const outer = PointOnCircle.bind(0, center, outer_radius);
1620
+ const inner = PointOnCircle.bind(0, center, inner_radius);
1621
+
1622
+ if (value > 0.5) {
1623
+ // split into two segments
1624
+
1625
+ half_angle = start_angle + (value / 2) * Math.PI * 2;
1626
+ end_angle = start_angle + value * Math.PI * 2;
1627
+
1628
+ const delta1 = half_angle - start_angle;
1629
+ const delta2 = end_angle - half_angle;
1630
+
1631
+ d.push(
1632
+ `M${outer(start_angle)}`,
1633
+ `A${outer_radius},${outer_radius},${delta1},0,1,${outer(half_angle)}`,
1634
+ `A${outer_radius},${outer_radius},${delta2},0,1,${outer(end_angle)}`,
1635
+ `L${inner(end_angle)}`,
1636
+ `A${inner_radius},${inner_radius},${delta2},0,0,${inner(half_angle)}`,
1637
+ `A${inner_radius},${inner_radius},${delta1},0,0,${inner(start_angle)}`,
1638
+ 'Z');
1639
+
1640
+ }
1641
+ else {
1642
+
1643
+ end_angle = start_angle + value * Math.PI * 2;
1644
+ half_angle = (end_angle - start_angle) / 2 + start_angle;
1645
+
1646
+ const delta = end_angle - start_angle;
1647
+ d.push(
1648
+ `M${outer(start_angle)}`,
1649
+ `A${outer_radius},${outer_radius},${delta},0,1,${outer(end_angle)}`,
1650
+ `L${inner(end_angle)}`,
1651
+ `A${inner_radius},${inner_radius},${delta},0,0,${inner(start_angle)}`,
1652
+ 'Z');
1653
+
1654
+ }
1655
+
1656
+ const node = SVGNode('path', {
1657
+ d, class: (typeof index === 'undefined' ? undefined : `series-${index}`)
1658
+ });
1659
+
1660
+ if (typeof index !== 'undefined') {
1661
+ node.setAttribute('data-index', index.toString());
1662
+ }
1663
+
1664
+ /*
1665
+ if (title) {
1666
+ node.addEventListener('mouseenter', (event) => {
1667
+ this.parent.setAttribute('title', title);
1668
+ });
1669
+ node.addEventListener('mouseleave', (event) => {
1670
+ this.parent.setAttribute('title', '');
1671
+ });
1672
+ }
1673
+ */
1674
+
1675
+ // we're creating a containing group so that we can nth-child the slices,
1676
+ // otherwise they'll be in the same group as the title
1677
+
1678
+ const donut = SVGNode('g', {class: classes});
1679
+
1680
+ donut.appendChild(node);
1681
+ this.group.appendChild(donut);
1682
+
1683
+ if (/*callouts &&*/ value >= .05 && title) {
1684
+
1685
+ const length = outer_radius - inner_radius;
1686
+ d = [];
1687
+
1688
+ const anchor = PointOnCircle(center,
1689
+ inner_radius + (outer_radius - inner_radius) / 2 + length, half_angle);
1690
+
1691
+ d.push(`M${PointOnCircle(center, inner_radius + (outer_radius - inner_radius) / 2, half_angle)}`);
1692
+ d.push(`L${anchor}`);
1693
+
1694
+ /*
1695
+ const callout = document.createElementNS(SVGNS, 'path');
1696
+ callout.setAttribute('d', d.join(' '));
1697
+ callout.setAttribute('class', 'callout');
1698
+ donut.appendChild(callout);
1699
+ */
1700
+ donut.appendChild(SVGNode('path', { d, class: 'callout' }));
1701
+
1702
+ const text_parts: string[] = [];
1703
+ const callout_label = SVGNode('text', {class: 'callout-label'});
1704
+ donut.appendChild(callout_label);
1705
+
1706
+ const corrected = half_angle + Math.PI / 2;
1707
+ const text = title;
1708
+
1709
+ callout_label.textContent = text;
1710
+ let bounds = callout_label.getBoundingClientRect();
1711
+ const metrics = {
1712
+ width: bounds.width,
1713
+ height: bounds.height,
1714
+ };
1715
+
1716
+ // const metrics = this.MeasureText(text, ['donut', 'callout-label']);
1717
+
1718
+ let [x, y] = anchor;
1719
+
1720
+ x += metrics.height / 2 * Math.cos(half_angle);
1721
+ y += metrics.height / 4 + metrics.height / 2 * Math.sin(half_angle);
1722
+
1723
+ let try_break = false;
1724
+
1725
+ if (corrected > Math.PI) {
1726
+ if (x - metrics.width <= bounds_area.left) {
1727
+ try_break = true;
1728
+ }
1729
+ }
1730
+ else {
1731
+ if (x + metrics.width > bounds_area.right) {
1732
+ try_break = true;
1733
+ }
1734
+ }
1735
+
1736
+ // this breaks numbers, bad!
1737
+
1738
+ // const break_regex = /[\s-\W]/;
1739
+ const break_regex = /[\s-]/;
1740
+
1741
+ if (try_break && break_regex.test(text)) {
1742
+ let break_index = -1;
1743
+ let break_value = 1;
1744
+
1745
+ const indices: number[] = [];
1746
+ for (let i = 0; i < text.length; i++) {
1747
+ if (break_regex.test(text[i])) {
1748
+ const index_value = Math.abs(0.5 - (i / text.length));
1749
+ if (index_value < break_value) {
1750
+ break_value = index_value;
1751
+ break_index = i;
1752
+ }
1753
+ }
1754
+ }
1755
+
1756
+ if (break_index > 0) {
1757
+ text_parts.push(text.substr(0, break_index + 1).trim());
1758
+ text_parts.push(text.substr(break_index + 1).trim());
1759
+ }
1760
+ }
1761
+ else {
1762
+ // ... ellipsis?
1763
+ }
1764
+
1765
+ /*
1766
+ if (y <= bounds_area.top) {
1767
+ console.info("break top", title, y);
1768
+ }
1769
+ if (y >= bounds_area.bottom) {
1770
+ console.info("break bottom", title, y);
1771
+ }
1772
+ */
1773
+
1774
+ if (text_parts.length) {
1775
+ let dy = 0;
1776
+ let widest = 0;
1777
+
1778
+ const parts = text_parts.map((part) => {
1779
+ callout_label.textContent = part;
1780
+ bounds = callout_label.getBoundingClientRect();
1781
+ const m = {
1782
+ width: bounds.width,
1783
+ height: bounds.height,
1784
+ };
1785
+ //const m = this.MeasureText(part, ['donut', 'callout-label']);
1786
+ widest = Math.max(widest, m.width);
1787
+ return { text: part, metrics: m };
1788
+ });
1789
+
1790
+ // console.info('p', parts);
1791
+
1792
+ callout_label.textContent = '';
1793
+ for (const part of parts) {
1794
+ const tspan = document.createElementNS(SVGNS, 'tspan');
1795
+ tspan.textContent = part.text;
1796
+
1797
+ const part_x = (corrected > Math.PI) ?
1798
+ (x - (widest - part.metrics.width) / 2) :
1799
+ (x + (widest - part.metrics.width) / 2);
1800
+
1801
+ tspan.setAttribute('x', part_x.toString());
1802
+ tspan.setAttribute('dy', dy.toString());
1803
+
1804
+ callout_label.appendChild(tspan);
1805
+ dy = part.metrics.height;
1806
+
1807
+ }
1808
+ }
1809
+ else {
1810
+ // already in from measurement
1811
+ // callout_label.textContent = title;
1812
+ }
1813
+
1814
+ const text_anchor = corrected > Math.PI ? 'end' : 'start';
1815
+ callout_label.setAttribute('text-anchor', text_anchor);
1816
+ callout_label.setAttribute('x', x.toString());
1817
+ callout_label.setAttribute('y', y.toString());
1818
+
1819
+ if (typeof index !== 'undefined') {
1820
+ callout_label.setAttribute('data-index', index.toString());
1821
+ }
1822
+
1823
+
1824
+ }
1825
+
1826
+ start_angle = end_angle;
1827
+
1828
+ }
1829
+
1830
+
1831
+ }
1832
+
1833
+ /*
1834
+ protected PointOnCircle(angle: number, center: Point, radius: number) {
1835
+ return [
1836
+ Math.cos(angle) * radius + center.x,
1837
+ Math.sin(angle) * radius + center.y,
1838
+ ];
1839
+ }
1840
+ */
1841
+ }