@motionscript/charts 0.0.0-stage → 0.1.0-alpha.0
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.
- package/CHANGELOG.md +5 -0
- package/LICENSE +201 -0
- package/dist/bar-chart/bar-chart.d.ts +286 -0
- package/dist/bar-chart/bar-chart.d.ts.map +1 -0
- package/dist/bar-chart/bar-chart.js +610 -0
- package/dist/bar-chart/bar-chart.js.map +1 -0
- package/dist/bar-chart/category-axis.d.ts +77 -0
- package/dist/bar-chart/category-axis.d.ts.map +1 -0
- package/dist/bar-chart/category-axis.js +141 -0
- package/dist/bar-chart/category-axis.js.map +1 -0
- package/dist/bar-chart/chart-bars.d.ts +149 -0
- package/dist/bar-chart/chart-bars.d.ts.map +1 -0
- package/dist/bar-chart/chart-bars.js +236 -0
- package/dist/bar-chart/chart-bars.js.map +1 -0
- package/dist/bar-chart/index.d.ts +16 -0
- package/dist/bar-chart/index.d.ts.map +1 -0
- package/dist/bar-chart/index.js +12 -0
- package/dist/bar-chart/index.js.map +1 -0
- package/dist/bar-chart/shared.d.ts +146 -0
- package/dist/bar-chart/shared.d.ts.map +1 -0
- package/dist/bar-chart/shared.js +182 -0
- package/dist/bar-chart/shared.js.map +1 -0
- package/dist/browser/chunks/chunk-ZMBOBHYL.js +2 -0
- package/dist/browser/chunks/chunk-ZMBOBHYL.js.map +7 -0
- package/dist/browser/index.js +2 -0
- package/dist/browser/index.js.map +7 -0
- package/dist/browser/kit.js +2 -0
- package/dist/browser/kit.js.map +7 -0
- package/dist/browser/manifest.json +12 -0
- package/dist/engine.d.ts +27 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +27 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/kit/chart-border.d.ts +28 -0
- package/dist/kit/chart-border.d.ts.map +1 -0
- package/dist/kit/chart-border.js +45 -0
- package/dist/kit/chart-border.js.map +1 -0
- package/dist/kit/chart-canvas.d.ts +56 -0
- package/dist/kit/chart-canvas.d.ts.map +1 -0
- package/dist/kit/chart-canvas.js +125 -0
- package/dist/kit/chart-canvas.js.map +1 -0
- package/dist/kit/chart-legend.d.ts +126 -0
- package/dist/kit/chart-legend.d.ts.map +1 -0
- package/dist/kit/chart-legend.js +212 -0
- package/dist/kit/chart-legend.js.map +1 -0
- package/dist/kit/shared.d.ts +251 -0
- package/dist/kit/shared.d.ts.map +1 -0
- package/dist/kit/shared.js +401 -0
- package/dist/kit/shared.js.map +1 -0
- package/dist/kit/vertical-axis.d.ts +99 -0
- package/dist/kit/vertical-axis.d.ts.map +1 -0
- package/dist/kit/vertical-axis.js +176 -0
- package/dist/kit/vertical-axis.js.map +1 -0
- package/dist/line-chart/chart-line.d.ts +91 -0
- package/dist/line-chart/chart-line.d.ts.map +1 -0
- package/dist/line-chart/chart-line.js +156 -0
- package/dist/line-chart/chart-line.js.map +1 -0
- package/dist/line-chart/chart-region.d.ts +47 -0
- package/dist/line-chart/chart-region.d.ts.map +1 -0
- package/dist/line-chart/chart-region.js +101 -0
- package/dist/line-chart/chart-region.js.map +1 -0
- package/dist/line-chart/horizontal-axis.d.ts +85 -0
- package/dist/line-chart/horizontal-axis.d.ts.map +1 -0
- package/dist/line-chart/horizontal-axis.js +158 -0
- package/dist/line-chart/horizontal-axis.js.map +1 -0
- package/dist/line-chart/index.d.ts +22 -0
- package/dist/line-chart/index.d.ts.map +1 -0
- package/dist/line-chart/index.js +19 -0
- package/dist/line-chart/index.js.map +1 -0
- package/dist/line-chart/line-chart.d.ts +347 -0
- package/dist/line-chart/line-chart.d.ts.map +1 -0
- package/dist/line-chart/line-chart.js +701 -0
- package/dist/line-chart/line-chart.js.map +1 -0
- package/dist/line-chart/shared.d.ts +171 -0
- package/dist/line-chart/shared.d.ts.map +1 -0
- package/dist/line-chart/shared.js +270 -0
- package/dist/line-chart/shared.js.map +1 -0
- package/dist/line-chart/x-scale.d.ts +84 -0
- package/dist/line-chart/x-scale.d.ts.map +1 -0
- package/dist/line-chart/x-scale.js +302 -0
- package/dist/line-chart/x-scale.js.map +1 -0
- package/dist/nodes.d.ts +20 -0
- package/dist/nodes.d.ts.map +1 -0
- package/dist/nodes.js +20 -0
- package/dist/nodes.js.map +1 -0
- package/dist/pie-chart/index.d.ts +18 -0
- package/dist/pie-chart/index.d.ts.map +1 -0
- package/dist/pie-chart/index.js +14 -0
- package/dist/pie-chart/index.js.map +1 -0
- package/dist/pie-chart/pie-chart.d.ts +170 -0
- package/dist/pie-chart/pie-chart.d.ts.map +1 -0
- package/dist/pie-chart/pie-chart.js +291 -0
- package/dist/pie-chart/pie-chart.js.map +1 -0
- package/dist/pie-chart/pie-legend.d.ts +60 -0
- package/dist/pie-chart/pie-legend.d.ts.map +1 -0
- package/dist/pie-chart/pie-legend.js +131 -0
- package/dist/pie-chart/pie-legend.js.map +1 -0
- package/dist/pie-chart/pie-ring.d.ts +266 -0
- package/dist/pie-chart/pie-ring.d.ts.map +1 -0
- package/dist/pie-chart/pie-ring.js +708 -0
- package/dist/pie-chart/pie-ring.js.map +1 -0
- package/dist/pie-chart/shared.d.ts +210 -0
- package/dist/pie-chart/shared.d.ts.map +1 -0
- package/dist/pie-chart/shared.js +240 -0
- package/dist/pie-chart/shared.js.map +1 -0
- package/package.json +69 -3
- package/registry.json +23 -0
- package/src/bar-chart/bar-chart.ts +804 -0
- package/src/bar-chart/category-axis.ts +177 -0
- package/src/bar-chart/chart-bars.ts +310 -0
- package/src/bar-chart/index.ts +31 -0
- package/src/bar-chart/shared.ts +354 -0
- package/src/engine.ts +26 -0
- package/src/index.ts +5 -0
- package/src/kit/chart-border.ts +61 -0
- package/src/kit/chart-canvas.ts +157 -0
- package/src/kit/chart-legend.ts +250 -0
- package/src/kit/shared.ts +675 -0
- package/src/kit/vertical-axis.ts +224 -0
- package/src/line-chart/chart-line.ts +208 -0
- package/src/line-chart/chart-region.ts +135 -0
- package/src/line-chart/horizontal-axis.ts +202 -0
- package/src/line-chart/index.ts +39 -0
- package/src/line-chart/line-chart.ts +894 -0
- package/src/line-chart/shared.ts +459 -0
- package/src/line-chart/x-scale.ts +367 -0
- package/src/nodes.ts +20 -0
- package/src/pie-chart/index.ts +37 -0
- package/src/pie-chart/pie-chart.ts +374 -0
- package/src/pie-chart/pie-legend.ts +159 -0
- package/src/pie-chart/pie-ring.ts +903 -0
- package/src/pie-chart/shared.ts +490 -0
- package/README.md +0 -4
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { Rect, command, Graphics2D, Node2D, PathBuilder, Text, easeOut, fillOps, property, strokeOps, textOps, } from "@motionscript/core";
|
|
8
|
+
import { clampUnit, formatPercent } from "../kit/shared.js";
|
|
9
|
+
import { DEFAULT_PIE_CHART_THEME, pieArcs, relaxLabels, ringPoint, } from "./shared.js";
|
|
10
|
+
import { declarePaints } from "@motionscript/core/component";
|
|
11
|
+
/** Vertical breathing room between one label block and the next, in px. */
|
|
12
|
+
const LABEL_GAP = 12;
|
|
13
|
+
/** Breathing room past the outermost label, so it never touches the node's edge, in px. */
|
|
14
|
+
const LABEL_PAD = 14;
|
|
15
|
+
/** Rough width of one glyph as a fraction of the font size, for gutter sizing. */
|
|
16
|
+
const GLYPH_RATIO = 0.62;
|
|
17
|
+
/** Gap between a wedge's outer edge and where its leader lifts off, in px. */
|
|
18
|
+
const LEADER_LIFTOFF = 6;
|
|
19
|
+
/** How far past the ring's outer edge a leader runs before it elbows, in px. */
|
|
20
|
+
const LEADER_LENGTH = 34;
|
|
21
|
+
/** Horizontal run from the elbow to the label, in px. */
|
|
22
|
+
const LABEL_STUB = 26;
|
|
23
|
+
/** Gap between a leader's end and the label text it points at, in px. */
|
|
24
|
+
const TEXT_INSET = 8;
|
|
25
|
+
/**
|
|
26
|
+
* Floor on the elbow's horizontal distance from the ring's axis, as a fraction
|
|
27
|
+
* of the leader radius — so a label pushed clear above (or below) the ring still
|
|
28
|
+
* elbows out sideways rather than running straight down the 12 o'clock line.
|
|
29
|
+
*/
|
|
30
|
+
const MIN_ELBOW = 0.18;
|
|
31
|
+
/** Segments used to tessellate a full turn of arc; a wedge takes its share. */
|
|
32
|
+
const ARC_STEPS = 240;
|
|
33
|
+
/** Rounding applied to the leader's elbow, in px. */
|
|
34
|
+
const ELBOW_RADIUS = 8;
|
|
35
|
+
/** Side padding used when nothing is leadered out — no gutter to reserve. */
|
|
36
|
+
const BARE_PAD = 2;
|
|
37
|
+
/** An inside label's caption size, as a fraction of its leading line. */
|
|
38
|
+
const INSIDE_CAPTION_RATIO = 0.44;
|
|
39
|
+
/**
|
|
40
|
+
* Where an inside label sits on a **solid pie** (no hole to split the band
|
|
41
|
+
* with), as a fraction of the wedge's radius — far enough out that the label
|
|
42
|
+
* clears the point every wedge converges on.
|
|
43
|
+
*/
|
|
44
|
+
const PIE_LABEL_RADIUS = 0.62;
|
|
45
|
+
/** Fraction of the hole's diameter its summary is allowed to run to. */
|
|
46
|
+
const CENTER_LABEL_FIT = 0.78;
|
|
47
|
+
/** Fraction of the room inside a wedge that its label is allowed to run to. */
|
|
48
|
+
const INSIDE_LABEL_FIT = 0.9;
|
|
49
|
+
/** Below this, a wedge's bisector counts as axis-aligned and that term stops binding. */
|
|
50
|
+
const AXIS_EPSILON = 1e-3;
|
|
51
|
+
/** Fallback type size when a theme style leaves it unset or on autofit. */
|
|
52
|
+
const FALLBACK_SIZE = 26;
|
|
53
|
+
/** A style's numeric size, guarding the autofit and unset cases. */
|
|
54
|
+
function sizeOf(style, fallback = FALLBACK_SIZE) {
|
|
55
|
+
const size = style.fontSize;
|
|
56
|
+
return typeof size === "number" ? size : fallback;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A ring-band wedge over `a0`→`a1` (radians, clockwise from 12 o'clock) spanning
|
|
60
|
+
* `rInner`→`rOuter`, centred on `cx`. Ring wedges trace the outer arc, step in,
|
|
61
|
+
* and trace the inner arc back; full pie wedges (`rInner <= 0`) close through the
|
|
62
|
+
* centre.
|
|
63
|
+
*
|
|
64
|
+
* `halfGap` is **half the gap to the neighbouring slice, in px**, shaved off both
|
|
65
|
+
* ends of the wedge. It's converted to an angle *per radius* (`halfGap / r` — a
|
|
66
|
+
* wider angle near the hole than out at the rim), which is what keeps the channel
|
|
67
|
+
* between two slices the same width all the way across the band. A fixed angular
|
|
68
|
+
* gap instead reads as a thin pizza slice: right at the rim, pinched shut at the
|
|
69
|
+
* hole. The inset is capped at the wedge's own half-span so a sliver of a slice
|
|
70
|
+
* narrows to nothing rather than turning inside out.
|
|
71
|
+
*/
|
|
72
|
+
function wedgePath(a0, a1, rInner, rOuter, halfGap, cx) {
|
|
73
|
+
const path = new PathBuilder();
|
|
74
|
+
const span = a1 - a0;
|
|
75
|
+
const steps = Math.max(2, Math.ceil((Math.abs(span) / (Math.PI * 2)) * ARC_STEPS));
|
|
76
|
+
/** Angular inset at radius `r` that puts the wedge's edge `halfGap` px off the divide. */
|
|
77
|
+
const insetAt = (r) => r <= 0 ? 0 : Math.min(halfGap / r, span / 2.2);
|
|
78
|
+
const arcTo = (r, i) => {
|
|
79
|
+
const inset = insetAt(r);
|
|
80
|
+
const from = a0 + inset;
|
|
81
|
+
const to = a1 - inset;
|
|
82
|
+
const point = ringPoint(r, from + ((to - from) * i) / steps);
|
|
83
|
+
return { x: point.x + cx, y: point.y };
|
|
84
|
+
};
|
|
85
|
+
if (rInner <= 0) {
|
|
86
|
+
// Solid pie wedge: the sides converge on the centre, so only the outer rim
|
|
87
|
+
// carries the gap.
|
|
88
|
+
path.moveTo(cx, 0);
|
|
89
|
+
for (let i = 0; i <= steps; i++) {
|
|
90
|
+
const p = arcTo(rOuter, i);
|
|
91
|
+
path.lineTo(p.x, p.y);
|
|
92
|
+
}
|
|
93
|
+
path.close();
|
|
94
|
+
return path;
|
|
95
|
+
}
|
|
96
|
+
for (let i = 0; i <= steps; i++) {
|
|
97
|
+
const p = arcTo(rOuter, i);
|
|
98
|
+
if (i === 0)
|
|
99
|
+
path.moveTo(p.x, p.y);
|
|
100
|
+
else
|
|
101
|
+
path.lineTo(p.x, p.y);
|
|
102
|
+
}
|
|
103
|
+
// Back along the inner arc — its own (wider) inset makes each side edge a
|
|
104
|
+
// straight chord sitting `halfGap` px off the divide at both ends.
|
|
105
|
+
for (let i = steps; i >= 0; i--) {
|
|
106
|
+
const p = arcTo(rInner, i);
|
|
107
|
+
path.lineTo(p.x, p.y);
|
|
108
|
+
}
|
|
109
|
+
path.close();
|
|
110
|
+
return path;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The ring itself: one wedge per {@link PieSlice}, sized by its share of the
|
|
114
|
+
* total, swept clockwise from the theme's `startAngle` with a `gap` of clear
|
|
115
|
+
* space between neighbours — a *constant-width* channel, the same at the hole as
|
|
116
|
+
* at the rim (see {@link wedgePath}). A **selected** slice reaches further out
|
|
117
|
+
* while its hole edge stays put — so it reads as that band swelling out of the
|
|
118
|
+
* ring rather than breaking off it — and the rest of the ring dims behind it,
|
|
119
|
+
* leaders and labels included.
|
|
120
|
+
*
|
|
121
|
+
* This is the only part of the chart family that draws a path: the wedges are
|
|
122
|
+
* filled {@link PathBuilder} polygons approximating each arc, and the leaders are
|
|
123
|
+
* stroked polylines — both in the node's centred, y-up space, so neither depends
|
|
124
|
+
* on SVG arc-sweep conventions. The **labels are real {@link Text} nodes**
|
|
125
|
+
* (stacked in a hug vertical flow and pinned by their inner edge), so no
|
|
126
|
+
* glyph-width guesswork decides where a caption starts.
|
|
127
|
+
*
|
|
128
|
+
* ### Label placement
|
|
129
|
+
* Each label sits either **inside** its wedge or **outside** on a leader.
|
|
130
|
+
*
|
|
131
|
+
* An inside label is centred on its slice's band and leads with the number, the
|
|
132
|
+
* caption set small underneath — inside a wedge the reader already knows which
|
|
133
|
+
* slice they're on, so the magnitude is the thing being read. It costs no gutter
|
|
134
|
+
* and no eye round-trip, so a ring labelled that way draws considerably bigger in
|
|
135
|
+
* the same box.
|
|
136
|
+
*
|
|
137
|
+
* A leader label sits outside on the end of a polyline that lifts off its wedge,
|
|
138
|
+
* elbows, and runs horizontally into the text. Labels are split into a left and a
|
|
139
|
+
* right column by which way their slice's bisector points, and each column is
|
|
140
|
+
* de-conflicted independently (see {@link relaxLabels}): a label takes the spot
|
|
141
|
+
* nearest its wedge that still clears its neighbour, and the column is squeezed
|
|
142
|
+
* to fit the node's height rather than spilling out of it. The elbow then rides on
|
|
143
|
+
* a circle just outside the ring **at the label's settled y**, so the run into the
|
|
144
|
+
* text is always horizontal and a nudged label never drags its leader back across
|
|
145
|
+
* the ring.
|
|
146
|
+
*
|
|
147
|
+
* Only the sides that actually carry a leader reserve a gutter, and the ring's
|
|
148
|
+
* centre slides into whatever the other side left over ({@link centerX}) — so a
|
|
149
|
+
* ring whose slices all leader one way doesn't leave a bald patch on the other.
|
|
150
|
+
*/
|
|
151
|
+
export class PieRing extends Node2D {
|
|
152
|
+
/** Each slice's fill, resolved once so `renderSelf` doesn't re-resolve every frame. */
|
|
153
|
+
fills = [];
|
|
154
|
+
constructor(props) {
|
|
155
|
+
super(props);
|
|
156
|
+
this.fills = this.slices.map((slice) => fillOps.resolve(slice.fill));
|
|
157
|
+
// One label per *drawn* slice (a zero-valued slice gets no wedge, so no
|
|
158
|
+
// label either), in whichever of the two forms the chart asked for. Both are
|
|
159
|
+
// real Text stacked in a hug vertical flow — the difference is only what they are
|
|
160
|
+
// pinned to and which line leads.
|
|
161
|
+
//
|
|
162
|
+
// The *point* each label is pinned to stays reactive and follows the ring as
|
|
163
|
+
// the box resizes or its slice pops out; only the choice of form, which
|
|
164
|
+
// depends on props alone, is settled here.
|
|
165
|
+
if (this.labelPlacement !== "none" && this.labelMode !== "none") {
|
|
166
|
+
for (const arc of this.arcs)
|
|
167
|
+
this.addLabel(arc);
|
|
168
|
+
}
|
|
169
|
+
// The hole's summary, pinned to the ring's centre so it tracks the sideways
|
|
170
|
+
// shift a one-sided label column puts on the ring.
|
|
171
|
+
if (this.hasCenterLabel)
|
|
172
|
+
this.addCenterLabel();
|
|
173
|
+
}
|
|
174
|
+
/** Build slice `arc`'s label, in whichever form the chart's placement asks for. */
|
|
175
|
+
addLabel(arc) {
|
|
176
|
+
const slice = this.slices[arc.index];
|
|
177
|
+
// Fades in with the ring's labels, then dims along with its own slice
|
|
178
|
+
// whenever another slice takes the focus.
|
|
179
|
+
const opacity = () => clampUnit(this.labelReveal) * this.sliceOpacity(arc.index);
|
|
180
|
+
const wantsCategory = this.labelMode === "both" || this.labelMode === "category";
|
|
181
|
+
const wantsPercent = this.labelMode === "both" || this.labelMode === "percent";
|
|
182
|
+
if (this.labelPlacement === "inside") {
|
|
183
|
+
// The number leads and the caption sits under it: inside a wedge the
|
|
184
|
+
// reader already knows *which* wedge they are looking at, so the magnitude
|
|
185
|
+
// is what the label is for. Both sizes are bound rather than read once —
|
|
186
|
+
// the fitted size derives from the ring's radius, which isn't known until
|
|
187
|
+
// the node has been laid out.
|
|
188
|
+
const inside = [];
|
|
189
|
+
if (wantsPercent) {
|
|
190
|
+
inside.push(new Text({
|
|
191
|
+
...textOps.styleProps(this.label.insideStyle),
|
|
192
|
+
text: formatPercent(arc.fraction),
|
|
193
|
+
fontSize: () => this.insideFont,
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
196
|
+
if (wantsCategory) {
|
|
197
|
+
inside.push(new Text({
|
|
198
|
+
...textOps.styleProps(this.label.insideStyle),
|
|
199
|
+
text: slice.category,
|
|
200
|
+
fontSize: () => this.labelMode === "category"
|
|
201
|
+
? this.insideFont
|
|
202
|
+
: this.insideFont * INSIDE_CAPTION_RATIO,
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
this.add(new Rect({ flow: "vertical",
|
|
206
|
+
width: "hug",
|
|
207
|
+
height: "hug",
|
|
208
|
+
gap: 0,
|
|
209
|
+
align: "center",
|
|
210
|
+
opacity,
|
|
211
|
+
center: () => this.insideAnchor(arc.index),
|
|
212
|
+
children: inside,
|
|
213
|
+
}));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const side = this.sideOf(arc.mid);
|
|
217
|
+
const at = () => this.placementFor(arc.index)?.text ?? { x: 0, y: 0 };
|
|
218
|
+
const children = [];
|
|
219
|
+
if (wantsCategory) {
|
|
220
|
+
children.push(new Text({
|
|
221
|
+
...textOps.styleProps(this.label.textStyle),
|
|
222
|
+
text: slice.category,
|
|
223
|
+
textAlign: side > 0 ? "left" : "right",
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
if (wantsPercent) {
|
|
227
|
+
children.push(new Text({
|
|
228
|
+
...textOps.styleProps(this.label.percentStyle),
|
|
229
|
+
text: formatPercent(arc.fraction),
|
|
230
|
+
textAlign: side > 0 ? "left" : "right",
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
this.add(new Rect({ flow: "vertical",
|
|
234
|
+
width: "hug",
|
|
235
|
+
height: "hug",
|
|
236
|
+
gap: 2,
|
|
237
|
+
align: side > 0 ? "centerLeft" : "centerRight",
|
|
238
|
+
opacity,
|
|
239
|
+
// Exactly one of these is set; `undefined` anchors are ignored.
|
|
240
|
+
centerLeft: side > 0 ? at : undefined,
|
|
241
|
+
centerRight: side > 0 ? undefined : at,
|
|
242
|
+
children,
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
/** Build the hole's summary block. */
|
|
246
|
+
addCenterLabel() {
|
|
247
|
+
const children = [];
|
|
248
|
+
if (this.centerLabel !== "") {
|
|
249
|
+
children.push(new Text({
|
|
250
|
+
...textOps.styleProps(this.centerStyle.labelStyle),
|
|
251
|
+
text: this.centerLabel,
|
|
252
|
+
fontSize: () => this.centerFont,
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
if (this.centerCaption !== "") {
|
|
256
|
+
children.push(new Text({
|
|
257
|
+
...textOps.styleProps(this.centerStyle.captionStyle),
|
|
258
|
+
text: this.centerCaption,
|
|
259
|
+
fontSize: () => this.centerFont *
|
|
260
|
+
(sizeOf(this.centerStyle.captionStyle, 22) /
|
|
261
|
+
sizeOf(this.centerStyle.labelStyle, 46)),
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
this.add(new Rect({ flow: "vertical",
|
|
265
|
+
width: "hug",
|
|
266
|
+
height: "hug",
|
|
267
|
+
gap: 2,
|
|
268
|
+
align: "center",
|
|
269
|
+
opacity: () => clampUnit(this.centerReveal),
|
|
270
|
+
center: () => ({ x: this.centerX, y: 0 }),
|
|
271
|
+
children,
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
// ---- Arcs --------------------------------------------------------------
|
|
275
|
+
/** The drawn slices' angular spans, re-read every frame so a value tween re-cuts them. */
|
|
276
|
+
get arcs() {
|
|
277
|
+
return pieArcs(this.slices, this.ring.startAngle);
|
|
278
|
+
}
|
|
279
|
+
/** Which side a leader off bisector `mid` runs to: +1 right, −1 left. */
|
|
280
|
+
sideOf(mid) {
|
|
281
|
+
return Math.sin(mid) >= 0 ? 1 : -1;
|
|
282
|
+
}
|
|
283
|
+
/** Whether any slice is labelled on a leader — the only thing that costs a gutter. */
|
|
284
|
+
get hasLeaderLabels() {
|
|
285
|
+
return (this.labelPlacement === "leader" &&
|
|
286
|
+
this.labelMode !== "none" &&
|
|
287
|
+
this.arcs.length > 0);
|
|
288
|
+
}
|
|
289
|
+
// ---- Label sizing ------------------------------------------------------
|
|
290
|
+
/**
|
|
291
|
+
* How wide a horizontal label centred in slice `arc`'s band can run before it
|
|
292
|
+
* leaves the wedge, in px.
|
|
293
|
+
*
|
|
294
|
+
* Two things bound it, and which one binds depends entirely on where the wedge
|
|
295
|
+
* sits. Stepping sideways from the label's centre moves you *radially* by
|
|
296
|
+
* `dx·sin(mid)` and *tangentially* by `dx·cos(mid)`, so a wedge at 3 o'clock is
|
|
297
|
+
* limited by the band's thickness (its label runs straight across the band)
|
|
298
|
+
* while a wedge at 12 o'clock is limited by its own arc (its label runs along
|
|
299
|
+
* the band, which is much roomier). Taking the smaller of the two is what stops
|
|
300
|
+
* a label overflowing the ring on one chart and being pointlessly shrunk on the
|
|
301
|
+
* next.
|
|
302
|
+
*
|
|
303
|
+
* Measured off the *unpopped* radius, so a slice growing out of the ring
|
|
304
|
+
* doesn't resize its own text mid-animation.
|
|
305
|
+
*/
|
|
306
|
+
insideRoom(arc) {
|
|
307
|
+
const outer = this.outerRadius;
|
|
308
|
+
const hole = this.holeRadius;
|
|
309
|
+
const band = hole > 0 ? outer - hole : outer * (1 - PIE_LABEL_RADIUS) * 2;
|
|
310
|
+
const mid = hole > 0 ? (hole + outer) / 2 : outer * PIE_LABEL_RADIUS;
|
|
311
|
+
const sin = Math.abs(Math.sin(arc.mid));
|
|
312
|
+
const cos = Math.abs(Math.cos(arc.mid));
|
|
313
|
+
const acrossBand = sin > AXIS_EPSILON ? band / sin : Infinity;
|
|
314
|
+
const alongArc = cos > AXIS_EPSILON ? ((arc.a1 - arc.a0) * mid) / cos : Infinity;
|
|
315
|
+
return Math.max(0, Math.min(acrossBand, alongArc));
|
|
316
|
+
}
|
|
317
|
+
/** Widest line, in glyphs at the leading line's size, an inside label runs to. */
|
|
318
|
+
insideGlyphs(arc) {
|
|
319
|
+
const category = this.slices[arc.index]?.category.length ?? 0;
|
|
320
|
+
const percent = this.labelMode === "both" || this.labelMode === "percent"
|
|
321
|
+
? formatPercent(arc.fraction).length
|
|
322
|
+
: 0;
|
|
323
|
+
// The caption is set at a fraction of the leading line, except when it *is*
|
|
324
|
+
// the leading line.
|
|
325
|
+
const caption = this.labelMode === "category"
|
|
326
|
+
? category
|
|
327
|
+
: this.labelMode === "both"
|
|
328
|
+
? category * INSIDE_CAPTION_RATIO
|
|
329
|
+
: 0;
|
|
330
|
+
return Math.max(1, percent, caption);
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Font size of an inside label's leading line: what the theme asked for,
|
|
334
|
+
* shrunk if that wouldn't fit inside its wedge. **One size across every inside
|
|
335
|
+
* label** — the smallest any of them needs — because labels at mismatched
|
|
336
|
+
* sizes read as a mistake rather than as emphasis.
|
|
337
|
+
*
|
|
338
|
+
* So the theme's `insideStyle.fontSize` is an upper bound, not a promise: ask
|
|
339
|
+
* for the size the chart wants and the ring keeps it legal.
|
|
340
|
+
*/
|
|
341
|
+
get insideFont() {
|
|
342
|
+
let size = sizeOf(this.label.insideStyle, FALLBACK_SIZE * 1.9);
|
|
343
|
+
for (const arc of this.arcs) {
|
|
344
|
+
const fits = (this.insideRoom(arc) * INSIDE_LABEL_FIT) /
|
|
345
|
+
(this.insideGlyphs(arc) * GLYPH_RATIO);
|
|
346
|
+
size = Math.min(size, fits);
|
|
347
|
+
}
|
|
348
|
+
return Math.max(1, size);
|
|
349
|
+
}
|
|
350
|
+
/** Whether the hole carries a summary at all. */
|
|
351
|
+
get hasCenterLabel() {
|
|
352
|
+
return this.centerLabel !== "" || this.centerCaption !== "";
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Font size of the hole's summary: the configured size, capped so it still
|
|
356
|
+
* fits across the hole. The cap matters because the hole's radius derives from
|
|
357
|
+
* the box, so a chart that reflows smaller would otherwise push its summary out
|
|
358
|
+
* through the ring.
|
|
359
|
+
*/
|
|
360
|
+
get centerFont() {
|
|
361
|
+
const wanted = sizeOf(this.centerStyle.labelStyle, 46);
|
|
362
|
+
const hole = this.holeRadius;
|
|
363
|
+
if (hole <= 0)
|
|
364
|
+
return wanted;
|
|
365
|
+
const captionRatio = sizeOf(this.centerStyle.captionStyle, 22) / Math.max(1, wanted);
|
|
366
|
+
// Both lines have to clear the hole, and the caption is set at a fraction of
|
|
367
|
+
// this size — so what binds is whichever is wider once drawn, not whichever
|
|
368
|
+
// has more characters.
|
|
369
|
+
const glyphs = Math.max(1, this.centerLabel.length, this.centerCaption.length * captionRatio);
|
|
370
|
+
return Math.min(wanted, (hole * 2 * CENTER_LABEL_FIT) / (glyphs * GLYPH_RATIO));
|
|
371
|
+
}
|
|
372
|
+
// ---- Ring geometry, all derived from layoutBounds ------------------------
|
|
373
|
+
/** Rim radius of a fully selected slice as a multiple of the ring's, clamped sane. */
|
|
374
|
+
get growFactor() {
|
|
375
|
+
return Math.max(1, Math.min(1.6, this.ring.selectedGrow));
|
|
376
|
+
}
|
|
377
|
+
/** How far past the ring's rim a fully selected slice reaches, in px. */
|
|
378
|
+
get maxPop() {
|
|
379
|
+
return this.outerRadius * (this.growFactor - 1);
|
|
380
|
+
}
|
|
381
|
+
/** Height of one **leader** label block, in px — the unit those columns are spaced by. */
|
|
382
|
+
get labelRowHeight() {
|
|
383
|
+
// Inside labels are stacked in the band, not in a column beside the ring, so
|
|
384
|
+
// they cost no vertical reserve.
|
|
385
|
+
if (!this.hasLeaderLabels)
|
|
386
|
+
return 0;
|
|
387
|
+
const caption = sizeOf(this.label.textStyle) * 1.25;
|
|
388
|
+
const percent = sizeOf(this.label.percentStyle) * 1.25;
|
|
389
|
+
if (this.labelMode === "both")
|
|
390
|
+
return caption + percent + 2;
|
|
391
|
+
return this.labelMode === "category" ? caption : percent;
|
|
392
|
+
}
|
|
393
|
+
/** Widest line, in glyphs, that a leader label for slice `index` can run to. */
|
|
394
|
+
captionGlyphs(index) {
|
|
395
|
+
// "100%" is the widest a percentage gets; captions are their own length.
|
|
396
|
+
const percent = this.labelMode === "percent" || this.labelMode === "both" ? 4 : 0;
|
|
397
|
+
const category = this.labelMode === "category" || this.labelMode === "both"
|
|
398
|
+
? (this.slices[index]?.category.length ?? 0)
|
|
399
|
+
: 0;
|
|
400
|
+
return Math.max(percent, category);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Horizontal room reserved on each side for the leaders and their labels, in
|
|
404
|
+
* px — measured **per side**, from the slices that actually leader out that
|
|
405
|
+
* way. A ring whose labels are all inside reserves nothing and draws bigger.
|
|
406
|
+
*
|
|
407
|
+
* Excludes the ring's own selection-growth allowance, which scales with the
|
|
408
|
+
* radius and so is divided out in {@link outerRadius} instead.
|
|
409
|
+
*/
|
|
410
|
+
get labelGutters() {
|
|
411
|
+
const gutters = { left: BARE_PAD, right: BARE_PAD };
|
|
412
|
+
if (!this.hasLeaderLabels)
|
|
413
|
+
return gutters;
|
|
414
|
+
const glyph = sizeOf(this.label.textStyle) * GLYPH_RATIO;
|
|
415
|
+
for (const arc of this.arcs) {
|
|
416
|
+
const room = LEADER_LENGTH +
|
|
417
|
+
LABEL_STUB +
|
|
418
|
+
TEXT_INSET +
|
|
419
|
+
this.captionGlyphs(arc.index) * glyph +
|
|
420
|
+
LABEL_PAD;
|
|
421
|
+
const key = this.sideOf(arc.mid) > 0 ? "right" : "left";
|
|
422
|
+
gutters[key] = Math.max(gutters[key], room);
|
|
423
|
+
}
|
|
424
|
+
return gutters;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* How far the ring's centre sits from the node's, in px. Zero while the
|
|
428
|
+
* gutters match; when the labels all fall one way it slides the ring into the
|
|
429
|
+
* empty half so the ring-plus-labels group stays centred in the box instead of
|
|
430
|
+
* the ring sitting dead centre with a bald patch beside it.
|
|
431
|
+
*/
|
|
432
|
+
get centerX() {
|
|
433
|
+
const { left, right } = this.labelGutters;
|
|
434
|
+
return (left - right) / 2;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Radius of the ring's outer edge, fitted to whatever the label gutters leave.
|
|
438
|
+
* A fully selected slice reaches `radius × growFactor`, so the room has to
|
|
439
|
+
* cover that — hence the divisor, which keeps even the grown slice inside the
|
|
440
|
+
* box (and holds the ring the same size whether or not anything is selected).
|
|
441
|
+
*/
|
|
442
|
+
get outerRadius() {
|
|
443
|
+
const { left, right } = this.labelGutters;
|
|
444
|
+
const room = Math.min((this.layoutBounds.width - left - right) / 2, this.layoutBounds.height / 2 - this.labelRowHeight - 4);
|
|
445
|
+
return Math.max(0, room / this.growFactor);
|
|
446
|
+
}
|
|
447
|
+
/** Radius of the hole in the middle: a fraction of the outer radius, or explicit px. */
|
|
448
|
+
get holeRadius() {
|
|
449
|
+
const configured = this.ring.holeRadius;
|
|
450
|
+
if (configured <= 0)
|
|
451
|
+
return 0;
|
|
452
|
+
const px = configured < 1 ? configured * this.outerRadius : configured;
|
|
453
|
+
return Math.min(px, this.outerRadius * 0.95);
|
|
454
|
+
}
|
|
455
|
+
/** Radius of the circle every leader elbows on, clear of even a popped-out slice. */
|
|
456
|
+
get elbowRadius() {
|
|
457
|
+
return this.outerRadius + this.maxPop + LEADER_LENGTH;
|
|
458
|
+
}
|
|
459
|
+
/** How far slice `index` is currently pushed out, in `[0, 1]`. */
|
|
460
|
+
popOf(index) {
|
|
461
|
+
return clampUnit(this.pops[index] ?? 0);
|
|
462
|
+
}
|
|
463
|
+
/** Radius of slice `index`'s outer edge, its selection growth included. */
|
|
464
|
+
anchorRadius(index) {
|
|
465
|
+
return this.outerRadius + this.maxPop * this.popOf(index);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Radius an inside label sits at on slice `index`: halfway across the band on a
|
|
469
|
+
* donut, and further out than halfway on a solid pie, where "halfway" would
|
|
470
|
+
* crowd the point every wedge converges on. Rides the slice's own pop, so an
|
|
471
|
+
* inside label travels out with the wedge it belongs to.
|
|
472
|
+
*/
|
|
473
|
+
bandMidRadius(index) {
|
|
474
|
+
const rim = this.anchorRadius(index);
|
|
475
|
+
const hole = this.holeRadius;
|
|
476
|
+
return hole > 0 ? (hole + rim) / 2 : rim * PIE_LABEL_RADIUS;
|
|
477
|
+
}
|
|
478
|
+
/** Node2D-space point slice `index`'s inside label is centred on. */
|
|
479
|
+
insideAnchor(index) {
|
|
480
|
+
const arc = this.arcs.find((a) => a.index === index);
|
|
481
|
+
if (arc === undefined)
|
|
482
|
+
return { x: this.centerX, y: 0 };
|
|
483
|
+
const point = ringPoint(this.bandMidRadius(index), arc.mid);
|
|
484
|
+
return { x: point.x + this.centerX, y: point.y };
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* How selected the ring is overall, in `[0, 1]` — the most-grown slice. Rides
|
|
488
|
+
* the same tween the pops do, so the dim below arrives with the growth rather
|
|
489
|
+
* than needing an animation of its own.
|
|
490
|
+
*/
|
|
491
|
+
get selectionStrength() {
|
|
492
|
+
let strongest = 0;
|
|
493
|
+
for (let i = 0; i < this.slices.length; i++) {
|
|
494
|
+
strongest = Math.max(strongest, this.popOf(i));
|
|
495
|
+
}
|
|
496
|
+
return strongest;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Opacity slice `index` (and its leader and label) is drawn at: full while
|
|
500
|
+
* nothing is selected, falling toward {@link dimOpacity} in proportion to how
|
|
501
|
+
* selected the *rest* of the ring is. A slice that's selected itself stays at
|
|
502
|
+
* full — the two factors cancel.
|
|
503
|
+
*/
|
|
504
|
+
sliceOpacity(index) {
|
|
505
|
+
const focus = this.selectionStrength;
|
|
506
|
+
if (focus <= 0)
|
|
507
|
+
return 1;
|
|
508
|
+
return 1 - (1 - clampUnit(this.dimOpacity)) * focus * (1 - this.popOf(index));
|
|
509
|
+
}
|
|
510
|
+
// ---- Label placement ---------------------------------------------------
|
|
511
|
+
/**
|
|
512
|
+
* Where every leader label sits and how its leader gets there. Labels are split
|
|
513
|
+
* into a right and a left column by their slice's bisector, each column is
|
|
514
|
+
* spread so nothing overlaps (and nothing leaves the box), and each leader is
|
|
515
|
+
* then built as wedge edge → elbow → label. The elbow rides the leader circle at
|
|
516
|
+
* the label's *settled* y, which keeps the final run horizontal and the whole
|
|
517
|
+
* polyline outside the ring however far the label was nudged.
|
|
518
|
+
*/
|
|
519
|
+
placements() {
|
|
520
|
+
if (!this.hasLeaderLabels)
|
|
521
|
+
return [];
|
|
522
|
+
const arcs = this.arcs;
|
|
523
|
+
if (arcs.length === 0 || this.outerRadius <= 0)
|
|
524
|
+
return [];
|
|
525
|
+
const radius = this.elbowRadius;
|
|
526
|
+
const rowHeight = this.labelRowHeight;
|
|
527
|
+
const top = this.layoutBounds.height / 2 - rowHeight / 2;
|
|
528
|
+
const bottom = -top;
|
|
529
|
+
const cx = this.centerX;
|
|
530
|
+
const right = [];
|
|
531
|
+
const left = [];
|
|
532
|
+
for (const arc of arcs) {
|
|
533
|
+
const side = this.sideOf(arc.mid);
|
|
534
|
+
const ideal = ringPoint(radius, arc.mid).y;
|
|
535
|
+
(side > 0 ? right : left).push({
|
|
536
|
+
index: arc.index,
|
|
537
|
+
side,
|
|
538
|
+
mid: arc.mid,
|
|
539
|
+
ideal,
|
|
540
|
+
y: ideal,
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
relaxLabels(right, rowHeight + LABEL_GAP, top, bottom);
|
|
544
|
+
relaxLabels(left, rowHeight + LABEL_GAP, top, bottom);
|
|
545
|
+
return [...right, ...left].map((slot) => {
|
|
546
|
+
const start = ringPoint(this.anchorRadius(slot.index) + LEADER_LIFTOFF, slot.mid);
|
|
547
|
+
// The elbow sits where the label's y crosses the leader circle. Solving
|
|
548
|
+
// x² + y² = radius² for x keeps it *on* that circle (never inside the
|
|
549
|
+
// ring); a label pushed past the circle's top or bottom falls back to the
|
|
550
|
+
// sideways floor so its leader still reads as pointing outward.
|
|
551
|
+
const onCircle = Math.sqrt(Math.max(0, radius * radius - slot.y * slot.y));
|
|
552
|
+
const elbowX = cx + slot.side * Math.max(onCircle, radius * MIN_ELBOW);
|
|
553
|
+
const endX = cx + slot.side * (radius + LABEL_STUB);
|
|
554
|
+
return {
|
|
555
|
+
index: slot.index,
|
|
556
|
+
side: slot.side,
|
|
557
|
+
leader: [
|
|
558
|
+
{ x: start.x + cx, y: start.y },
|
|
559
|
+
{ x: elbowX, y: slot.y },
|
|
560
|
+
{ x: endX, y: slot.y },
|
|
561
|
+
],
|
|
562
|
+
text: { x: endX + slot.side * TEXT_INSET, y: slot.y },
|
|
563
|
+
};
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
/** This frame's placement for slice `index`, or `undefined` if it isn't leadered. */
|
|
567
|
+
placementFor(index) {
|
|
568
|
+
return this.placements().find((placement) => placement.index === index);
|
|
569
|
+
}
|
|
570
|
+
// ---- Draw --------------------------------------------------------------
|
|
571
|
+
/**
|
|
572
|
+
* The ring's paints live nowhere `ShapeNode` would look: one fill per slice,
|
|
573
|
+
* resolved once into {@link fills}, plus the leader line's own stroke. Both are
|
|
574
|
+
* passed explicitly — see {@link declarePaints}.
|
|
575
|
+
*/
|
|
576
|
+
declareAssets(assets) {
|
|
577
|
+
super.declareAssets(assets);
|
|
578
|
+
declarePaints(this, assets, {
|
|
579
|
+
fills: this.fills,
|
|
580
|
+
strokes: [strokeOps.resolve(this.label.leaderStroke)],
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
renderSelf(ctx) {
|
|
584
|
+
const rOuter = this.outerRadius;
|
|
585
|
+
if (rOuter <= 0)
|
|
586
|
+
return;
|
|
587
|
+
const arcs = this.arcs;
|
|
588
|
+
if (arcs.length === 0)
|
|
589
|
+
return;
|
|
590
|
+
const sweep = clampUnit(this.growth);
|
|
591
|
+
const revealEnd = this.ring.startAngle * (Math.PI / 180) + Math.PI * 2 * sweep;
|
|
592
|
+
const hole = this.holeRadius;
|
|
593
|
+
const halfGap = Math.max(0, this.ring.gap) / 2;
|
|
594
|
+
// One shared frame (the whole ring square, grown slices included) pinned onto
|
|
595
|
+
// every wedge path, so `Graphics2D.path()` doesn't re-centre each wedge on its
|
|
596
|
+
// own bbox and collapse them all onto the origin. It stays symmetric about the
|
|
597
|
+
// *node's* origin — so path coordinates land exactly in node space — and is
|
|
598
|
+
// grown by the ring's sideways offset rather than moved with it, which would
|
|
599
|
+
// undo the very shift it has to contain.
|
|
600
|
+
const cx = this.centerX;
|
|
601
|
+
const half = rOuter + this.maxPop + 1 + Math.abs(cx);
|
|
602
|
+
const centerBounds = [
|
|
603
|
+
-half,
|
|
604
|
+
-half,
|
|
605
|
+
half,
|
|
606
|
+
half,
|
|
607
|
+
];
|
|
608
|
+
// Explicit width/height alongside centerBounds: a wedge path's command list
|
|
609
|
+
// carries no implied extent, so a slice filled with an image or a video needs
|
|
610
|
+
// this to size its decode.
|
|
611
|
+
const span = half * 2;
|
|
612
|
+
for (const arc of arcs) {
|
|
613
|
+
// Clip the wedge to the growth-revealed arc; skip it until the sweep
|
|
614
|
+
// reaches it, so the ring draws in as one clockwise wipe.
|
|
615
|
+
const a1 = Math.min(arc.a1, revealEnd);
|
|
616
|
+
if (a1 <= arc.a0)
|
|
617
|
+
continue;
|
|
618
|
+
const path = wedgePath(arc.a0, a1, hole, this.anchorRadius(arc.index), halfGap, cx);
|
|
619
|
+
const g = new Graphics2D().path(path.toPathState({ centerBounds, width: span, height: span }));
|
|
620
|
+
g.shadow(this.ring.shadow);
|
|
621
|
+
g.fill(this.fills[arc.index] ?? []);
|
|
622
|
+
g.stroke(this.ring.stroke);
|
|
623
|
+
ctx.draw(g.opacity(this.sliceOpacity(arc.index)));
|
|
624
|
+
}
|
|
625
|
+
// Leaders. Each polyline gets its own `Graphics2D`: several open polylines
|
|
626
|
+
// chained into one command list are combined into a single surface before the
|
|
627
|
+
// stroke, which closes each contour back on itself and paints the leaders as
|
|
628
|
+
// thin triangles.
|
|
629
|
+
const reveal = clampUnit(this.labelReveal);
|
|
630
|
+
if (reveal <= 0.001)
|
|
631
|
+
return;
|
|
632
|
+
for (const placement of this.placements()) {
|
|
633
|
+
ctx.draw(new Graphics2D()
|
|
634
|
+
.line({ points: placement.leader, radius: ELBOW_RADIUS })
|
|
635
|
+
.stroke(this.label.leaderStroke)
|
|
636
|
+
.opacity(reveal * this.sliceOpacity(placement.index)));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// ---- Entrance ----------------------------------------------------------
|
|
640
|
+
/** Tween {@link growth} `0 → 1`, wiping the wedges in around the ring. */
|
|
641
|
+
sweep(args) {
|
|
642
|
+
// No priming to 0 first — the value at `t` is a function of `t`, so a repeat
|
|
643
|
+
// call re-sweeps by definition rather than by having reset something.
|
|
644
|
+
return this.command((t) => ({ growth: t }), args.duration, args.easing ?? easeOut());
|
|
645
|
+
}
|
|
646
|
+
/** Tween {@link labelReveal} `0 → 1`, fading the leaders and captions up. */
|
|
647
|
+
revealLabels(args) {
|
|
648
|
+
return this.command((t) => ({ labelReveal: t }), args.duration, args.easing ?? easeOut());
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Fade the hole's summary up. Deliberately not part of the ring's entrance: a
|
|
652
|
+
* hole label is usually the chart's conclusion, so the scene decides when it
|
|
653
|
+
* lands.
|
|
654
|
+
*/
|
|
655
|
+
revealCenter(args = {}) {
|
|
656
|
+
const from = clampUnit(this.centerReveal);
|
|
657
|
+
return this.command((t) => ({ centerReveal: from + (1 - from) * t }), args.duration ?? 0.6, args.easing ?? easeOut());
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
__decorate([
|
|
661
|
+
property({ default: () => [] })
|
|
662
|
+
], PieRing.prototype, "slices", void 0);
|
|
663
|
+
__decorate([
|
|
664
|
+
property({ default: DEFAULT_PIE_CHART_THEME.ring })
|
|
665
|
+
], PieRing.prototype, "ring", void 0);
|
|
666
|
+
__decorate([
|
|
667
|
+
property({ default: DEFAULT_PIE_CHART_THEME.label })
|
|
668
|
+
], PieRing.prototype, "label", void 0);
|
|
669
|
+
__decorate([
|
|
670
|
+
property({ default: DEFAULT_PIE_CHART_THEME.center })
|
|
671
|
+
], PieRing.prototype, "centerStyle", void 0);
|
|
672
|
+
__decorate([
|
|
673
|
+
property({ default: "percent" })
|
|
674
|
+
], PieRing.prototype, "labelMode", void 0);
|
|
675
|
+
__decorate([
|
|
676
|
+
property({ default: "leader" })
|
|
677
|
+
], PieRing.prototype, "labelPlacement", void 0);
|
|
678
|
+
__decorate([
|
|
679
|
+
property({ default: 1 })
|
|
680
|
+
], PieRing.prototype, "growth", void 0);
|
|
681
|
+
__decorate([
|
|
682
|
+
property({ default: 1 })
|
|
683
|
+
], PieRing.prototype, "labelReveal", void 0);
|
|
684
|
+
__decorate([
|
|
685
|
+
property({ default: () => [] })
|
|
686
|
+
], PieRing.prototype, "pops", void 0);
|
|
687
|
+
__decorate([
|
|
688
|
+
property({ default: DEFAULT_PIE_CHART_THEME.spotlight.dimOpacity })
|
|
689
|
+
], PieRing.prototype, "dimOpacity", void 0);
|
|
690
|
+
__decorate([
|
|
691
|
+
property({ default: "" })
|
|
692
|
+
], PieRing.prototype, "centerLabel", void 0);
|
|
693
|
+
__decorate([
|
|
694
|
+
property({ default: "" })
|
|
695
|
+
], PieRing.prototype, "centerCaption", void 0);
|
|
696
|
+
__decorate([
|
|
697
|
+
property({ default: 1 })
|
|
698
|
+
], PieRing.prototype, "centerReveal", void 0);
|
|
699
|
+
__decorate([
|
|
700
|
+
command()
|
|
701
|
+
], PieRing.prototype, "sweep", null);
|
|
702
|
+
__decorate([
|
|
703
|
+
command()
|
|
704
|
+
], PieRing.prototype, "revealLabels", null);
|
|
705
|
+
__decorate([
|
|
706
|
+
command()
|
|
707
|
+
], PieRing.prototype, "revealCenter", null);
|
|
708
|
+
//# sourceMappingURL=pie-ring.js.map
|