@hpcc-js/chart 3.6.5 → 3.6.6

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