@hpcc-js/chart 2.86.3 → 2.86.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +43 -43
  2. package/README.md +93 -93
  3. package/dist/index.es6.js +2 -2
  4. package/dist/index.es6.js.map +1 -1
  5. package/dist/index.js +2 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.min.js +1 -1
  8. package/dist/index.min.js.map +1 -1
  9. package/package.json +5 -5
  10. package/src/Area.md +176 -176
  11. package/src/Area.ts +12 -12
  12. package/src/Axis.css +34 -34
  13. package/src/Axis.ts +733 -733
  14. package/src/Bar.md +90 -90
  15. package/src/Bar.ts +9 -9
  16. package/src/Bubble.css +16 -16
  17. package/src/Bubble.md +69 -69
  18. package/src/Bubble.ts +191 -191
  19. package/src/BubbleXY.ts +14 -14
  20. package/src/Bullet.css +60 -60
  21. package/src/Bullet.md +104 -104
  22. package/src/Bullet.ts +167 -167
  23. package/src/Column.css +17 -17
  24. package/src/Column.md +90 -90
  25. package/src/Column.ts +659 -659
  26. package/src/Contour.md +88 -88
  27. package/src/Contour.ts +97 -97
  28. package/src/D3Cloud.ts +400 -400
  29. package/src/Gantt.md +119 -119
  30. package/src/Gantt.ts +14 -14
  31. package/src/Gauge.md +148 -148
  32. package/src/Gauge.ts +358 -358
  33. package/src/HalfPie.md +62 -62
  34. package/src/HalfPie.ts +26 -26
  35. package/src/Heat.md +42 -42
  36. package/src/Heat.ts +283 -283
  37. package/src/HexBin.css +9 -9
  38. package/src/HexBin.md +88 -88
  39. package/src/HexBin.ts +139 -139
  40. package/src/Line.css +6 -6
  41. package/src/Line.md +170 -170
  42. package/src/Line.ts +14 -14
  43. package/src/Pie.css +23 -23
  44. package/src/Pie.md +88 -88
  45. package/src/Pie.ts +503 -503
  46. package/src/QuarterPie.md +61 -61
  47. package/src/QuarterPie.ts +35 -35
  48. package/src/QuartileCandlestick.md +129 -129
  49. package/src/QuartileCandlestick.ts +349 -349
  50. package/src/Radar.css +15 -15
  51. package/src/Radar.md +104 -104
  52. package/src/Radar.ts +336 -336
  53. package/src/RadialBar.css +25 -25
  54. package/src/RadialBar.md +91 -91
  55. package/src/RadialBar.ts +212 -212
  56. package/src/Scatter.css +16 -16
  57. package/src/Scatter.md +163 -163
  58. package/src/Scatter.ts +376 -376
  59. package/src/StatChart.md +117 -117
  60. package/src/StatChart.ts +253 -253
  61. package/src/Step.md +163 -163
  62. package/src/Step.ts +12 -12
  63. package/src/Summary.css +56 -56
  64. package/src/Summary.md +219 -219
  65. package/src/Summary.ts +322 -322
  66. package/src/SummaryC.md +154 -154
  67. package/src/SummaryC.ts +240 -240
  68. package/src/WordCloud.css +3 -3
  69. package/src/WordCloud.md +144 -144
  70. package/src/WordCloud.ts +263 -263
  71. package/src/XYAxis.css +41 -41
  72. package/src/XYAxis.md +149 -149
  73. package/src/XYAxis.ts +803 -803
  74. package/src/__package__.ts +3 -3
  75. package/src/__tests__/heat.ts +71 -71
  76. package/src/__tests__/index.ts +3 -3
  77. package/src/__tests__/pie.ts +20 -20
  78. package/src/__tests__/stat.ts +16 -16
  79. package/src/__tests__/test3.ts +69 -69
  80. package/src/index.ts +27 -27
  81. package/src/test.ts +71 -71
  82. package/types/__package__.d.ts +2 -2
  83. package/types/__package__.d.ts.map +1 -1
  84. package/types-3.4/__package__.d.ts +2 -2
package/src/Pie.ts CHANGED
@@ -1,503 +1,503 @@
1
- import { I2DChart, ITooltip } from "@hpcc-js/api";
2
- import { InputField, SVGWidget, Utility } from "@hpcc-js/common";
3
- import { degreesToRadians, normalizeRadians } from "@hpcc-js/util";
4
- import { format as d3Format } from "d3-format";
5
- import { interpolate as d3Interpolate } from "d3-interpolate";
6
- import { select as d3Select } from "d3-selection";
7
- import { arc as d3Arc, pie as d3Pie } from "d3-shape";
8
-
9
- import "../src/Pie.css";
10
-
11
- const sortAscending = (a, b) => a[1] - b[1] > 0 ? 1 : -1;
12
- const sortDescending = (a, b) => a[1] - b[1] > 0 ? -1 : 1;
13
-
14
- export class Pie extends SVGWidget {
15
- static __inputs: InputField[] = [{
16
- id: "label",
17
- type: "string"
18
- }, {
19
- id: "value",
20
- type: "number"
21
- }];
22
-
23
- protected _totalValue: number;
24
-
25
- d3Pie;
26
- d3Arc;
27
- d3LabelArc;
28
- private _labelPositions;
29
- private _smallValueLabelHeight;
30
- private _labelWidthLimit: number;
31
- private _quadIdxArr;
32
- private _minLabelTop = 0;
33
- private _maxLabelBottom = 0;
34
- private _seriesValueFormatter;
35
- private _seriesPercentageFormatter;
36
- constructor() {
37
- super();
38
- I2DChart.call(this);
39
- ITooltip.call(this);
40
- Utility.SimpleSelectionMixin.call(this);
41
-
42
- this.d3Pie = d3Pie();
43
-
44
- this.d3Arc = d3Arc();
45
- this.d3LabelArc = d3Arc();
46
- this
47
- .tooltipTick_default(false)
48
- .tooltipOffset_default(0)
49
- ;
50
- }
51
-
52
- intersection(pointA, pointB) {
53
- return this.intersectCircle(this.calcOuterRadius(), pointA, pointB);
54
- }
55
-
56
- calcInnerRadius() {
57
- return this.innerRadius_exists() ? this.calcOuterRadius() * (this.innerRadius() as number) / 100 : 0;
58
- }
59
-
60
- calcOuterRadius() {
61
- const maxTextWidth = this.textSize(this.data().map(d => this.getLabelText({ data: d }, false)), "Verdana", 12).width;
62
- const horizontalLimit = this._size.width - (this.showLabels() ? maxTextWidth * 2 : 0) - 20;
63
- const verticalLimit = this._size.height - 12 * 3 - (this.showLabels() ? this._smallValueLabelHeight : 0);
64
- const outerRadius = Math.min(horizontalLimit, verticalLimit) / 2 - 2;
65
- if ((horizontalLimit / 2) - 2 < this.minOuterRadius()) {
66
- this._labelWidthLimit = maxTextWidth - (this.minOuterRadius() - ((horizontalLimit / 2) - 2));
67
- } else {
68
- this._labelWidthLimit = maxTextWidth;
69
- }
70
- if (outerRadius < this.minOuterRadius()) {
71
- return this.minOuterRadius();
72
- }
73
- return outerRadius;
74
- }
75
-
76
- calcSmallValueLabelHeight() {
77
- const smallDef = 0.1;
78
- const totalVal = this.data().reduce((acc, n) => acc + n[1], 0);
79
- let smallCount = 0;
80
- this.data().forEach(row => {
81
- if (row[1] / totalVal < smallDef) {
82
- smallCount++;
83
- }
84
- });
85
- return this.labelHeight() * smallCount;
86
- }
87
-
88
- calcTotalValue(): number {
89
- return this.data().reduce((acc, d) => {
90
- return acc + d[1];
91
- }, 0);
92
- }
93
-
94
- getLabelText(d, truncate?) {
95
- let len;
96
- let label = d.data[0];
97
- if (typeof this._labelWidthLimit !== "undefined" && truncate) {
98
- const labelWidth = this.textSize(label, "Verdana", this.labelHeight()).width;
99
- if (this._labelWidthLimit < labelWidth) {
100
- len = label.length * (this._labelWidthLimit / labelWidth) - 3;
101
- label = len < label.length ? label.slice(0, len) + "..." : label;
102
- }
103
- }
104
- if (this.showSeriesValue()) {
105
- label += ` : ${this._seriesValueFormatter(d.data[1])}`;
106
- }
107
- if (this.showSeriesPercentage()) {
108
- let sum = this._totalValue;
109
- const dm = this.dataMeta();
110
- if (typeof dm.sum !== "undefined") {
111
- sum = dm.sum;
112
- }
113
- const perc = (d.data[1] / sum) * 100;
114
- label += ` : ${this._seriesPercentageFormatter(perc)}%`;
115
- }
116
- return label;
117
- }
118
-
119
- selection(): any[]; // any[] === single row
120
- selection(_: any[]): this;
121
- selection(_?: any[]): any[] | this {
122
- if (!arguments.length) {
123
- try {
124
- return this._selection.selection2()[0]?.data;
125
- } catch (e) {
126
- return undefined;
127
- }
128
- }
129
- // Stringify to enable a deep equal
130
- const strRow = JSON.stringify(_);
131
- this._selection.selection2(d => strRow === JSON.stringify(d.data));
132
- }
133
-
134
- selectByLabel(_: string) {
135
- const row = this.data().filter(row => row[0] === _)[0];
136
- if (row) {
137
- this.selection(row);
138
- }
139
- }
140
-
141
- _slices;
142
- _labels;
143
- enter(_domNode, element) {
144
- super.enter(_domNode, element);
145
- this._selection.widgetElement(element);
146
- this._slices = element.append("g");
147
- this._labels = element.append("g");
148
- const context = this;
149
- this
150
- .tooltipHTML(function (d) {
151
- switch (context.tooltipStyle()) {
152
- case "series-table":
153
- return context.tooltipFormat({
154
- label: d.data[0],
155
- arr: context.columns().slice(1).map(function (column, i) {
156
- return {
157
- label: column,
158
- color: context._palette(d.data[0]),
159
- value: d.data[i + 1]
160
- };
161
- })
162
- });
163
- default:
164
- return context.tooltipFormat({ label: d.data[0], value: d.data[1] });
165
- }
166
- })
167
- ;
168
- }
169
-
170
- update(_domNode, element) {
171
- super.update(_domNode, element);
172
- const context = this;
173
- this.updateD3Pie();
174
- this._palette = this._palette.switch(this.paletteID());
175
- this._seriesValueFormatter = d3Format(this.seriesValueFormat() as string);
176
- this._seriesPercentageFormatter = d3Format(this.seriesPercentageFormat() as string);
177
- if (this.useClonedPalette()) {
178
- this._palette = this._palette.cloneNotExists(this.paletteID() + "_" + this.id());
179
- }
180
- this._smallValueLabelHeight = this.calcSmallValueLabelHeight();
181
- this._totalValue = this.calcTotalValue();
182
- const innerRadius = this.calcInnerRadius();
183
- const outerRadius = this.calcOuterRadius();
184
- const labelRadius = outerRadius + 12;
185
- this.d3Arc
186
- .innerRadius(innerRadius)
187
- .padRadius(outerRadius)
188
- .outerRadius(outerRadius)
189
- ;
190
-
191
- this._quadIdxArr = [[], [], [], []];
192
- const data = [...this.data()];
193
- switch (this.sortDataByValue()) {
194
- case "ascending":
195
- data.sort(sortAscending);
196
- break;
197
- case "descending":
198
- data.sort(sortDescending);
199
- break;
200
- }
201
- const arc = this._slices.selectAll(".arc").data(this.d3Pie(data), d => d.data[0]);
202
-
203
- this._labelPositions = [];
204
-
205
- // Enter ---
206
- arc.enter().append("g")
207
- .attr("class", (d, i) => "arc series series-" + this.cssTag(d.data[0]))
208
- .attr("opacity", 0)
209
- .call(this._selection.enter.bind(this._selection))
210
- .on("click", function (d) {
211
- context.click(context.rowToObj(d.data), context.columns()[1], context._selection.selected(this));
212
- })
213
- .on("dblclick", function (d) {
214
- context.dblclick(context.rowToObj(d.data), context.columns()[1], context._selection.selected(this));
215
- })
216
- .each(function (d, i) {
217
- d3Select(this).append("path")
218
- .on("mouseout.tooltip", context.tooltip.hide)
219
- .on("mousemove.tooltip", context.tooltip.show)
220
- .on("mouseover", arcTween(0, 0))
221
- .on("mouseout", arcTween(-5, 150))
222
- ;
223
- })
224
- .merge(arc).transition()
225
- .attr("opacity", 1)
226
- .each(function (d, i) {
227
- const quad = context.getQuadrant(midAngle(d));
228
- context._quadIdxArr[quad].push(i);
229
- d.outerRadius = outerRadius - 5;
230
- const element2 = d3Select(this);
231
- element2.select("path").transition()
232
- .attr("d", context.d3Arc)
233
- .style("fill", context.fillColor(d.data, context.columns()[1], d.data[1]))
234
- ;
235
- })
236
- ;
237
-
238
- // Exit ---
239
- arc.exit().transition()
240
- .style("opacity", 0)
241
- .remove()
242
- ;
243
- // Labels ---
244
- this.d3LabelArc
245
- .innerRadius(labelRadius)
246
- .outerRadius(labelRadius)
247
- ;
248
- const text = this._labels.selectAll("text").data(this.showLabels() ? this.d3Pie(data) : [], d => d.data[0]);
249
-
250
- const mergedText = text.enter().append("text")
251
- .on("mouseout.tooltip", context.tooltip.hide)
252
- .on("mousemove.tooltip", context.tooltip.show)
253
- .attr("dy", ".5em")
254
- .on("click", function (d) {
255
- context._slices.selectAll("g").filter(function (d2) {
256
- if (d.data === d2.data) {
257
- context._selection.click(this);
258
- context.click(context.rowToObj(d.data), context.columns()[1], context._selection.selected(this));
259
- }
260
- });
261
- })
262
- .on("dblclick", function (d) {
263
- context._slices.selectAll("g").filter(function (d2) {
264
- if (d.data === d2.data) {
265
- context.dblclick(context.rowToObj(d.data), context.columns()[1], context._selection.selected(this));
266
- }
267
- });
268
- })
269
- .merge(text)
270
- .text(d => this.getLabelText(d, true))
271
- .each(function (d, i) {
272
- const pos = context.d3LabelArc.centroid(d);
273
- const mid_angle = midAngle(d);
274
- pos[0] = labelRadius * (context.isLeftSide(mid_angle) ? 1 : -1);
275
- context._labelPositions.push({
276
- top: pos[1],
277
- bottom: pos[1] + context.labelHeight()
278
- });
279
- });
280
- if (this.showLabels()) {
281
- this.adjustForOverlap();
282
- mergedText.transition()
283
- .style("font-size", this.labelHeight() + "px")
284
- .attr("transform", (d, i) => {
285
- const pos = context.d3LabelArc.centroid(d);
286
- pos[0] = labelRadius * (context.isLeftSide(midAngle(d)) ? 1 : -1);
287
- pos[1] = context._labelPositions[i].top;
288
- return "translate(" + pos + ")";
289
- })
290
- .style("text-anchor", d => this.isLeftSide(midAngle(d)) ? "start" : "end");
291
- }
292
-
293
- text.exit()
294
- .remove();
295
-
296
- const polyline = this._labels.selectAll("polyline").data(this.showLabels() ? this.d3Pie(data) : [], d => this.getLabelText(d, true));
297
-
298
- polyline.enter()
299
- .append("polyline")
300
- .merge(polyline).transition()
301
- .attr("points", function (d, i) {
302
- const pos = context.d3LabelArc.centroid(d);
303
- const pos1 = context.d3Arc.centroid(d);
304
- const pos2 = [...pos];
305
- pos[0] = labelRadius * (context.isLeftSide(midAngle(d)) ? 1 : -1);
306
- pos[1] = context._labelPositions[i].top;
307
- return [pos1, pos2, pos];
308
- });
309
-
310
- polyline.exit()
311
- .remove();
312
-
313
- if (this.showLabels()) {
314
- this.centerOnLabels();
315
- }
316
- function midAngle(d) {
317
- return d.startAngle + (d.endAngle - d.startAngle) / 2;
318
- }
319
-
320
- function arcTween(outerRadiusDelta, delay) {
321
- return function () {
322
- d3Select(this).transition().delay(delay).attrTween("d", function (d: any) {
323
- const i = d3Interpolate(d.outerRadius, outerRadius + outerRadiusDelta);
324
- return function (t) { d.outerRadius = i(t); return context.d3Arc(d); };
325
- });
326
- };
327
- }
328
- }
329
-
330
- isLeftSide(midAngle) {
331
- midAngle = normalizeRadians(midAngle);
332
- const isLeft = midAngle > Math.PI * 2 ? midAngle : midAngle < Math.PI && midAngle > 0;
333
- return isLeft;
334
- }
335
-
336
- getQuadrant(radians) {
337
- let quad = 0;
338
- const rad = normalizeRadians(radians);
339
- quad = rad <= Math.PI * 1.0 && rad >= Math.PI * 0.5 ? 3 : quad;
340
- quad = rad <= Math.PI * 0.5 && rad >= Math.PI * 0.0 ? 2 : quad;
341
- quad = rad <= Math.PI * 0.0 && rad >= Math.PI * -0.5 ? 1 : quad;
342
- return quad;
343
- }
344
-
345
- centerOnLabels() {
346
- const gY = this.pos().y;
347
- const gY2 = gY * 2;
348
- const radius = this.calcOuterRadius();
349
- const top = Math.min(this._minLabelTop, -radius);
350
- const bottom = Math.max(this._maxLabelBottom, radius);
351
- const h = bottom - top;
352
- const heightDiff = gY2 - h;
353
- const absTop = Math.abs(this._minLabelTop);
354
- let yShift = 0;
355
- if (bottom > gY) {
356
- yShift = gY - bottom + (this.labelHeight() / 2);
357
- yShift -= heightDiff / 2;
358
- } else if (top < 0 && absTop > gY) {
359
- yShift = absTop - gY + (this.labelHeight() / 2);
360
- yShift += heightDiff / 2;
361
- }
362
- const pos = this.pos();
363
- this.pos({
364
- y: pos.y + yShift,
365
- x: pos.x
366
- });
367
- }
368
-
369
- adjustForOverlap() {
370
- const labelHeight = this.labelHeight();
371
- this._quadIdxArr.forEach((arr, quad) => {
372
- this._quadIdxArr[quad].sort((a, b) => {
373
- if (quad === 1 || quad === 2) {
374
- return this._labelPositions[a].top > this._labelPositions[b].top ? -1 : 1;
375
- } else if (quad === 0 || quad === 3) {
376
- return this._labelPositions[a].top > this._labelPositions[b].top ? 1 : -1;
377
- }
378
- });
379
- let prevTop;
380
- this._quadIdxArr[quad].forEach((n, i) => {
381
- if (i > 0) {
382
- if (quad === 1 || quad === 2) {
383
- if (prevTop < this._labelPositions[n].bottom) {
384
- const overlap = this._labelPositions[n].bottom - prevTop;
385
- this._labelPositions[n].top -= overlap;
386
- this._labelPositions[n].bottom -= overlap;
387
- }
388
- } else if (quad === 0 || quad === 3) {
389
- if (prevTop + labelHeight > this._labelPositions[n].top) {
390
- const overlap = Math.abs(this._labelPositions[n].top) - Math.abs(prevTop + labelHeight);
391
- this._labelPositions[n].top -= overlap;
392
- this._labelPositions[n].bottom -= overlap;
393
- }
394
- }
395
- }
396
- prevTop = this._labelPositions[n].top;
397
- });
398
- });
399
- this._minLabelTop = 0;
400
- this._maxLabelBottom = 0;
401
- this._quadIdxArr.forEach((arr, quad) => {
402
- this._quadIdxArr[quad].forEach((n, i) => {
403
- if (this._minLabelTop > this._labelPositions[n].top) {
404
- this._minLabelTop = this._labelPositions[n].top;
405
- }
406
- if (this._maxLabelBottom < this._labelPositions[n].bottom) {
407
- this._maxLabelBottom = this._labelPositions[n].bottom;
408
- }
409
- });
410
- });
411
- }
412
-
413
- exit(domNode, element) {
414
- super.exit(domNode, element);
415
- }
416
-
417
- updateD3Pie() {
418
- const startAngle = normalizeRadians(degreesToRadians(this.startAngle()));
419
-
420
- switch (this.sortDataByValue()) {
421
- case "ascending":
422
- this.d3Pie.sort(sortAscending);
423
- break;
424
- case "descending":
425
- this.d3Pie.sort(sortDescending);
426
- break;
427
- default:
428
- this.d3Pie.sort(null);
429
- }
430
-
431
- this.d3Pie
432
- .padAngle(0.0025)
433
- .startAngle(startAngle)
434
- .endAngle(2 * Math.PI + startAngle)
435
- .value(function (d) {
436
- return d[1];
437
- })
438
- ;
439
- }
440
-
441
- paletteID: (_?: string) => string | Pie;
442
- useClonedPalette: (_?: boolean) => boolean | Pie;
443
- outerText: (_?: boolean) => boolean | Pie;
444
- innerRadius: { (): number; (_: number): Pie; };
445
- innerRadius_exists: () => boolean;
446
-
447
- // I2DChart
448
- _palette;
449
- fillColor: (row: any[], column: string, value: number) => string;
450
- textColor: (row: any[], column: string, value: number) => string;
451
- click: (row, column, selected) => void;
452
- dblclick: (row, column, selected) => void;
453
-
454
- // ITooltip
455
- tooltip;
456
- tooltipHTML: (_) => string;
457
- tooltipFormat: (_) => string;
458
- tooltipStyle: () => "default" | "none" | "series-table";
459
- tooltipTick: { (): boolean; (_: boolean): Pie; };
460
- tooltipTick_default: { (): boolean; (_: boolean): Pie; };
461
- tooltipOffset: { (): number; (_: number): Pie; };
462
- tooltipOffset_default: { (): number; (_: number): Pie; };
463
-
464
- // SimpleSelectionMixin
465
- _selection: Utility.SimpleSelection;
466
- }
467
-
468
- Pie.prototype._class += " chart_Pie";
469
- Pie.prototype.implements(I2DChart.prototype);
470
- Pie.prototype.implements(ITooltip.prototype);
471
- Pie.prototype.mixin(Utility.SimpleSelectionMixin);
472
- export interface Pie {
473
- showSeriesValue(): boolean;
474
- showSeriesValue(_: boolean): this;
475
- seriesValueFormat(): string;
476
- seriesValueFormat(_: string): this;
477
- showSeriesPercentage(): boolean;
478
- showSeriesPercentage(_: boolean): this;
479
- minOuterRadius(): number;
480
- minOuterRadius(_: number): this;
481
- startAngle(): number;
482
- startAngle(_: number): this;
483
- labelHeight(): number;
484
- labelHeight(_: number): this;
485
- seriesPercentageFormat(): string;
486
- seriesPercentageFormat(_: string): this;
487
- showLabels(): boolean;
488
- showLabels(_: boolean): this;
489
- sortDataByValue(): "none" | "ascending" | "descending";
490
- sortDataByValue(_: "none" | "ascending" | "descending"): this;
491
- }
492
- Pie.prototype.publish("showLabels", true, "boolean", "If true, wedge labels will display");
493
- Pie.prototype.publish("showSeriesValue", false, "boolean", "Append data series value next to label", null, { disable: w => !w.showLabels() });
494
- Pie.prototype.publish("seriesValueFormat", ",.0f", "string", "Number format used for formatting series values", null, { disable: w => !w.showSeriesValue() });
495
- Pie.prototype.publish("showSeriesPercentage", false, "boolean", "Append data series percentage next to label", null, { disable: w => !w.showLabels() });
496
- Pie.prototype.publish("seriesPercentageFormat", ",.0f", "string", "Number format used for formatting series percentages", null, { disable: w => !w.showSeriesPercentage() });
497
- Pie.prototype.publish("paletteID", "default", "set", "Color palette for this widget", Pie.prototype._palette.switch(), { tags: ["Basic", "Shared"] });
498
- Pie.prototype.publish("useClonedPalette", false, "boolean", "Enable or disable using a cloned palette", null, { tags: ["Intermediate", "Shared"] });
499
- Pie.prototype.publish("innerRadius", 0, "number", "Sets inner pie hole radius as a percentage of the radius of the pie chart", null, { tags: ["Basic"], range: { min: 0, step: 1, max: 100 } });
500
- Pie.prototype.publish("minOuterRadius", 20, "number", "Minimum outer radius (pixels)");
501
- Pie.prototype.publish("startAngle", 0, "number", "Starting angle of the first (and largest) wedge (degrees)");
502
- Pie.prototype.publish("labelHeight", 12, "number", "Font size of labels (pixels)", null, { disable: w => !w.showLabels() });
503
- Pie.prototype.publish("sortDataByValue", "descending", "set", "Sort data by value", ["none", "ascending", "descending"]);
1
+ import { I2DChart, ITooltip } from "@hpcc-js/api";
2
+ import { InputField, SVGWidget, Utility } from "@hpcc-js/common";
3
+ import { degreesToRadians, normalizeRadians } from "@hpcc-js/util";
4
+ import { format as d3Format } from "d3-format";
5
+ import { interpolate as d3Interpolate } from "d3-interpolate";
6
+ import { select as d3Select } from "d3-selection";
7
+ import { arc as d3Arc, pie as d3Pie } from "d3-shape";
8
+
9
+ import "../src/Pie.css";
10
+
11
+ const sortAscending = (a, b) => a[1] - b[1] > 0 ? 1 : -1;
12
+ const sortDescending = (a, b) => a[1] - b[1] > 0 ? -1 : 1;
13
+
14
+ export class Pie extends SVGWidget {
15
+ static __inputs: InputField[] = [{
16
+ id: "label",
17
+ type: "string"
18
+ }, {
19
+ id: "value",
20
+ type: "number"
21
+ }];
22
+
23
+ protected _totalValue: number;
24
+
25
+ d3Pie;
26
+ d3Arc;
27
+ d3LabelArc;
28
+ private _labelPositions;
29
+ private _smallValueLabelHeight;
30
+ private _labelWidthLimit: number;
31
+ private _quadIdxArr;
32
+ private _minLabelTop = 0;
33
+ private _maxLabelBottom = 0;
34
+ private _seriesValueFormatter;
35
+ private _seriesPercentageFormatter;
36
+ constructor() {
37
+ super();
38
+ I2DChart.call(this);
39
+ ITooltip.call(this);
40
+ Utility.SimpleSelectionMixin.call(this);
41
+
42
+ this.d3Pie = d3Pie();
43
+
44
+ this.d3Arc = d3Arc();
45
+ this.d3LabelArc = d3Arc();
46
+ this
47
+ .tooltipTick_default(false)
48
+ .tooltipOffset_default(0)
49
+ ;
50
+ }
51
+
52
+ intersection(pointA, pointB) {
53
+ return this.intersectCircle(this.calcOuterRadius(), pointA, pointB);
54
+ }
55
+
56
+ calcInnerRadius() {
57
+ return this.innerRadius_exists() ? this.calcOuterRadius() * (this.innerRadius() as number) / 100 : 0;
58
+ }
59
+
60
+ calcOuterRadius() {
61
+ const maxTextWidth = this.textSize(this.data().map(d => this.getLabelText({ data: d }, false)), "Verdana", 12).width;
62
+ const horizontalLimit = this._size.width - (this.showLabels() ? maxTextWidth * 2 : 0) - 20;
63
+ const verticalLimit = this._size.height - 12 * 3 - (this.showLabels() ? this._smallValueLabelHeight : 0);
64
+ const outerRadius = Math.min(horizontalLimit, verticalLimit) / 2 - 2;
65
+ if ((horizontalLimit / 2) - 2 < this.minOuterRadius()) {
66
+ this._labelWidthLimit = maxTextWidth - (this.minOuterRadius() - ((horizontalLimit / 2) - 2));
67
+ } else {
68
+ this._labelWidthLimit = maxTextWidth;
69
+ }
70
+ if (outerRadius < this.minOuterRadius()) {
71
+ return this.minOuterRadius();
72
+ }
73
+ return outerRadius;
74
+ }
75
+
76
+ calcSmallValueLabelHeight() {
77
+ const smallDef = 0.1;
78
+ const totalVal = this.data().reduce((acc, n) => acc + n[1], 0);
79
+ let smallCount = 0;
80
+ this.data().forEach(row => {
81
+ if (row[1] / totalVal < smallDef) {
82
+ smallCount++;
83
+ }
84
+ });
85
+ return this.labelHeight() * smallCount;
86
+ }
87
+
88
+ calcTotalValue(): number {
89
+ return this.data().reduce((acc, d) => {
90
+ return acc + d[1];
91
+ }, 0);
92
+ }
93
+
94
+ getLabelText(d, truncate?) {
95
+ let len;
96
+ let label = d.data[0];
97
+ if (typeof this._labelWidthLimit !== "undefined" && truncate) {
98
+ const labelWidth = this.textSize(label, "Verdana", this.labelHeight()).width;
99
+ if (this._labelWidthLimit < labelWidth) {
100
+ len = label.length * (this._labelWidthLimit / labelWidth) - 3;
101
+ label = len < label.length ? label.slice(0, len) + "..." : label;
102
+ }
103
+ }
104
+ if (this.showSeriesValue()) {
105
+ label += ` : ${this._seriesValueFormatter(d.data[1])}`;
106
+ }
107
+ if (this.showSeriesPercentage()) {
108
+ let sum = this._totalValue;
109
+ const dm = this.dataMeta();
110
+ if (typeof dm.sum !== "undefined") {
111
+ sum = dm.sum;
112
+ }
113
+ const perc = (d.data[1] / sum) * 100;
114
+ label += ` : ${this._seriesPercentageFormatter(perc)}%`;
115
+ }
116
+ return label;
117
+ }
118
+
119
+ selection(): any[]; // any[] === single row
120
+ selection(_: any[]): this;
121
+ selection(_?: any[]): any[] | this {
122
+ if (!arguments.length) {
123
+ try {
124
+ return this._selection.selection2()[0]?.data;
125
+ } catch (e) {
126
+ return undefined;
127
+ }
128
+ }
129
+ // Stringify to enable a deep equal
130
+ const strRow = JSON.stringify(_);
131
+ this._selection.selection2(d => strRow === JSON.stringify(d.data));
132
+ }
133
+
134
+ selectByLabel(_: string) {
135
+ const row = this.data().filter(row => row[0] === _)[0];
136
+ if (row) {
137
+ this.selection(row);
138
+ }
139
+ }
140
+
141
+ _slices;
142
+ _labels;
143
+ enter(_domNode, element) {
144
+ super.enter(_domNode, element);
145
+ this._selection.widgetElement(element);
146
+ this._slices = element.append("g");
147
+ this._labels = element.append("g");
148
+ const context = this;
149
+ this
150
+ .tooltipHTML(function (d) {
151
+ switch (context.tooltipStyle()) {
152
+ case "series-table":
153
+ return context.tooltipFormat({
154
+ label: d.data[0],
155
+ arr: context.columns().slice(1).map(function (column, i) {
156
+ return {
157
+ label: column,
158
+ color: context._palette(d.data[0]),
159
+ value: d.data[i + 1]
160
+ };
161
+ })
162
+ });
163
+ default:
164
+ return context.tooltipFormat({ label: d.data[0], value: d.data[1] });
165
+ }
166
+ })
167
+ ;
168
+ }
169
+
170
+ update(_domNode, element) {
171
+ super.update(_domNode, element);
172
+ const context = this;
173
+ this.updateD3Pie();
174
+ this._palette = this._palette.switch(this.paletteID());
175
+ this._seriesValueFormatter = d3Format(this.seriesValueFormat() as string);
176
+ this._seriesPercentageFormatter = d3Format(this.seriesPercentageFormat() as string);
177
+ if (this.useClonedPalette()) {
178
+ this._palette = this._palette.cloneNotExists(this.paletteID() + "_" + this.id());
179
+ }
180
+ this._smallValueLabelHeight = this.calcSmallValueLabelHeight();
181
+ this._totalValue = this.calcTotalValue();
182
+ const innerRadius = this.calcInnerRadius();
183
+ const outerRadius = this.calcOuterRadius();
184
+ const labelRadius = outerRadius + 12;
185
+ this.d3Arc
186
+ .innerRadius(innerRadius)
187
+ .padRadius(outerRadius)
188
+ .outerRadius(outerRadius)
189
+ ;
190
+
191
+ this._quadIdxArr = [[], [], [], []];
192
+ const data = [...this.data()];
193
+ switch (this.sortDataByValue()) {
194
+ case "ascending":
195
+ data.sort(sortAscending);
196
+ break;
197
+ case "descending":
198
+ data.sort(sortDescending);
199
+ break;
200
+ }
201
+ const arc = this._slices.selectAll(".arc").data(this.d3Pie(data), d => d.data[0]);
202
+
203
+ this._labelPositions = [];
204
+
205
+ // Enter ---
206
+ arc.enter().append("g")
207
+ .attr("class", (d, i) => "arc series series-" + this.cssTag(d.data[0]))
208
+ .attr("opacity", 0)
209
+ .call(this._selection.enter.bind(this._selection))
210
+ .on("click", function (d) {
211
+ context.click(context.rowToObj(d.data), context.columns()[1], context._selection.selected(this));
212
+ })
213
+ .on("dblclick", function (d) {
214
+ context.dblclick(context.rowToObj(d.data), context.columns()[1], context._selection.selected(this));
215
+ })
216
+ .each(function (d, i) {
217
+ d3Select(this).append("path")
218
+ .on("mouseout.tooltip", context.tooltip.hide)
219
+ .on("mousemove.tooltip", context.tooltip.show)
220
+ .on("mouseover", arcTween(0, 0))
221
+ .on("mouseout", arcTween(-5, 150))
222
+ ;
223
+ })
224
+ .merge(arc).transition()
225
+ .attr("opacity", 1)
226
+ .each(function (d, i) {
227
+ const quad = context.getQuadrant(midAngle(d));
228
+ context._quadIdxArr[quad].push(i);
229
+ d.outerRadius = outerRadius - 5;
230
+ const element2 = d3Select(this);
231
+ element2.select("path").transition()
232
+ .attr("d", context.d3Arc)
233
+ .style("fill", context.fillColor(d.data, context.columns()[1], d.data[1]))
234
+ ;
235
+ })
236
+ ;
237
+
238
+ // Exit ---
239
+ arc.exit().transition()
240
+ .style("opacity", 0)
241
+ .remove()
242
+ ;
243
+ // Labels ---
244
+ this.d3LabelArc
245
+ .innerRadius(labelRadius)
246
+ .outerRadius(labelRadius)
247
+ ;
248
+ const text = this._labels.selectAll("text").data(this.showLabels() ? this.d3Pie(data) : [], d => d.data[0]);
249
+
250
+ const mergedText = text.enter().append("text")
251
+ .on("mouseout.tooltip", context.tooltip.hide)
252
+ .on("mousemove.tooltip", context.tooltip.show)
253
+ .attr("dy", ".5em")
254
+ .on("click", function (d) {
255
+ context._slices.selectAll("g").filter(function (d2) {
256
+ if (d.data === d2.data) {
257
+ context._selection.click(this);
258
+ context.click(context.rowToObj(d.data), context.columns()[1], context._selection.selected(this));
259
+ }
260
+ });
261
+ })
262
+ .on("dblclick", function (d) {
263
+ context._slices.selectAll("g").filter(function (d2) {
264
+ if (d.data === d2.data) {
265
+ context.dblclick(context.rowToObj(d.data), context.columns()[1], context._selection.selected(this));
266
+ }
267
+ });
268
+ })
269
+ .merge(text)
270
+ .text(d => this.getLabelText(d, true))
271
+ .each(function (d, i) {
272
+ const pos = context.d3LabelArc.centroid(d);
273
+ const mid_angle = midAngle(d);
274
+ pos[0] = labelRadius * (context.isLeftSide(mid_angle) ? 1 : -1);
275
+ context._labelPositions.push({
276
+ top: pos[1],
277
+ bottom: pos[1] + context.labelHeight()
278
+ });
279
+ });
280
+ if (this.showLabels()) {
281
+ this.adjustForOverlap();
282
+ mergedText.transition()
283
+ .style("font-size", this.labelHeight() + "px")
284
+ .attr("transform", (d, i) => {
285
+ const pos = context.d3LabelArc.centroid(d);
286
+ pos[0] = labelRadius * (context.isLeftSide(midAngle(d)) ? 1 : -1);
287
+ pos[1] = context._labelPositions[i].top;
288
+ return "translate(" + pos + ")";
289
+ })
290
+ .style("text-anchor", d => this.isLeftSide(midAngle(d)) ? "start" : "end");
291
+ }
292
+
293
+ text.exit()
294
+ .remove();
295
+
296
+ const polyline = this._labels.selectAll("polyline").data(this.showLabels() ? this.d3Pie(data) : [], d => this.getLabelText(d, true));
297
+
298
+ polyline.enter()
299
+ .append("polyline")
300
+ .merge(polyline).transition()
301
+ .attr("points", function (d, i) {
302
+ const pos = context.d3LabelArc.centroid(d);
303
+ const pos1 = context.d3Arc.centroid(d);
304
+ const pos2 = [...pos];
305
+ pos[0] = labelRadius * (context.isLeftSide(midAngle(d)) ? 1 : -1);
306
+ pos[1] = context._labelPositions[i].top;
307
+ return [pos1, pos2, pos];
308
+ });
309
+
310
+ polyline.exit()
311
+ .remove();
312
+
313
+ if (this.showLabels()) {
314
+ this.centerOnLabels();
315
+ }
316
+ function midAngle(d) {
317
+ return d.startAngle + (d.endAngle - d.startAngle) / 2;
318
+ }
319
+
320
+ function arcTween(outerRadiusDelta, delay) {
321
+ return function () {
322
+ d3Select(this).transition().delay(delay).attrTween("d", function (d: any) {
323
+ const i = d3Interpolate(d.outerRadius, outerRadius + outerRadiusDelta);
324
+ return function (t) { d.outerRadius = i(t); return context.d3Arc(d); };
325
+ });
326
+ };
327
+ }
328
+ }
329
+
330
+ isLeftSide(midAngle) {
331
+ midAngle = normalizeRadians(midAngle);
332
+ const isLeft = midAngle > Math.PI * 2 ? midAngle : midAngle < Math.PI && midAngle > 0;
333
+ return isLeft;
334
+ }
335
+
336
+ getQuadrant(radians) {
337
+ let quad = 0;
338
+ const rad = normalizeRadians(radians);
339
+ quad = rad <= Math.PI * 1.0 && rad >= Math.PI * 0.5 ? 3 : quad;
340
+ quad = rad <= Math.PI * 0.5 && rad >= Math.PI * 0.0 ? 2 : quad;
341
+ quad = rad <= Math.PI * 0.0 && rad >= Math.PI * -0.5 ? 1 : quad;
342
+ return quad;
343
+ }
344
+
345
+ centerOnLabels() {
346
+ const gY = this.pos().y;
347
+ const gY2 = gY * 2;
348
+ const radius = this.calcOuterRadius();
349
+ const top = Math.min(this._minLabelTop, -radius);
350
+ const bottom = Math.max(this._maxLabelBottom, radius);
351
+ const h = bottom - top;
352
+ const heightDiff = gY2 - h;
353
+ const absTop = Math.abs(this._minLabelTop);
354
+ let yShift = 0;
355
+ if (bottom > gY) {
356
+ yShift = gY - bottom + (this.labelHeight() / 2);
357
+ yShift -= heightDiff / 2;
358
+ } else if (top < 0 && absTop > gY) {
359
+ yShift = absTop - gY + (this.labelHeight() / 2);
360
+ yShift += heightDiff / 2;
361
+ }
362
+ const pos = this.pos();
363
+ this.pos({
364
+ y: pos.y + yShift,
365
+ x: pos.x
366
+ });
367
+ }
368
+
369
+ adjustForOverlap() {
370
+ const labelHeight = this.labelHeight();
371
+ this._quadIdxArr.forEach((arr, quad) => {
372
+ this._quadIdxArr[quad].sort((a, b) => {
373
+ if (quad === 1 || quad === 2) {
374
+ return this._labelPositions[a].top > this._labelPositions[b].top ? -1 : 1;
375
+ } else if (quad === 0 || quad === 3) {
376
+ return this._labelPositions[a].top > this._labelPositions[b].top ? 1 : -1;
377
+ }
378
+ });
379
+ let prevTop;
380
+ this._quadIdxArr[quad].forEach((n, i) => {
381
+ if (i > 0) {
382
+ if (quad === 1 || quad === 2) {
383
+ if (prevTop < this._labelPositions[n].bottom) {
384
+ const overlap = this._labelPositions[n].bottom - prevTop;
385
+ this._labelPositions[n].top -= overlap;
386
+ this._labelPositions[n].bottom -= overlap;
387
+ }
388
+ } else if (quad === 0 || quad === 3) {
389
+ if (prevTop + labelHeight > this._labelPositions[n].top) {
390
+ const overlap = Math.abs(this._labelPositions[n].top) - Math.abs(prevTop + labelHeight);
391
+ this._labelPositions[n].top -= overlap;
392
+ this._labelPositions[n].bottom -= overlap;
393
+ }
394
+ }
395
+ }
396
+ prevTop = this._labelPositions[n].top;
397
+ });
398
+ });
399
+ this._minLabelTop = 0;
400
+ this._maxLabelBottom = 0;
401
+ this._quadIdxArr.forEach((arr, quad) => {
402
+ this._quadIdxArr[quad].forEach((n, i) => {
403
+ if (this._minLabelTop > this._labelPositions[n].top) {
404
+ this._minLabelTop = this._labelPositions[n].top;
405
+ }
406
+ if (this._maxLabelBottom < this._labelPositions[n].bottom) {
407
+ this._maxLabelBottom = this._labelPositions[n].bottom;
408
+ }
409
+ });
410
+ });
411
+ }
412
+
413
+ exit(domNode, element) {
414
+ super.exit(domNode, element);
415
+ }
416
+
417
+ updateD3Pie() {
418
+ const startAngle = normalizeRadians(degreesToRadians(this.startAngle()));
419
+
420
+ switch (this.sortDataByValue()) {
421
+ case "ascending":
422
+ this.d3Pie.sort(sortAscending);
423
+ break;
424
+ case "descending":
425
+ this.d3Pie.sort(sortDescending);
426
+ break;
427
+ default:
428
+ this.d3Pie.sort(null);
429
+ }
430
+
431
+ this.d3Pie
432
+ .padAngle(0.0025)
433
+ .startAngle(startAngle)
434
+ .endAngle(2 * Math.PI + startAngle)
435
+ .value(function (d) {
436
+ return d[1];
437
+ })
438
+ ;
439
+ }
440
+
441
+ paletteID: (_?: string) => string | Pie;
442
+ useClonedPalette: (_?: boolean) => boolean | Pie;
443
+ outerText: (_?: boolean) => boolean | Pie;
444
+ innerRadius: { (): number; (_: number): Pie; };
445
+ innerRadius_exists: () => boolean;
446
+
447
+ // I2DChart
448
+ _palette;
449
+ fillColor: (row: any[], column: string, value: number) => string;
450
+ textColor: (row: any[], column: string, value: number) => string;
451
+ click: (row, column, selected) => void;
452
+ dblclick: (row, column, selected) => void;
453
+
454
+ // ITooltip
455
+ tooltip;
456
+ tooltipHTML: (_) => string;
457
+ tooltipFormat: (_) => string;
458
+ tooltipStyle: () => "default" | "none" | "series-table";
459
+ tooltipTick: { (): boolean; (_: boolean): Pie; };
460
+ tooltipTick_default: { (): boolean; (_: boolean): Pie; };
461
+ tooltipOffset: { (): number; (_: number): Pie; };
462
+ tooltipOffset_default: { (): number; (_: number): Pie; };
463
+
464
+ // SimpleSelectionMixin
465
+ _selection: Utility.SimpleSelection;
466
+ }
467
+
468
+ Pie.prototype._class += " chart_Pie";
469
+ Pie.prototype.implements(I2DChart.prototype);
470
+ Pie.prototype.implements(ITooltip.prototype);
471
+ Pie.prototype.mixin(Utility.SimpleSelectionMixin);
472
+ export interface Pie {
473
+ showSeriesValue(): boolean;
474
+ showSeriesValue(_: boolean): this;
475
+ seriesValueFormat(): string;
476
+ seriesValueFormat(_: string): this;
477
+ showSeriesPercentage(): boolean;
478
+ showSeriesPercentage(_: boolean): this;
479
+ minOuterRadius(): number;
480
+ minOuterRadius(_: number): this;
481
+ startAngle(): number;
482
+ startAngle(_: number): this;
483
+ labelHeight(): number;
484
+ labelHeight(_: number): this;
485
+ seriesPercentageFormat(): string;
486
+ seriesPercentageFormat(_: string): this;
487
+ showLabels(): boolean;
488
+ showLabels(_: boolean): this;
489
+ sortDataByValue(): "none" | "ascending" | "descending";
490
+ sortDataByValue(_: "none" | "ascending" | "descending"): this;
491
+ }
492
+ Pie.prototype.publish("showLabels", true, "boolean", "If true, wedge labels will display");
493
+ Pie.prototype.publish("showSeriesValue", false, "boolean", "Append data series value next to label", null, { disable: w => !w.showLabels() });
494
+ Pie.prototype.publish("seriesValueFormat", ",.0f", "string", "Number format used for formatting series values", null, { disable: w => !w.showSeriesValue() });
495
+ Pie.prototype.publish("showSeriesPercentage", false, "boolean", "Append data series percentage next to label", null, { disable: w => !w.showLabels() });
496
+ Pie.prototype.publish("seriesPercentageFormat", ",.0f", "string", "Number format used for formatting series percentages", null, { disable: w => !w.showSeriesPercentage() });
497
+ Pie.prototype.publish("paletteID", "default", "set", "Color palette for this widget", Pie.prototype._palette.switch(), { tags: ["Basic", "Shared"] });
498
+ Pie.prototype.publish("useClonedPalette", false, "boolean", "Enable or disable using a cloned palette", null, { tags: ["Intermediate", "Shared"] });
499
+ Pie.prototype.publish("innerRadius", 0, "number", "Sets inner pie hole radius as a percentage of the radius of the pie chart", null, { tags: ["Basic"], range: { min: 0, step: 1, max: 100 } });
500
+ Pie.prototype.publish("minOuterRadius", 20, "number", "Minimum outer radius (pixels)");
501
+ Pie.prototype.publish("startAngle", 0, "number", "Starting angle of the first (and largest) wedge (degrees)");
502
+ Pie.prototype.publish("labelHeight", 12, "number", "Font size of labels (pixels)", null, { disable: w => !w.showLabels() });
503
+ Pie.prototype.publish("sortDataByValue", "descending", "set", "Sort data by value", ["none", "ascending", "descending"]);