@hpcc-js/chart 2.86.2 → 2.86.3

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 (78) hide show
  1. package/LICENSE +43 -43
  2. package/README.md +93 -93
  3. package/dist/index.es6.js.map +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.min.js.map +1 -1
  6. package/package.json +6 -6
  7. package/src/Area.md +176 -176
  8. package/src/Area.ts +12 -12
  9. package/src/Axis.css +34 -34
  10. package/src/Axis.ts +733 -733
  11. package/src/Bar.md +90 -90
  12. package/src/Bar.ts +9 -9
  13. package/src/Bubble.css +16 -16
  14. package/src/Bubble.md +69 -69
  15. package/src/Bubble.ts +191 -191
  16. package/src/BubbleXY.ts +14 -14
  17. package/src/Bullet.css +60 -60
  18. package/src/Bullet.md +104 -104
  19. package/src/Bullet.ts +167 -167
  20. package/src/Column.css +17 -17
  21. package/src/Column.md +90 -90
  22. package/src/Column.ts +659 -659
  23. package/src/Contour.md +88 -88
  24. package/src/Contour.ts +97 -97
  25. package/src/D3Cloud.ts +400 -400
  26. package/src/Gantt.md +119 -119
  27. package/src/Gantt.ts +14 -14
  28. package/src/Gauge.md +148 -148
  29. package/src/Gauge.ts +358 -358
  30. package/src/HalfPie.md +62 -62
  31. package/src/HalfPie.ts +26 -26
  32. package/src/Heat.md +42 -42
  33. package/src/Heat.ts +283 -283
  34. package/src/HexBin.css +9 -9
  35. package/src/HexBin.md +88 -88
  36. package/src/HexBin.ts +139 -139
  37. package/src/Line.css +6 -6
  38. package/src/Line.md +170 -170
  39. package/src/Line.ts +14 -14
  40. package/src/Pie.css +23 -23
  41. package/src/Pie.md +88 -88
  42. package/src/Pie.ts +503 -503
  43. package/src/QuarterPie.md +61 -61
  44. package/src/QuarterPie.ts +35 -35
  45. package/src/QuartileCandlestick.md +129 -129
  46. package/src/QuartileCandlestick.ts +349 -349
  47. package/src/Radar.css +15 -15
  48. package/src/Radar.md +104 -104
  49. package/src/Radar.ts +336 -336
  50. package/src/RadialBar.css +25 -25
  51. package/src/RadialBar.md +91 -91
  52. package/src/RadialBar.ts +212 -212
  53. package/src/Scatter.css +16 -16
  54. package/src/Scatter.md +163 -163
  55. package/src/Scatter.ts +376 -376
  56. package/src/StatChart.md +117 -117
  57. package/src/StatChart.ts +253 -253
  58. package/src/Step.md +163 -163
  59. package/src/Step.ts +12 -12
  60. package/src/Summary.css +56 -56
  61. package/src/Summary.md +219 -219
  62. package/src/Summary.ts +322 -322
  63. package/src/SummaryC.md +154 -154
  64. package/src/SummaryC.ts +240 -240
  65. package/src/WordCloud.css +3 -3
  66. package/src/WordCloud.md +144 -144
  67. package/src/WordCloud.ts +263 -263
  68. package/src/XYAxis.css +41 -41
  69. package/src/XYAxis.md +149 -149
  70. package/src/XYAxis.ts +803 -803
  71. package/src/__package__.ts +3 -3
  72. package/src/__tests__/heat.ts +71 -71
  73. package/src/__tests__/index.ts +3 -3
  74. package/src/__tests__/pie.ts +20 -20
  75. package/src/__tests__/stat.ts +16 -16
  76. package/src/__tests__/test3.ts +69 -69
  77. package/src/index.ts +27 -27
  78. package/src/test.ts +71 -71
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"]);