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