cx 24.10.0 → 24.10.2

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.
@@ -1,527 +1,529 @@
1
- import { Widget, VDOM } from "../ui/Widget";
2
- import { Container } from "../ui/Container";
3
- import { BoundedObject } from "../svg/BoundedObject";
4
- import { Rect } from "../svg/util/Rect";
5
- import { Selection } from "../ui/selection/Selection";
6
- import { tooltipMouseMove, tooltipMouseLeave } from "../widgets/overlay/tooltip-ops";
7
- import { isNumber } from "../util/isNumber";
8
- import { shallowEquals } from "../util/shallowEquals";
9
- import { withHoverSync } from "../ui/HoverSync";
10
-
11
- export class PieChart extends BoundedObject {
12
- declareData() {
13
- super.declareData(...arguments, {
14
- angle: undefined,
15
- startAngle: undefined,
16
- clockwise: undefined,
17
- gap: undefined,
18
- });
19
- }
20
-
21
- explore(context, instance) {
22
- if (!instance.pie) instance.pie = new PieCalculator();
23
- let { data } = instance;
24
- instance.pie.reset(data.angle, data.startAngle, data.clockwise, data.gap);
25
-
26
- context.push("pie", instance.pie);
27
- super.explore(context, instance);
28
- }
29
-
30
- exploreCleanup(context, instance) {
31
- context.pop("pie");
32
- }
33
-
34
- prepare(context, instance) {
35
- this.prepareBounds(context, instance);
36
- let { data, pie } = instance;
37
- pie.measure(data.bounds);
38
- let hash = pie.hash();
39
- instance.cache("hash", hash);
40
- pie.shouldUpdate = !shallowEquals(hash, instance.cached.hash);
41
- if (!pie.shouldUpdate) instance.markShouldUpdate(context);
42
- super.prepare(context, instance);
43
- }
44
- }
45
-
46
- PieChart.prototype.anchors = "0 1 1 0";
47
-
48
- class PieCalculator {
49
- reset(angle, startAngle, clockwise, gap) {
50
- if (angle == 360) angle = 359.99; // really hacky way to draw full circles
51
- this.angleTotal = (angle / 180) * Math.PI;
52
- this.startAngle = (startAngle / 180) * Math.PI;
53
- this.clockwise = clockwise;
54
- this.gap = gap;
55
- this.stacks = {};
56
- }
57
-
58
- acknowledge(stack, value, r, r0, percentageRadius) {
59
- let s = this.stacks[stack];
60
- if (!s) s = this.stacks[stack] = { total: 0, r0s: this.gap > 0 ? [] : null, r0ps: this.gap > 0 ? [] : null };
61
- if (value > 0) {
62
- s.total += value;
63
- if (this.gap > 0 && r0 > 0)
64
- if (percentageRadius) s.r0ps.push(r0);
65
- else s.r0s.push(r0);
66
- }
67
- }
68
-
69
- hash() {
70
- return {
71
- angleTotal: this.angleTotal,
72
- startAngle: this.startAngle,
73
- clockwise: this.clockwise,
74
- stacks: Object.keys(this.stacks)
75
- .map((s) => `${this.stacks[s].angleFactor}`)
76
- .join(":"),
77
- cx: this.cx,
78
- cy: this.cy,
79
- R: this.R,
80
- gap: this.gap,
81
- };
82
- }
83
-
84
- measure(rect) {
85
- this.R = Math.max(0, Math.min(rect.width(), rect.height())) / 2;
86
- for (let s in this.stacks) {
87
- let stack = this.stacks[s];
88
- let gapAngleTotal = 0;
89
- stack.gap = this.gap;
90
- if (this.gap > 0) {
91
- // gap cannot be larger of two times the smallest r0
92
- for (let index = 0; index < stack.r0s.length; index++)
93
- if (2 * stack.r0s[index] < stack.gap) stack.gap = 2 * stack.r0s[index];
94
- for (let index = 0; index < stack.r0ps.length; index++) {
95
- let r0 = (stack.r0ps[index] * this.R) / 100;
96
- if (2 * r0 < stack.gap) stack.gap = 2 * r0;
97
- }
98
- }
99
- while (stack.gap > 0) {
100
- for (let index = 0; index < stack.r0s.length; index++)
101
- gapAngleTotal += 2 * Math.asin(stack.gap / stack.r0s[index] / 2);
102
-
103
- for (let index = 0; index < stack.r0ps.length; index++)
104
- gapAngleTotal += 2 * Math.asin(stack.gap / ((stack.r0ps[index] * this.R) / 100) / 2);
105
-
106
- if (gapAngleTotal < 0.25 * this.angleTotal) break;
107
- stack.gap = stack.gap * 0.95;
108
- gapAngleTotal = 0;
109
- }
110
- if (gapAngleTotal == 0) stack.gap = 0;
111
- stack.angleFactor = stack.total > 0 ? (this.angleTotal - gapAngleTotal) / stack.total : 0;
112
- stack.lastAngle = this.startAngle;
113
- }
114
- this.cx = (rect.l + rect.r) / 2;
115
- this.cy = (rect.t + rect.b) / 2;
116
- }
117
-
118
- map(stack, value, r, r0, percentageRadius) {
119
- if (percentageRadius) {
120
- r = (r * this.R) / 100;
121
- r0 = (r0 * this.R) / 100;
122
- }
123
- let s = this.stacks[stack];
124
- let angle = value * s.angleFactor;
125
- let startAngle = s.lastAngle;
126
- let clockFactor = this.clockwise ? -1 : 1;
127
- let gapAngle = r0 > 0 && s.gap > 0 ? 2 * Math.asin(s.gap / r0 / 2) : 0;
128
- s.lastAngle += clockFactor * (angle + gapAngle);
129
- let endAngle = startAngle + clockFactor * angle + gapAngle;
130
-
131
- return {
132
- startAngle,
133
- endAngle: startAngle + clockFactor * angle + gapAngle,
134
- angle,
135
- midAngle: (startAngle + endAngle) / 2,
136
- gap: s.gap,
137
- cx: this.cx,
138
- cy: this.cy,
139
- R: this.R,
140
- };
141
- }
142
- }
143
-
144
- function createSvgArc(cx, cy, r0 = 0, r, startAngle, endAngle, br = 0, gap = 0) {
145
- let gap2 = gap / 2;
146
-
147
- if (startAngle > endAngle) {
148
- let s = startAngle;
149
- startAngle = endAngle;
150
- endAngle = s;
151
- }
152
-
153
- let path = [];
154
- // limit br size based on r and r0
155
- if (br > (r - r0) / 2) br = (r - r0) / 2;
156
-
157
- if (br > 0) {
158
- if (r0 > 0) {
159
- let innerBr = br;
160
- let innerSmallArcAngle = Math.asin((br + gap2) / (r0 + br));
161
-
162
- // adjust br according to the available area
163
- if (innerSmallArcAngle > (endAngle - startAngle) / 2) {
164
- innerSmallArcAngle = (endAngle - startAngle) / 2;
165
- let sin = Math.sin(innerSmallArcAngle);
166
- innerBr = Math.max((r0 * sin - gap2) / (1 - sin), 0);
167
- }
168
-
169
- let innerHipDiagonal = (r0 + innerBr) * Math.cos(innerSmallArcAngle);
170
-
171
- let innerSmallArc1XFrom = cx + Math.cos(endAngle) * innerHipDiagonal + Math.cos(endAngle - Math.PI / 2) * gap2;
172
- let innerSmallArc1YFrom = cy - Math.sin(endAngle) * innerHipDiagonal - Math.sin(endAngle - Math.PI / 2) * gap2;
173
-
174
- // move from the first small inner arc
175
- path.push(move(innerSmallArc1XFrom, innerSmallArc1YFrom));
176
-
177
- let innerSmallArc1XTo = cx + Math.cos(endAngle - innerSmallArcAngle) * r0;
178
- let innerSmallArc1YTo = cy - Math.sin(endAngle - innerSmallArcAngle) * r0;
179
-
180
- // add first small inner arc
181
- path.push(arc(innerBr, innerBr, 0, 0, 0, innerSmallArc1XTo, innerSmallArc1YTo));
182
-
183
- // SECOND ARC
184
-
185
- let innerArcXTo = cx + Math.cos(startAngle + innerSmallArcAngle) * r0;
186
- let innerArcYTo = cy - Math.sin(startAngle + innerSmallArcAngle) * r0;
187
- // add large inner arc
188
- path.push(
189
- arc(
190
- r0,
191
- r0,
192
- 0,
193
- largeArcFlag(endAngle - innerSmallArcAngle - startAngle - innerSmallArcAngle),
194
- 1,
195
- innerArcXTo,
196
- innerArcYTo,
197
- ),
198
- );
199
-
200
- let innerSmallArc2XTo =
201
- cx + Math.cos(startAngle) * innerHipDiagonal + Math.cos(startAngle + Math.PI / 2) * gap2;
202
- let innerSmallArc2YTo =
203
- cy - Math.sin(startAngle) * innerHipDiagonal - Math.sin(startAngle + Math.PI / 2) * gap2;
204
- // add second small inner arc
205
- path.push(arc(innerBr, innerBr, 0, 0, 0, innerSmallArc2XTo, innerSmallArc2YTo));
206
- } else {
207
- path.push(move(cx, cy));
208
- }
209
-
210
- let outerBr = br;
211
- let outerSmallArcAngle = Math.asin((br + gap2) / (r - br));
212
-
213
- // tweak br according to the available area
214
- if (outerSmallArcAngle > (endAngle - startAngle) / 2) {
215
- outerSmallArcAngle = (endAngle - startAngle) / 2;
216
- let sin = Math.sin(outerSmallArcAngle);
217
- outerBr = Math.max((r * sin - gap2) / (1 + sin), 0);
218
- }
219
-
220
- let outerHipDiagonal = Math.cos(outerSmallArcAngle) * (r - outerBr);
221
-
222
- let outerSmallArc1XFrom =
223
- cx + Math.cos(startAngle) * outerHipDiagonal + Math.cos(startAngle + Math.PI / 2) * gap2;
224
- let outerSmallArc1YFrom =
225
- cy - Math.sin(startAngle) * outerHipDiagonal - Math.sin(startAngle + Math.PI / 2) * gap2;
226
-
227
- let outerSmallArc1XTo = cx + Math.cos(startAngle + outerSmallArcAngle) * r;
228
- let outerSmallArc1YTo = cy - Math.sin(startAngle + outerSmallArcAngle) * r;
229
-
230
- let outerLargeArcXTo = cx + Math.cos(endAngle - outerSmallArcAngle) * r;
231
- let outerLargeArcYTo = cy - Math.sin(endAngle - outerSmallArcAngle) * r;
232
-
233
- let outerSmallArc2XTo = cx + Math.cos(endAngle) * outerHipDiagonal + Math.cos(endAngle - Math.PI / 2) * gap2;
234
- let outerSmallArc2YTo = cy - Math.sin(endAngle) * outerHipDiagonal - Math.sin(endAngle - Math.PI / 2) * gap2;
235
-
236
- path.push(
237
- line(outerSmallArc1XFrom, outerSmallArc1YFrom),
238
- arc(outerBr, outerBr, 0, 0, 0, outerSmallArc1XTo, outerSmallArc1YTo),
239
- arc(
240
- r,
241
- r,
242
- 0,
243
- largeArcFlag(endAngle - outerSmallArcAngle - startAngle - outerSmallArcAngle),
244
- 0,
245
- outerLargeArcXTo,
246
- outerLargeArcYTo,
247
- ),
248
- arc(outerBr, outerBr, 0, 0, 0, outerSmallArc2XTo, outerSmallArc2YTo),
249
- );
250
- } else {
251
- if (r0 > 0) {
252
- let innerGapAngle = r0 > 0 && gap2 > 0 ? Math.asin(gap2 / r0) : 0;
253
- let innerStartAngle = startAngle + innerGapAngle;
254
- let innerEndAngle = endAngle - innerGapAngle;
255
- let startX = cx + Math.cos(innerEndAngle) * r0;
256
- let startY = cy - Math.sin(innerEndAngle) * r0;
257
- path.push(move(startX, startY));
258
-
259
- let innerArcToX = cx + Math.cos(innerStartAngle) * r0;
260
- let innerArcToY = cy - Math.sin(innerStartAngle) * r0;
261
-
262
- path.push(arc(r0, r0, 0, largeArcFlag(innerStartAngle - innerEndAngle), 1, innerArcToX, innerArcToY));
263
- } else {
264
- path.push(move(cx, cy));
265
- }
266
-
267
- let outerGapAngle = r > 0 && gap2 > 0 ? Math.asin(gap2 / r) : 0;
268
- let outerStartAngle = startAngle + outerGapAngle;
269
- let outerEndAngle = endAngle - outerGapAngle;
270
- let lineToX = cx + Math.cos(outerStartAngle) * r;
271
- let lineToY = cy - Math.sin(outerStartAngle) * r;
272
- path.push(line(lineToX, lineToY));
273
-
274
- let arcToX = cx + Math.cos(outerEndAngle) * r;
275
- let arcToY = cy - Math.sin(outerEndAngle) * r;
276
- path.push(arc(r, r, 0, largeArcFlag(outerEndAngle - outerStartAngle), 0, arcToX, arcToY));
277
- }
278
-
279
- path.push(z());
280
- return path.join(" ");
281
- }
282
-
283
- PieChart.prototype.anchors = "0 1 1 0";
284
- PieChart.prototype.angle = 360;
285
- PieChart.prototype.startAngle = 0;
286
- PieChart.prototype.gap = 0;
287
-
288
- Widget.alias("pie-slice");
289
- export class PieSlice extends Container {
290
- init() {
291
- this.selection = Selection.create(this.selection);
292
- if (this.borderRadius) this.br = this.borderRadius;
293
- super.init();
294
- }
295
-
296
- declareData() {
297
- let selection = this.selection.configureWidget(this);
298
- super.declareData(...arguments, selection, {
299
- active: true,
300
- r0: undefined,
301
- r: undefined,
302
- colorIndex: undefined,
303
- colorMap: undefined,
304
- colorName: undefined,
305
- offset: undefined,
306
- value: undefined,
307
- disabled: undefined,
308
- innerPointRadius: undefined,
309
- outerPointRadius: undefined,
310
- name: undefined,
311
- stack: undefined,
312
- legend: undefined,
313
- hoverId: undefined,
314
- br: undefined,
315
- });
316
- }
317
-
318
- prepareData(context, instance) {
319
- let { data } = instance;
320
-
321
- if (data.name && !data.colorName) data.colorName = data.name;
322
-
323
- super.prepareData(context, instance);
324
- }
325
-
326
- explore(context, instance) {
327
- instance.pie = context.pie;
328
- if (!instance.pie) throw new Error("Pie.Slice must be placed inside a Pie.");
329
-
330
- let { data } = instance;
331
-
332
- instance.valid = isNumber(data.value) && data.value > 0;
333
-
334
- instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap);
335
- if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName);
336
-
337
- instance.hoverSync = context.hoverSync;
338
-
339
- if (instance.valid && data.active) {
340
- instance.pie.acknowledge(data.stack, data.value, data.r, data.r0, this.percentageRadius);
341
- super.explore(context, instance);
342
- }
343
- }
344
-
345
- prepare(context, instance) {
346
- let { data, segment, pie, colorMap } = instance;
347
-
348
- if (colorMap && data.colorName) {
349
- data.colorIndex = colorMap.map(data.colorName);
350
- if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context);
351
- }
352
-
353
- if (instance.valid && data.active) {
354
- let seg = pie.map(data.stack, data.value, data.r, data.r0, this.percentageRadius);
355
-
356
- if (
357
- !segment ||
358
- instance.shouldUpdate ||
359
- seg.startAngle != segment.startAngle ||
360
- seg.endAngle != segment.endAngle ||
361
- pie.shouldUpdate
362
- ) {
363
- if (data.offset > 0) {
364
- seg.ox = seg.cx + Math.cos(seg.midAngle) * data.offset;
365
- seg.oy = seg.cy - Math.sin(seg.midAngle) * data.offset;
366
- } else {
367
- seg.ox = seg.cx;
368
- seg.oy = seg.cy;
369
- }
370
-
371
- seg.radiusMultiplier = 1;
372
- if (this.percentageRadius) seg.radiusMultiplier = seg.R / 100;
373
-
374
- let innerR = data.innerPointRadius != null ? data.innerPointRadius : data.r0;
375
- let outerR = data.outerPointRadius != null ? data.outerPointRadius : data.r;
376
-
377
- let ix = seg.ox + Math.cos(seg.midAngle) * innerR * seg.radiusMultiplier;
378
- let iy = seg.oy - Math.sin(seg.midAngle) * innerR * seg.radiusMultiplier;
379
- let ox = seg.ox + Math.cos(seg.midAngle) * outerR * seg.radiusMultiplier;
380
- let oy = seg.oy - Math.sin(seg.midAngle) * outerR * seg.radiusMultiplier;
381
-
382
- instance.segment = seg;
383
- instance.bounds = new Rect({
384
- l: ix,
385
- r: ox,
386
- t: iy,
387
- b: oy,
388
- });
389
-
390
- instance.markShouldUpdate(context);
391
- }
392
-
393
- context.push("parentRect", instance.bounds);
394
- }
395
-
396
- if (data.name && data.legend && context.addLegendEntry)
397
- context.addLegendEntry(data.legend, {
398
- name: data.name,
399
- active: data.active,
400
- colorIndex: data.colorIndex,
401
- disabled: data.disabled,
402
- selected: this.selection.isInstanceSelected(instance),
403
- style: data.style,
404
- shape: this.legendShape,
405
- hoverId: data.hoverId,
406
- hoverChannel: this.hoverChannel,
407
- hoverSync: instance.hoverSync,
408
- onClick: (e) => {
409
- this.onLegendClick(e, instance);
410
- },
411
- });
412
- }
413
-
414
- prepareCleanup(context, instance) {
415
- if (instance.valid && instance.data.active) {
416
- context.pop("parentRect");
417
- }
418
- }
419
-
420
- onLegendClick(e, instance) {
421
- let allActions = this.legendAction == "auto";
422
- let { data } = instance;
423
- if (allActions || this.legendAction == "toggle") if (instance.set("active", !data.active)) return;
424
-
425
- if (allActions || this.legendAction == "select") this.handleClick(e, instance);
426
- }
427
-
428
- render(context, instance, key) {
429
- let { segment, data } = instance;
430
- if (!instance.valid || !data.active) return null;
431
-
432
- return withHoverSync(
433
- key,
434
- instance.hoverSync,
435
- this.hoverChannel,
436
- data.hoverId,
437
- ({ hover, onMouseMove, onMouseLeave }) => {
438
- let stateMods = {
439
- selected: this.selection.isInstanceSelected(instance),
440
- disabled: data.disabled,
441
- selectable: !this.selection.isDummy,
442
- [`color-${data.colorIndex}`]: data.colorIndex != null,
443
- hover,
444
- };
445
-
446
- let d = createSvgArc(
447
- segment.ox,
448
- segment.oy,
449
- data.r0 * segment.radiusMultiplier,
450
- data.r * segment.radiusMultiplier,
451
- segment.startAngle,
452
- segment.endAngle,
453
- data.br,
454
- segment.gap,
455
- );
456
-
457
- return (
458
- <g key={key} className={data.classNames}>
459
- <path
460
- className={this.CSS.element(this.baseClass, "slice", stateMods)}
461
- style={data.style}
462
- d={d}
463
- onMouseMove={(e) => {
464
- onMouseMove(e, instance);
465
- tooltipMouseMove(e, instance, this.tooltip);
466
- }}
467
- onMouseLeave={(e) => {
468
- onMouseLeave(e, instance);
469
- tooltipMouseLeave(e, instance, this.tooltip);
470
- }}
471
- onClick={(e) => {
472
- this.handleClick(e, instance);
473
- }}
474
- />
475
- {this.renderChildren(context, instance)}
476
- </g>
477
- );
478
- },
479
- );
480
- }
481
-
482
- handleClick(e, instance) {
483
- if (!this.selection.isDummy) {
484
- this.selection.selectInstance(instance, {
485
- toggle: e.ctrlKey,
486
- });
487
- e.stopPropagation();
488
- e.preventDefault();
489
- }
490
- }
491
- }
492
-
493
- function move(x, y) {
494
- return `M ${x} ${y}`;
495
- }
496
-
497
- function line(x, y) {
498
- return `L ${x} ${y}`;
499
- }
500
-
501
- function z() {
502
- return "Z";
503
- }
504
-
505
- function arc(rx, ry, xRotation, largeArc, sweep, x, y) {
506
- return `A ${rx} ${ry} ${xRotation} ${largeArc} ${sweep} ${x} ${y}`;
507
- }
508
-
509
- function largeArcFlag(angle) {
510
- return angle > Math.PI || angle < -Math.PI ? 1 : 0;
511
- }
512
-
513
- PieSlice.prototype.offset = 0;
514
- PieSlice.prototype.r0 = 0;
515
- PieSlice.prototype.r = 50;
516
- PieSlice.prototype.percentageRadius = true;
517
- PieSlice.prototype.baseClass = "pieslice";
518
- PieSlice.prototype.legend = "legend";
519
- PieSlice.prototype.active = true;
520
- PieSlice.prototype.stack = "stack";
521
- PieSlice.prototype.legendAction = "auto";
522
- PieSlice.prototype.legendShape = "circle";
523
- PieSlice.prototype.hoverChannel = "default";
524
- PieSlice.prototype.styled = true;
525
- PieSlice.prototype.br = 0;
526
-
527
- Widget.alias("pie-chart", PieChart);
1
+ import { Widget, VDOM } from "../ui/Widget";
2
+ import { Container } from "../ui/Container";
3
+ import { BoundedObject } from "../svg/BoundedObject";
4
+ import { Rect } from "../svg/util/Rect";
5
+ import { Selection } from "../ui/selection/Selection";
6
+ import { tooltipMouseMove, tooltipMouseLeave } from "../widgets/overlay/tooltip-ops";
7
+ import { isNumber } from "../util/isNumber";
8
+ import { shallowEquals } from "../util/shallowEquals";
9
+ import { withHoverSync } from "../ui/HoverSync";
10
+
11
+ export class PieChart extends BoundedObject {
12
+ declareData() {
13
+ super.declareData(...arguments, {
14
+ angle: undefined,
15
+ startAngle: undefined,
16
+ clockwise: undefined,
17
+ gap: undefined,
18
+ });
19
+ }
20
+
21
+ explore(context, instance) {
22
+ if (!instance.pie) instance.pie = new PieCalculator();
23
+ let { data } = instance;
24
+ instance.pie.reset(data.angle, data.startAngle, data.clockwise, data.gap);
25
+
26
+ context.push("pie", instance.pie);
27
+ super.explore(context, instance);
28
+ }
29
+
30
+ exploreCleanup(context, instance) {
31
+ context.pop("pie");
32
+ }
33
+
34
+ prepare(context, instance) {
35
+ this.prepareBounds(context, instance);
36
+ let { data, pie } = instance;
37
+ pie.measure(data.bounds);
38
+ let hash = pie.hash();
39
+ instance.cache("hash", hash);
40
+ pie.shouldUpdate = !shallowEquals(hash, instance.cached.hash);
41
+ if (!pie.shouldUpdate) instance.markShouldUpdate(context);
42
+ super.prepare(context, instance);
43
+ }
44
+ }
45
+
46
+ PieChart.prototype.anchors = "0 1 1 0";
47
+
48
+ class PieCalculator {
49
+ reset(angle, startAngle, clockwise, gap) {
50
+ if (angle == 360) angle = 359.99; // really hacky way to draw full circles
51
+ this.angleTotal = (angle / 180) * Math.PI;
52
+ this.startAngle = (startAngle / 180) * Math.PI;
53
+ this.clockwise = clockwise;
54
+ this.gap = gap;
55
+ this.stacks = {};
56
+ }
57
+
58
+ acknowledge(stack, value, r, r0, percentageRadius) {
59
+ let s = this.stacks[stack];
60
+ if (!s) s = this.stacks[stack] = { total: 0, r0s: this.gap > 0 ? [] : null, r0ps: this.gap > 0 ? [] : null };
61
+ if (value > 0) {
62
+ s.total += value;
63
+ if (this.gap > 0 && r0 > 0)
64
+ if (percentageRadius) s.r0ps.push(r0);
65
+ else s.r0s.push(r0);
66
+ }
67
+ }
68
+
69
+ hash() {
70
+ return {
71
+ angleTotal: this.angleTotal,
72
+ startAngle: this.startAngle,
73
+ clockwise: this.clockwise,
74
+ stacks: Object.keys(this.stacks)
75
+ .map((s) => `${this.stacks[s].angleFactor}`)
76
+ .join(":"),
77
+ cx: this.cx,
78
+ cy: this.cy,
79
+ R: this.R,
80
+ gap: this.gap,
81
+ };
82
+ }
83
+
84
+ measure(rect) {
85
+ this.R = Math.max(0, Math.min(rect.width(), rect.height())) / 2;
86
+ for (let s in this.stacks) {
87
+ let stack = this.stacks[s];
88
+ let gapAngleTotal = 0;
89
+ stack.gap = this.gap;
90
+ if (this.gap > 0) {
91
+ // gap cannot be larger of two times the smallest r0
92
+ for (let index = 0; index < stack.r0s.length; index++)
93
+ if (2 * stack.r0s[index] < stack.gap) stack.gap = 2 * stack.r0s[index];
94
+ for (let index = 0; index < stack.r0ps.length; index++) {
95
+ let r0 = (stack.r0ps[index] * this.R) / 100;
96
+ if (2 * r0 < stack.gap) stack.gap = 2 * r0;
97
+ }
98
+ }
99
+ while (stack.gap > 0) {
100
+ for (let index = 0; index < stack.r0s.length; index++)
101
+ gapAngleTotal += 2 * Math.asin(stack.gap / stack.r0s[index] / 2);
102
+
103
+ for (let index = 0; index < stack.r0ps.length; index++)
104
+ gapAngleTotal += 2 * Math.asin(stack.gap / ((stack.r0ps[index] * this.R) / 100) / 2);
105
+
106
+ if (gapAngleTotal < 0.25 * this.angleTotal) break;
107
+ stack.gap = stack.gap * 0.95;
108
+ gapAngleTotal = 0;
109
+ }
110
+ if (gapAngleTotal == 0) stack.gap = 0;
111
+ stack.angleFactor = stack.total > 0 ? (this.angleTotal - gapAngleTotal) / stack.total : 0;
112
+ stack.lastAngle = this.startAngle;
113
+ }
114
+ this.cx = (rect.l + rect.r) / 2;
115
+ this.cy = (rect.t + rect.b) / 2;
116
+ }
117
+
118
+ map(stack, value, r, r0, percentageRadius) {
119
+ if (percentageRadius) {
120
+ r = (r * this.R) / 100;
121
+ r0 = (r0 * this.R) / 100;
122
+ }
123
+ let s = this.stacks[stack];
124
+ let angle = value * s.angleFactor;
125
+ let startAngle = s.lastAngle;
126
+ let clockFactor = this.clockwise ? -1 : 1;
127
+ let gapAngle = r0 > 0 && s.gap > 0 ? 2 * Math.asin(s.gap / r0 / 2) : 0;
128
+ s.lastAngle += clockFactor * (angle + gapAngle);
129
+ let endAngle = startAngle + clockFactor * angle + gapAngle;
130
+
131
+ return {
132
+ startAngle,
133
+ endAngle: startAngle + clockFactor * angle + gapAngle,
134
+ angle,
135
+ midAngle: (startAngle + endAngle) / 2,
136
+ gap: s.gap,
137
+ cx: this.cx,
138
+ cy: this.cy,
139
+ R: this.R,
140
+ };
141
+ }
142
+ }
143
+
144
+ function createSvgArc(cx, cy, r0 = 0, r, startAngle, endAngle, br = 0, gap = 0) {
145
+ let gap2 = gap / 2;
146
+
147
+ if (startAngle > endAngle) {
148
+ let s = startAngle;
149
+ startAngle = endAngle;
150
+ endAngle = s;
151
+ }
152
+
153
+ let path = [];
154
+ // limit br size based on r and r0
155
+ if (br > (r - r0) / 2) br = (r - r0) / 2;
156
+
157
+ if (br > 0) {
158
+ if (r0 > 0) {
159
+ let innerBr = br;
160
+ let innerSmallArcAngle = Math.asin((br + gap2) / (r0 + br));
161
+
162
+ // adjust br according to the available area
163
+ if (innerSmallArcAngle > (endAngle - startAngle) / 2) {
164
+ innerSmallArcAngle = (endAngle - startAngle) / 2;
165
+ let sin = Math.sin(innerSmallArcAngle);
166
+ innerBr = Math.max((r0 * sin - gap2) / (1 - sin), 0);
167
+ }
168
+
169
+ let innerHipDiagonal = (r0 + innerBr) * Math.cos(innerSmallArcAngle);
170
+
171
+ let innerSmallArc1XFrom = cx + Math.cos(endAngle) * innerHipDiagonal + Math.cos(endAngle - Math.PI / 2) * gap2;
172
+ let innerSmallArc1YFrom = cy - Math.sin(endAngle) * innerHipDiagonal - Math.sin(endAngle - Math.PI / 2) * gap2;
173
+
174
+ // move from the first small inner arc
175
+ path.push(move(innerSmallArc1XFrom, innerSmallArc1YFrom));
176
+
177
+ let innerSmallArc1XTo = cx + Math.cos(endAngle - innerSmallArcAngle) * r0;
178
+ let innerSmallArc1YTo = cy - Math.sin(endAngle - innerSmallArcAngle) * r0;
179
+
180
+ // add first small inner arc
181
+ path.push(arc(innerBr, innerBr, 0, 0, 0, innerSmallArc1XTo, innerSmallArc1YTo));
182
+
183
+ // SECOND ARC
184
+
185
+ let innerArcXTo = cx + Math.cos(startAngle + innerSmallArcAngle) * r0;
186
+ let innerArcYTo = cy - Math.sin(startAngle + innerSmallArcAngle) * r0;
187
+ // add large inner arc
188
+ path.push(
189
+ arc(
190
+ r0,
191
+ r0,
192
+ 0,
193
+ largeArcFlag(endAngle - innerSmallArcAngle - startAngle - innerSmallArcAngle),
194
+ 1,
195
+ innerArcXTo,
196
+ innerArcYTo,
197
+ ),
198
+ );
199
+
200
+ let innerSmallArc2XTo =
201
+ cx + Math.cos(startAngle) * innerHipDiagonal + Math.cos(startAngle + Math.PI / 2) * gap2;
202
+ let innerSmallArc2YTo =
203
+ cy - Math.sin(startAngle) * innerHipDiagonal - Math.sin(startAngle + Math.PI / 2) * gap2;
204
+ // add second small inner arc
205
+ path.push(arc(innerBr, innerBr, 0, 0, 0, innerSmallArc2XTo, innerSmallArc2YTo));
206
+ } else {
207
+ path.push(move(cx, cy));
208
+ }
209
+
210
+ let outerBr = br;
211
+ let outerSmallArcAngle = Math.asin((br + gap2) / (r - br));
212
+
213
+ // tweak br according to the available area
214
+ if (outerSmallArcAngle > (endAngle - startAngle) / 2) {
215
+ outerSmallArcAngle = (endAngle - startAngle) / 2;
216
+ let sin = Math.sin(outerSmallArcAngle);
217
+ outerBr = Math.max((r * sin - gap2) / (1 + sin), 0);
218
+ }
219
+
220
+ let outerHipDiagonal = Math.cos(outerSmallArcAngle) * (r - outerBr);
221
+
222
+ let outerSmallArc1XFrom =
223
+ cx + Math.cos(startAngle) * outerHipDiagonal + Math.cos(startAngle + Math.PI / 2) * gap2;
224
+ let outerSmallArc1YFrom =
225
+ cy - Math.sin(startAngle) * outerHipDiagonal - Math.sin(startAngle + Math.PI / 2) * gap2;
226
+
227
+ let outerSmallArc1XTo = cx + Math.cos(startAngle + outerSmallArcAngle) * r;
228
+ let outerSmallArc1YTo = cy - Math.sin(startAngle + outerSmallArcAngle) * r;
229
+
230
+ let outerLargeArcXTo = cx + Math.cos(endAngle - outerSmallArcAngle) * r;
231
+ let outerLargeArcYTo = cy - Math.sin(endAngle - outerSmallArcAngle) * r;
232
+
233
+ let outerSmallArc2XTo = cx + Math.cos(endAngle) * outerHipDiagonal + Math.cos(endAngle - Math.PI / 2) * gap2;
234
+ let outerSmallArc2YTo = cy - Math.sin(endAngle) * outerHipDiagonal - Math.sin(endAngle - Math.PI / 2) * gap2;
235
+
236
+ path.push(
237
+ line(outerSmallArc1XFrom, outerSmallArc1YFrom),
238
+ arc(outerBr, outerBr, 0, 0, 0, outerSmallArc1XTo, outerSmallArc1YTo),
239
+ arc(
240
+ r,
241
+ r,
242
+ 0,
243
+ largeArcFlag(endAngle - outerSmallArcAngle - startAngle - outerSmallArcAngle),
244
+ 0,
245
+ outerLargeArcXTo,
246
+ outerLargeArcYTo,
247
+ ),
248
+ arc(outerBr, outerBr, 0, 0, 0, outerSmallArc2XTo, outerSmallArc2YTo),
249
+ );
250
+ } else {
251
+ if (r0 > 0) {
252
+ let innerGapAngle = r0 > 0 && gap2 > 0 ? Math.asin(gap2 / r0) : 0;
253
+ let innerStartAngle = startAngle + innerGapAngle;
254
+ let innerEndAngle = endAngle - innerGapAngle;
255
+ let startX = cx + Math.cos(innerEndAngle) * r0;
256
+ let startY = cy - Math.sin(innerEndAngle) * r0;
257
+ path.push(move(startX, startY));
258
+
259
+ let innerArcToX = cx + Math.cos(innerStartAngle) * r0;
260
+ let innerArcToY = cy - Math.sin(innerStartAngle) * r0;
261
+
262
+ path.push(arc(r0, r0, 0, largeArcFlag(innerStartAngle - innerEndAngle), 1, innerArcToX, innerArcToY));
263
+ } else {
264
+ path.push(move(cx, cy));
265
+ }
266
+
267
+ let outerGapAngle = r > 0 && gap2 > 0 ? Math.asin(gap2 / r) : 0;
268
+ let outerStartAngle = startAngle + outerGapAngle;
269
+ let outerEndAngle = endAngle - outerGapAngle;
270
+ let lineToX = cx + Math.cos(outerStartAngle) * r;
271
+ let lineToY = cy - Math.sin(outerStartAngle) * r;
272
+ path.push(line(lineToX, lineToY));
273
+
274
+ let arcToX = cx + Math.cos(outerEndAngle) * r;
275
+ let arcToY = cy - Math.sin(outerEndAngle) * r;
276
+ path.push(arc(r, r, 0, largeArcFlag(outerEndAngle - outerStartAngle), 0, arcToX, arcToY));
277
+ }
278
+
279
+ path.push(z());
280
+ return path.join(" ");
281
+ }
282
+
283
+ PieChart.prototype.anchors = "0 1 1 0";
284
+ PieChart.prototype.angle = 360;
285
+ PieChart.prototype.startAngle = 0;
286
+ PieChart.prototype.gap = 0;
287
+
288
+ Widget.alias("pie-slice");
289
+ export class PieSlice extends Container {
290
+ init() {
291
+ this.selection = Selection.create(this.selection);
292
+ if (this.borderRadius) this.br = this.borderRadius;
293
+ super.init();
294
+ }
295
+
296
+ declareData() {
297
+ let selection = this.selection.configureWidget(this);
298
+ super.declareData(...arguments, selection, {
299
+ active: true,
300
+ r0: undefined,
301
+ r: undefined,
302
+ colorIndex: undefined,
303
+ colorMap: undefined,
304
+ colorName: undefined,
305
+ offset: undefined,
306
+ value: undefined,
307
+ disabled: undefined,
308
+ innerPointRadius: undefined,
309
+ outerPointRadius: undefined,
310
+ name: undefined,
311
+ stack: undefined,
312
+ legend: undefined,
313
+ hoverId: undefined,
314
+ br: undefined,
315
+ legendDisplayText: undefined,
316
+ });
317
+ }
318
+
319
+ prepareData(context, instance) {
320
+ let { data } = instance;
321
+
322
+ if (data.name && !data.colorName) data.colorName = data.name;
323
+
324
+ super.prepareData(context, instance);
325
+ }
326
+
327
+ explore(context, instance) {
328
+ instance.pie = context.pie;
329
+ if (!instance.pie) throw new Error("Pie.Slice must be placed inside a Pie.");
330
+
331
+ let { data } = instance;
332
+
333
+ instance.valid = isNumber(data.value) && data.value > 0;
334
+
335
+ instance.colorMap = data.colorMap && context.getColorMap && context.getColorMap(data.colorMap);
336
+ if (instance.colorMap && data.colorName) instance.colorMap.acknowledge(data.colorName);
337
+
338
+ instance.hoverSync = context.hoverSync;
339
+
340
+ if (instance.valid && data.active) {
341
+ instance.pie.acknowledge(data.stack, data.value, data.r, data.r0, this.percentageRadius);
342
+ super.explore(context, instance);
343
+ }
344
+ }
345
+
346
+ prepare(context, instance) {
347
+ let { data, segment, pie, colorMap } = instance;
348
+
349
+ if (colorMap && data.colorName) {
350
+ data.colorIndex = colorMap.map(data.colorName);
351
+ if (instance.cache("colorIndex", data.colorIndex)) instance.markShouldUpdate(context);
352
+ }
353
+
354
+ if (instance.valid && data.active) {
355
+ let seg = pie.map(data.stack, data.value, data.r, data.r0, this.percentageRadius);
356
+
357
+ if (
358
+ !segment ||
359
+ instance.shouldUpdate ||
360
+ seg.startAngle != segment.startAngle ||
361
+ seg.endAngle != segment.endAngle ||
362
+ pie.shouldUpdate
363
+ ) {
364
+ if (data.offset > 0) {
365
+ seg.ox = seg.cx + Math.cos(seg.midAngle) * data.offset;
366
+ seg.oy = seg.cy - Math.sin(seg.midAngle) * data.offset;
367
+ } else {
368
+ seg.ox = seg.cx;
369
+ seg.oy = seg.cy;
370
+ }
371
+
372
+ seg.radiusMultiplier = 1;
373
+ if (this.percentageRadius) seg.radiusMultiplier = seg.R / 100;
374
+
375
+ let innerR = data.innerPointRadius != null ? data.innerPointRadius : data.r0;
376
+ let outerR = data.outerPointRadius != null ? data.outerPointRadius : data.r;
377
+
378
+ let ix = seg.ox + Math.cos(seg.midAngle) * innerR * seg.radiusMultiplier;
379
+ let iy = seg.oy - Math.sin(seg.midAngle) * innerR * seg.radiusMultiplier;
380
+ let ox = seg.ox + Math.cos(seg.midAngle) * outerR * seg.radiusMultiplier;
381
+ let oy = seg.oy - Math.sin(seg.midAngle) * outerR * seg.radiusMultiplier;
382
+
383
+ instance.segment = seg;
384
+ instance.bounds = new Rect({
385
+ l: ix,
386
+ r: ox,
387
+ t: iy,
388
+ b: oy,
389
+ });
390
+
391
+ instance.markShouldUpdate(context);
392
+ }
393
+
394
+ context.push("parentRect", instance.bounds);
395
+ }
396
+
397
+ if (data.name && data.legend && context.addLegendEntry)
398
+ context.addLegendEntry(data.legend, {
399
+ name: data.name,
400
+ active: data.active,
401
+ colorIndex: data.colorIndex,
402
+ disabled: data.disabled,
403
+ selected: this.selection.isInstanceSelected(instance),
404
+ style: data.style,
405
+ shape: this.legendShape,
406
+ hoverId: data.hoverId,
407
+ hoverChannel: this.hoverChannel,
408
+ hoverSync: instance.hoverSync,
409
+ displayText: data.legendDisplayText,
410
+ onClick: (e) => {
411
+ this.onLegendClick(e, instance);
412
+ },
413
+ });
414
+ }
415
+
416
+ prepareCleanup(context, instance) {
417
+ if (instance.valid && instance.data.active) {
418
+ context.pop("parentRect");
419
+ }
420
+ }
421
+
422
+ onLegendClick(e, instance) {
423
+ let allActions = this.legendAction == "auto";
424
+ let { data } = instance;
425
+ if (allActions || this.legendAction == "toggle") if (instance.set("active", !data.active)) return;
426
+
427
+ if (allActions || this.legendAction == "select") this.handleClick(e, instance);
428
+ }
429
+
430
+ render(context, instance, key) {
431
+ let { segment, data } = instance;
432
+ if (!instance.valid || !data.active) return null;
433
+
434
+ return withHoverSync(
435
+ key,
436
+ instance.hoverSync,
437
+ this.hoverChannel,
438
+ data.hoverId,
439
+ ({ hover, onMouseMove, onMouseLeave }) => {
440
+ let stateMods = {
441
+ selected: this.selection.isInstanceSelected(instance),
442
+ disabled: data.disabled,
443
+ selectable: !this.selection.isDummy,
444
+ [`color-${data.colorIndex}`]: data.colorIndex != null,
445
+ hover,
446
+ };
447
+
448
+ let d = createSvgArc(
449
+ segment.ox,
450
+ segment.oy,
451
+ data.r0 * segment.radiusMultiplier,
452
+ data.r * segment.radiusMultiplier,
453
+ segment.startAngle,
454
+ segment.endAngle,
455
+ data.br,
456
+ segment.gap,
457
+ );
458
+
459
+ return (
460
+ <g key={key} className={data.classNames}>
461
+ <path
462
+ className={this.CSS.element(this.baseClass, "slice", stateMods)}
463
+ style={data.style}
464
+ d={d}
465
+ onMouseMove={(e) => {
466
+ onMouseMove(e, instance);
467
+ tooltipMouseMove(e, instance, this.tooltip);
468
+ }}
469
+ onMouseLeave={(e) => {
470
+ onMouseLeave(e, instance);
471
+ tooltipMouseLeave(e, instance, this.tooltip);
472
+ }}
473
+ onClick={(e) => {
474
+ this.handleClick(e, instance);
475
+ }}
476
+ />
477
+ {this.renderChildren(context, instance)}
478
+ </g>
479
+ );
480
+ },
481
+ );
482
+ }
483
+
484
+ handleClick(e, instance) {
485
+ if (!this.selection.isDummy) {
486
+ this.selection.selectInstance(instance, {
487
+ toggle: e.ctrlKey,
488
+ });
489
+ e.stopPropagation();
490
+ e.preventDefault();
491
+ }
492
+ }
493
+ }
494
+
495
+ function move(x, y) {
496
+ return `M ${x} ${y}`;
497
+ }
498
+
499
+ function line(x, y) {
500
+ return `L ${x} ${y}`;
501
+ }
502
+
503
+ function z() {
504
+ return "Z";
505
+ }
506
+
507
+ function arc(rx, ry, xRotation, largeArc, sweep, x, y) {
508
+ return `A ${rx} ${ry} ${xRotation} ${largeArc} ${sweep} ${x} ${y}`;
509
+ }
510
+
511
+ function largeArcFlag(angle) {
512
+ return angle > Math.PI || angle < -Math.PI ? 1 : 0;
513
+ }
514
+
515
+ PieSlice.prototype.offset = 0;
516
+ PieSlice.prototype.r0 = 0;
517
+ PieSlice.prototype.r = 50;
518
+ PieSlice.prototype.percentageRadius = true;
519
+ PieSlice.prototype.baseClass = "pieslice";
520
+ PieSlice.prototype.legend = "legend";
521
+ PieSlice.prototype.active = true;
522
+ PieSlice.prototype.stack = "stack";
523
+ PieSlice.prototype.legendAction = "auto";
524
+ PieSlice.prototype.legendShape = "circle";
525
+ PieSlice.prototype.hoverChannel = "default";
526
+ PieSlice.prototype.styled = true;
527
+ PieSlice.prototype.br = 0;
528
+
529
+ Widget.alias("pie-chart", PieChart);