@motionscript/charts 0.1.1-alpha.0 → 0.1.1-alpha.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.
- package/CHANGELOG.md +34 -0
- package/README.md +109 -28
- package/dist/bar-chart/bar-chart.d.ts +265 -39
- package/dist/bar-chart/bar-chart.d.ts.map +1 -1
- package/dist/bar-chart/bar-chart.js +219 -171
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/category-axis.d.ts +12 -7
- package/dist/bar-chart/category-axis.d.ts.map +1 -1
- package/dist/bar-chart/category-axis.js +56 -42
- package/dist/bar-chart/category-axis.js.map +1 -1
- package/dist/bar-chart/chart-bars.d.ts +76 -52
- package/dist/bar-chart/chart-bars.d.ts.map +1 -1
- package/dist/bar-chart/chart-bars.js +105 -89
- package/dist/bar-chart/chart-bars.js.map +1 -1
- package/dist/bar-chart/index.d.ts +4 -4
- package/dist/bar-chart/index.d.ts.map +1 -1
- package/dist/bar-chart/index.js +2 -2
- package/dist/bar-chart/index.js.map +1 -1
- package/dist/bar-chart/shared.d.ts +41 -65
- package/dist/bar-chart/shared.d.ts.map +1 -1
- package/dist/bar-chart/shared.js +73 -117
- package/dist/bar-chart/shared.js.map +1 -1
- package/dist/browser/chunks/chunk-7XCRX6GE.js +2 -0
- package/dist/browser/chunks/chunk-7XCRX6GE.js.map +7 -0
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +4 -4
- package/dist/browser/kit.js +1 -1
- package/dist/browser/manifest.json +1 -1
- package/dist/engine.d.ts +5 -2
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +5 -2
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/kit/cartesian.d.ts +51 -15
- package/dist/kit/cartesian.d.ts.map +1 -1
- package/dist/kit/cartesian.js +81 -29
- package/dist/kit/cartesian.js.map +1 -1
- package/dist/kit/chart-canvas.d.ts +32 -24
- package/dist/kit/chart-canvas.d.ts.map +1 -1
- package/dist/kit/chart-canvas.js +52 -46
- package/dist/kit/chart-canvas.js.map +1 -1
- package/dist/kit/chart-legend.d.ts +8 -4
- package/dist/kit/chart-legend.d.ts.map +1 -1
- package/dist/kit/chart-legend.js +30 -20
- package/dist/kit/chart-legend.js.map +1 -1
- package/dist/{line-chart → kit}/horizontal-axis.d.ts +31 -6
- package/dist/kit/horizontal-axis.d.ts.map +1 -0
- package/dist/{line-chart → kit}/horizontal-axis.js +23 -24
- package/dist/kit/horizontal-axis.js.map +1 -0
- package/dist/kit/motion.d.ts +5 -0
- package/dist/kit/motion.d.ts.map +1 -1
- package/dist/kit/motion.js +8 -0
- package/dist/kit/motion.js.map +1 -1
- package/dist/{pie-chart → kit}/pie-legend.d.ts +3 -3
- package/dist/kit/pie-legend.d.ts.map +1 -0
- package/dist/{pie-chart → kit}/pie-legend.js +11 -10
- package/dist/kit/pie-legend.js.map +1 -0
- package/dist/kit/polar.d.ts +31 -0
- package/dist/kit/polar.d.ts.map +1 -0
- package/dist/kit/polar.js +33 -0
- package/dist/kit/polar.js.map +1 -0
- package/dist/kit/shared.d.ts +11 -50
- package/dist/kit/shared.d.ts.map +1 -1
- package/dist/kit/shared.js +58 -141
- package/dist/kit/shared.js.map +1 -1
- package/dist/kit/style.d.ts +517 -0
- package/dist/kit/style.d.ts.map +1 -0
- package/dist/kit/style.js +292 -0
- package/dist/kit/style.js.map +1 -0
- package/dist/kit/vertical-axis.d.ts +22 -2
- package/dist/kit/vertical-axis.d.ts.map +1 -1
- package/dist/kit/vertical-axis.js +56 -43
- package/dist/kit/vertical-axis.js.map +1 -1
- package/dist/line-chart/chart-line.d.ts +95 -3
- package/dist/line-chart/chart-line.d.ts.map +1 -1
- package/dist/line-chart/chart-line.js +165 -53
- package/dist/line-chart/chart-line.js.map +1 -1
- package/dist/line-chart/chart-region.d.ts +1 -1
- package/dist/line-chart/chart-region.d.ts.map +1 -1
- package/dist/line-chart/chart-region.js +7 -8
- package/dist/line-chart/chart-region.js.map +1 -1
- package/dist/line-chart/index.d.ts +5 -6
- package/dist/line-chart/index.d.ts.map +1 -1
- package/dist/line-chart/index.js +3 -4
- package/dist/line-chart/index.js.map +1 -1
- package/dist/line-chart/line-chart.d.ts +281 -18
- package/dist/line-chart/line-chart.d.ts.map +1 -1
- package/dist/line-chart/line-chart.js +284 -134
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/line-chart/shared.d.ts +51 -34
- package/dist/line-chart/shared.d.ts.map +1 -1
- package/dist/line-chart/shared.js +175 -83
- package/dist/line-chart/shared.js.map +1 -1
- package/dist/line-chart/x-scale.d.ts +2 -0
- package/dist/line-chart/x-scale.d.ts.map +1 -1
- package/dist/line-chart/x-scale.js +15 -0
- package/dist/line-chart/x-scale.js.map +1 -1
- package/dist/nodes.d.ts +3 -1
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +9 -1
- package/dist/nodes.js.map +1 -1
- package/dist/pie-chart/index.d.ts +2 -3
- package/dist/pie-chart/index.d.ts.map +1 -1
- package/dist/pie-chart/index.js +1 -2
- package/dist/pie-chart/index.js.map +1 -1
- package/dist/pie-chart/pie-chart.d.ts +114 -18
- package/dist/pie-chart/pie-chart.d.ts.map +1 -1
- package/dist/pie-chart/pie-chart.js +38 -24
- package/dist/pie-chart/pie-chart.js.map +1 -1
- package/dist/pie-chart/pie-ring.d.ts +16 -5
- package/dist/pie-chart/pie-ring.d.ts.map +1 -1
- package/dist/pie-chart/pie-ring.js +89 -55
- package/dist/pie-chart/pie-ring.js.map +1 -1
- package/dist/pie-chart/shared.d.ts +61 -62
- package/dist/pie-chart/shared.d.ts.map +1 -1
- package/dist/pie-chart/shared.js +44 -123
- package/dist/pie-chart/shared.js.map +1 -1
- package/dist/radar-chart/index.d.ts +5 -0
- package/dist/radar-chart/index.d.ts.map +1 -0
- package/dist/radar-chart/index.js +5 -0
- package/dist/radar-chart/index.js.map +1 -0
- package/dist/radar-chart/radar-chart.d.ts +212 -0
- package/dist/radar-chart/radar-chart.d.ts.map +1 -0
- package/dist/radar-chart/radar-chart.js +224 -0
- package/dist/radar-chart/radar-chart.js.map +1 -0
- package/dist/radar-chart/radar-grid.d.ts +49 -0
- package/dist/radar-chart/radar-grid.d.ts.map +1 -0
- package/dist/radar-chart/radar-grid.js +63 -0
- package/dist/radar-chart/radar-grid.js.map +1 -0
- package/dist/radar-chart/radar-series.d.ts +59 -0
- package/dist/radar-chart/radar-series.d.ts.map +1 -0
- package/dist/radar-chart/radar-series.js +71 -0
- package/dist/radar-chart/radar-series.js.map +1 -0
- package/dist/radar-chart/shared.d.ts +78 -0
- package/dist/radar-chart/shared.d.ts.map +1 -0
- package/dist/radar-chart/shared.js +129 -0
- package/dist/radar-chart/shared.js.map +1 -0
- package/dist/radial-chart/index.d.ts +6 -0
- package/dist/radial-chart/index.d.ts.map +1 -0
- package/dist/radial-chart/index.js +4 -0
- package/dist/radial-chart/index.js.map +1 -0
- package/dist/radial-chart/radial-chart.d.ts +210 -0
- package/dist/radial-chart/radial-chart.d.ts.map +1 -0
- package/dist/radial-chart/radial-chart.js +175 -0
- package/dist/radial-chart/radial-chart.js.map +1 -0
- package/dist/radial-chart/radial-rings.d.ts +121 -0
- package/dist/radial-chart/radial-rings.d.ts.map +1 -0
- package/dist/radial-chart/radial-rings.js +273 -0
- package/dist/radial-chart/radial-rings.js.map +1 -0
- package/dist/radial-chart/shared.d.ts +77 -0
- package/dist/radial-chart/shared.d.ts.map +1 -0
- package/dist/radial-chart/shared.js +78 -0
- package/dist/radial-chart/shared.js.map +1 -0
- package/package.json +4 -4
- package/registry.json +21 -6
- package/src/bar-chart/bar-chart.ts +263 -179
- package/src/bar-chart/category-axis.ts +46 -40
- package/src/bar-chart/chart-bars.ts +136 -117
- package/src/bar-chart/index.ts +7 -10
- package/src/bar-chart/shared.ts +88 -233
- package/src/engine.ts +5 -2
- package/src/index.ts +2 -0
- package/src/kit/cartesian.ts +121 -40
- package/src/kit/chart-canvas.ts +49 -54
- package/src/kit/chart-legend.ts +28 -20
- package/src/kit/horizontal-axis.ts +114 -0
- package/src/kit/motion.ts +7 -0
- package/src/{pie-chart → kit}/pie-legend.ts +12 -11
- package/src/kit/polar.ts +54 -0
- package/src/kit/shared.ts +55 -277
- package/src/kit/style.ts +353 -0
- package/src/kit/vertical-axis.ts +67 -48
- package/src/line-chart/chart-line.ts +176 -54
- package/src/line-chart/chart-region.ts +6 -10
- package/src/line-chart/index.ts +14 -10
- package/src/line-chart/line-chart.ts +353 -151
- package/src/line-chart/shared.ts +193 -174
- package/src/line-chart/x-scale.ts +13 -0
- package/src/nodes.ts +9 -1
- package/src/pie-chart/index.ts +2 -23
- package/src/pie-chart/pie-chart.ts +54 -35
- package/src/pie-chart/pie-ring.ts +92 -71
- package/src/pie-chart/shared.ts +80 -270
- package/src/radar-chart/index.ts +4 -0
- package/src/radar-chart/radar-chart.ts +290 -0
- package/src/radar-chart/radar-grid.ts +83 -0
- package/src/radar-chart/radar-series.ts +88 -0
- package/src/radar-chart/shared.ts +153 -0
- package/src/radial-chart/index.ts +5 -0
- package/src/radial-chart/radial-chart.ts +212 -0
- package/src/radial-chart/radial-rings.ts +328 -0
- package/src/radial-chart/shared.ts +126 -0
- package/dist/browser/chunks/chunk-4SMRCU24.js +0 -2
- package/dist/browser/chunks/chunk-4SMRCU24.js.map +0 -7
- package/dist/kit/chart-border.d.ts +0 -18
- package/dist/kit/chart-border.d.ts.map +0 -1
- package/dist/kit/chart-border.js +0 -23
- package/dist/kit/chart-border.js.map +0 -1
- package/dist/line-chart/horizontal-axis.d.ts.map +0 -1
- package/dist/line-chart/horizontal-axis.js.map +0 -1
- package/dist/pie-chart/pie-legend.d.ts.map +0 -1
- package/dist/pie-chart/pie-legend.js.map +0 -1
- package/src/kit/chart-border.ts +0 -36
- package/src/line-chart/horizontal-axis.ts +0 -122
package/src/line-chart/shared.ts
CHANGED
|
@@ -1,41 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
lerpNumber,
|
|
5
|
-
strokeOps,
|
|
6
|
-
type DataRecord,
|
|
7
|
-
type Fill,
|
|
8
|
-
type FillResolved,
|
|
9
|
-
type Stroke,
|
|
10
|
-
type StrokeResolved,
|
|
11
|
-
type Vector2,
|
|
12
|
-
} from "@motionscript/sdk"
|
|
13
|
-
import { niceStep } from "@motionscript/sdk/component"
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
CHART_PALETTE,
|
|
17
|
-
DEFAULT_LEGEND,
|
|
18
|
-
DEFAULT_PLOT_AREA,
|
|
19
|
-
DEFAULT_SERIES_STROKES,
|
|
20
|
-
DEFAULT_X_AXIS,
|
|
21
|
-
DEFAULT_Y_AXIS,
|
|
22
|
-
SPOTLIGHT_DIM,
|
|
23
|
-
lerpAxisConfig,
|
|
24
|
-
lerpLegendConfig,
|
|
25
|
-
lerpPlotArea,
|
|
26
|
-
lerpSeriesStrokes,
|
|
27
|
-
num,
|
|
28
|
-
resolveAxisConfig,
|
|
29
|
-
resolveLegendConfig,
|
|
30
|
-
resolvePlotArea,
|
|
31
|
-
resolveSeriesStrokes,
|
|
32
|
-
type AxisConfig,
|
|
33
|
-
type ChartSeries,
|
|
34
|
-
type LegendConfig,
|
|
35
|
-
type LegendTheme,
|
|
36
|
-
type PlotAreaConfig,
|
|
37
|
-
type Range,
|
|
38
|
-
} from "../kit/shared"
|
|
1
|
+
import { Types, type DataRecord, type Vector2 } from "@motionscript/sdk"
|
|
2
|
+
|
|
3
|
+
import { niceRange, num, type ChartSeries, type Range } from "../kit/shared"
|
|
39
4
|
import type { XScale } from "./x-scale"
|
|
40
5
|
|
|
41
6
|
/** One polyline of a {@link LineChart}: {@link ChartSeries} under the name for what it draws here. */
|
|
@@ -198,129 +163,6 @@ function clipSegment(
|
|
|
198
163
|
]
|
|
199
164
|
}
|
|
200
165
|
|
|
201
|
-
/** How a spotlit {@link PlotRegion} is drawn: rules on its bounded edges, a wash, and how far series fade outside it. */
|
|
202
|
-
export interface RegionConfig {
|
|
203
|
-
/** Marks each bounded edge. */
|
|
204
|
-
stroke: Stroke
|
|
205
|
-
/** Drawn under the series lines. */
|
|
206
|
-
fill: Fill
|
|
207
|
-
/** Opacity the stretch of every series outside the region fades to. */
|
|
208
|
-
dimOpacity: number
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** A {@link LineChart}'s full, resolved visual theme. */
|
|
212
|
-
export interface ChartConfig {
|
|
213
|
-
/** Cycled by series index; a series' own `fill` overrides just the colour. */
|
|
214
|
-
seriesStyles: Stroke[]
|
|
215
|
-
xAxis: AxisConfig
|
|
216
|
-
yAxis: AxisConfig
|
|
217
|
-
plotArea: PlotAreaConfig
|
|
218
|
-
legend: LegendConfig
|
|
219
|
-
region: RegionConfig
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/** The author's partial theme, deep-merged onto {@link DEFAULT_CHART_THEME}. */
|
|
223
|
-
export type ChartTheme = {
|
|
224
|
-
seriesStyles?: Stroke[]
|
|
225
|
-
xAxis?: Partial<AxisConfig>
|
|
226
|
-
yAxis?: Partial<AxisConfig>
|
|
227
|
-
plotArea?: Partial<PlotAreaConfig>
|
|
228
|
-
legend?: LegendTheme
|
|
229
|
-
region?: Partial<RegionConfig>
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** The shared legend with a small square swatch: a line's swatch stands for a colour, where a bar's or wedge's stands for an area of paint. */
|
|
233
|
-
const LINE_CHART_LEGEND: LegendConfig = {
|
|
234
|
-
...DEFAULT_LEGEND,
|
|
235
|
-
markerStyle: {
|
|
236
|
-
...DEFAULT_LEGEND.markerStyle,
|
|
237
|
-
width: 20,
|
|
238
|
-
height: 20,
|
|
239
|
-
borderRadius: 6,
|
|
240
|
-
},
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export const DEFAULT_CHART_THEME: ChartConfig = {
|
|
244
|
-
seriesStyles: DEFAULT_SERIES_STROKES,
|
|
245
|
-
xAxis: DEFAULT_X_AXIS,
|
|
246
|
-
yAxis: DEFAULT_Y_AXIS,
|
|
247
|
-
plotArea: DEFAULT_PLOT_AREA,
|
|
248
|
-
legend: LINE_CHART_LEGEND,
|
|
249
|
-
region: {
|
|
250
|
-
// Brighter and dashed so it reads as a call-out, not an axis line; `butt` caps keep dash lengths exact.
|
|
251
|
-
stroke: {
|
|
252
|
-
weight: 3,
|
|
253
|
-
fill: CHART_PALETTE.regionColor,
|
|
254
|
-
dash: [14, 12],
|
|
255
|
-
cap: "butt",
|
|
256
|
-
},
|
|
257
|
-
fill: Fills.color(CHART_PALETTE.regionColor, { opacity: 0.08 }),
|
|
258
|
-
dimOpacity: SPOTLIGHT_DIM,
|
|
259
|
-
},
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function resolveRegionConfig(
|
|
263
|
-
region: Partial<RegionConfig> | undefined,
|
|
264
|
-
previous: RegionConfig
|
|
265
|
-
): RegionConfig {
|
|
266
|
-
return {
|
|
267
|
-
stroke: strokeOps.resolve(region?.stroke ?? previous.stroke),
|
|
268
|
-
fill: fillOps.resolve(region?.fill ?? previous.fill),
|
|
269
|
-
dimOpacity: region?.dimOpacity ?? previous.dimOpacity,
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function lerpRegionConfig(
|
|
274
|
-
from: RegionConfig,
|
|
275
|
-
to: RegionConfig,
|
|
276
|
-
t: number
|
|
277
|
-
): RegionConfig {
|
|
278
|
-
return {
|
|
279
|
-
stroke: strokeOps.lerp(
|
|
280
|
-
from.stroke as StrokeResolved[],
|
|
281
|
-
to.stroke as StrokeResolved[],
|
|
282
|
-
t
|
|
283
|
-
),
|
|
284
|
-
fill: fillOps.lerp(
|
|
285
|
-
from.fill as FillResolved[],
|
|
286
|
-
to.fill as FillResolved[],
|
|
287
|
-
t
|
|
288
|
-
),
|
|
289
|
-
dimOpacity: lerpNumber(from.dimOpacity, to.dimOpacity, t),
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** The `theme` entry's resolve: merges a partial {@link ChartTheme} onto `previous` (first resolution: the default). */
|
|
294
|
-
export function resolveChartTheme(
|
|
295
|
-
theme: ChartTheme | undefined,
|
|
296
|
-
previous?: ChartConfig
|
|
297
|
-
): ChartConfig {
|
|
298
|
-
const base = previous ?? DEFAULT_CHART_THEME
|
|
299
|
-
return {
|
|
300
|
-
seriesStyles: resolveSeriesStrokes(theme?.seriesStyles, base.seriesStyles),
|
|
301
|
-
xAxis: resolveAxisConfig(theme?.xAxis, base.xAxis),
|
|
302
|
-
yAxis: resolveAxisConfig(theme?.yAxis, base.yAxis),
|
|
303
|
-
plotArea: resolvePlotArea(theme?.plotArea, base.plotArea),
|
|
304
|
-
legend: resolveLegendConfig(theme?.legend, base.legend),
|
|
305
|
-
region: resolveRegionConfig(theme?.region, base.region),
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export function lerpChartTheme(
|
|
310
|
-
from: ChartConfig,
|
|
311
|
-
to: ChartConfig,
|
|
312
|
-
t: number
|
|
313
|
-
): ChartConfig {
|
|
314
|
-
return {
|
|
315
|
-
seriesStyles: lerpSeriesStrokes(from.seriesStyles, to.seriesStyles, t),
|
|
316
|
-
xAxis: lerpAxisConfig(from.xAxis, to.xAxis, t),
|
|
317
|
-
yAxis: lerpAxisConfig(from.yAxis, to.yAxis, t),
|
|
318
|
-
plotArea: lerpPlotArea(from.plotArea, to.plotArea, t),
|
|
319
|
-
legend: lerpLegendConfig(from.legend, to.legend, t),
|
|
320
|
-
region: lerpRegionConfig(from.region, to.region, t),
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
166
|
/**
|
|
325
167
|
* One series' `[x, y]` points, sorted along x so the polyline traces left to
|
|
326
168
|
* right. A row missing its y contributes 0 rather than a gap, since dropping it
|
|
@@ -359,21 +201,198 @@ export function resolveYScale(
|
|
|
359
201
|
range: Range | undefined,
|
|
360
202
|
divisions: number
|
|
361
203
|
): Range {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
204
|
+
return range ?? niceRange(yExtent(data, series), divisions)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** How the series sit on one another: apart, each on the sum of those before it, or as shares of the row's total. */
|
|
208
|
+
export type LineStack = "none" | "stacked" | "percent"
|
|
209
|
+
|
|
210
|
+
/** One series as drawn: its `[x, y]` points along x, and the line its area fills down to (`null`: the baseline). */
|
|
211
|
+
export interface StackedSeries {
|
|
212
|
+
readonly points: [x: number, y: number][]
|
|
213
|
+
readonly base: [x: number, y: number][] | null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Every series' points under `stack`, rows in x order. A series contributes its
|
|
218
|
+
* values times its weight (`1` when missing), so a series sinking out of a stack
|
|
219
|
+
* lowers the ones above it. `percent` reads each row as shares of its total
|
|
220
|
+
* magnitude, so the top of a full stack is 100.
|
|
221
|
+
*/
|
|
222
|
+
export function stackSeries(
|
|
223
|
+
data: DataRecord[],
|
|
224
|
+
x: XScale,
|
|
225
|
+
series: readonly LineSeries[],
|
|
226
|
+
stack: LineStack,
|
|
227
|
+
weights: readonly number[] = []
|
|
228
|
+
): StackedSeries[] {
|
|
229
|
+
const order = data.map((_, i) => i).sort((a, b) => x.at(a) - x.at(b))
|
|
230
|
+
if (stack !== "stacked" && stack !== "percent") {
|
|
231
|
+
return series.map((s) => ({ points: order.map((r): [number, number] => [x.at(r), num(data[r], s.field)]), base: null }))
|
|
232
|
+
}
|
|
233
|
+
const weight = (i: number) => Math.max(0, weights[i] ?? 1)
|
|
234
|
+
const totals = order.map((r) => series.reduce((sum, s, i) => sum + Math.abs(num(data[r], s.field) * weight(i)), 0))
|
|
235
|
+
const running = order.map(() => 0)
|
|
236
|
+
return series.map((s, i) => {
|
|
237
|
+
const base = order.map((r, k): [number, number] => [x.at(r), running[k]])
|
|
238
|
+
const points = order.map((r, k): [number, number] => {
|
|
239
|
+
const value = num(data[r], s.field) * weight(i)
|
|
240
|
+
running[k] += stack === "percent" ? (totals[k] > 0 ? (value / totals[k]) * 100 : 0) : value
|
|
241
|
+
return [x.at(r), running[k]]
|
|
242
|
+
})
|
|
243
|
+
return { points, base }
|
|
244
|
+
})
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The lowest and highest y anything in `stacks` reaches, its bases included. */
|
|
248
|
+
export function stackExtent(stacks: readonly StackedSeries[]): Range {
|
|
249
|
+
let lo = Infinity
|
|
250
|
+
let hi = -Infinity
|
|
251
|
+
for (const { points, base } of stacks)
|
|
252
|
+
for (const [, y] of [...points, ...(base ?? [])]) {
|
|
253
|
+
if (y < lo) lo = y
|
|
254
|
+
if (y > hi) hi = y
|
|
255
|
+
}
|
|
256
|
+
return lo <= hi ? [lo, hi] : [0, 1]
|
|
368
257
|
}
|
|
369
258
|
|
|
259
|
+
/** How a series' line runs between its points. */
|
|
260
|
+
export type LineCurve = "smooth" | "linear" | "step"
|
|
261
|
+
|
|
262
|
+
/** Points along a cubic Bezier segment, its start left out. */
|
|
263
|
+
const SAMPLES = 16
|
|
264
|
+
|
|
370
265
|
/**
|
|
371
|
-
* The
|
|
372
|
-
*
|
|
373
|
-
*
|
|
266
|
+
* The drawn path through `points` (ordered along x) as a polyline: the points
|
|
267
|
+
* themselves for `linear`; for `step`, level runs that change at the midpoint
|
|
268
|
+
* between neighbours; for `smooth`, a monotone cubic (Fritsch-Carlson, as d3's
|
|
269
|
+
* `curveMonotoneX`), which never overshoots the data between two points.
|
|
374
270
|
*/
|
|
375
|
-
function
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
|
|
271
|
+
export function curvePoints(points: readonly Vector2[], curve: LineCurve): Vector2[] {
|
|
272
|
+
if (curve === "step") {
|
|
273
|
+
const out: Vector2[] = points.length > 0 ? [points[0]] : []
|
|
274
|
+
for (let i = 1; i < points.length; i++) {
|
|
275
|
+
const mid = (points[i - 1].x + points[i].x) / 2
|
|
276
|
+
out.push({ x: mid, y: points[i - 1].y }, { x: mid, y: points[i].y }, points[i])
|
|
277
|
+
}
|
|
278
|
+
return out
|
|
279
|
+
}
|
|
280
|
+
if (curve !== "smooth" || points.length < 3) return [...points]
|
|
281
|
+
const tangents = monotoneTangents(points)
|
|
282
|
+
const out: Vector2[] = [points[0]]
|
|
283
|
+
for (let i = 1; i < points.length; i++) {
|
|
284
|
+
const a = points[i - 1]
|
|
285
|
+
const b = points[i]
|
|
286
|
+
const dx = (b.x - a.x) / 3
|
|
287
|
+
const c1 = { x: a.x + dx, y: a.y + dx * tangents[i - 1] }
|
|
288
|
+
const c2 = { x: b.x - dx, y: b.y - dx * tangents[i] }
|
|
289
|
+
for (let k = 1; k <= SAMPLES; k++) {
|
|
290
|
+
const t = k / SAMPLES
|
|
291
|
+
const u = 1 - t
|
|
292
|
+
out.push({
|
|
293
|
+
x: u * u * u * a.x + 3 * u * u * t * c1.x + 3 * u * t * t * c2.x + t * t * t * b.x,
|
|
294
|
+
y: u * u * u * a.y + 3 * u * u * t * c1.y + 3 * u * t * t * c2.y + t * t * t * b.y,
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return out
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function monotoneTangents(points: readonly Vector2[]): number[] {
|
|
302
|
+
const n = points.length
|
|
303
|
+
const slope = (i: number) => {
|
|
304
|
+
const h = points[i + 1].x - points[i].x
|
|
305
|
+
return h === 0 ? 0 : (points[i + 1].y - points[i].y) / h
|
|
306
|
+
}
|
|
307
|
+
const m = new Array<number>(n).fill(0)
|
|
308
|
+
for (let i = 1; i < n - 1; i++) {
|
|
309
|
+
const h0 = points[i].x - points[i - 1].x
|
|
310
|
+
const h1 = points[i + 1].x - points[i].x
|
|
311
|
+
const s0 = slope(i - 1)
|
|
312
|
+
const s1 = slope(i)
|
|
313
|
+
const p = h0 + h1 === 0 ? 0 : (s0 * h1 + s1 * h0) / (h0 + h1)
|
|
314
|
+
m[i] = (Math.sign(s0) + Math.sign(s1)) * Math.min(Math.abs(s0), Math.abs(s1), 0.5 * Math.abs(p)) || 0
|
|
315
|
+
}
|
|
316
|
+
m[0] = endTangent(slope(0), m[1])
|
|
317
|
+
m[n - 1] = endTangent(slope(n - 2), m[n - 2])
|
|
318
|
+
return m
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** d3's one-sided end tangent, held to the end segment's own direction and to three times its slope, past which it would overshoot. */
|
|
322
|
+
function endTangent(secant: number, inner: number): number {
|
|
323
|
+
const t = (3 * secant - inner) / 2
|
|
324
|
+
if (secant === 0 || Math.sign(t) !== Math.sign(secant)) return 0
|
|
325
|
+
return Math.abs(t) > 3 * Math.abs(secant) ? 3 * secant : t
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** The part of `points` (ordered along x) left of `x`, ending on the point where it crosses `x`. */
|
|
329
|
+
export function cutAt(points: readonly Vector2[], x: number): Vector2[] {
|
|
330
|
+
if (points.length === 0 || x < points[0].x) return []
|
|
331
|
+
const out: Vector2[] = []
|
|
332
|
+
for (let i = 0; i < points.length; i++) {
|
|
333
|
+
const p = points[i]
|
|
334
|
+
if (p.x <= x) {
|
|
335
|
+
out.push(p)
|
|
336
|
+
continue
|
|
337
|
+
}
|
|
338
|
+
const a = points[i - 1]
|
|
339
|
+
if (a && p.x > a.x) out.push({ x, y: a.y + ((p.y - a.y) * (x - a.x)) / (p.x - a.x) })
|
|
340
|
+
break
|
|
341
|
+
}
|
|
342
|
+
return out
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* `polygon` clipped to `box` (Sutherland-Hodgman against each edge in turn).
|
|
347
|
+
* The box is convex, so the result is the part inside, possibly with seams of
|
|
348
|
+
* no width where a concave outline leaves and re-enters, which fill as nothing.
|
|
349
|
+
*/
|
|
350
|
+
export function clipPolygonToBox(polygon: readonly Vector2[], box: RegionBox): Vector2[] {
|
|
351
|
+
if (polygon.length < 3 || !hasRegion(box)) return []
|
|
352
|
+
const edges: ((p: Vector2) => number)[] = [
|
|
353
|
+
(p) => p.x - box.left,
|
|
354
|
+
(p) => box.right - p.x,
|
|
355
|
+
(p) => p.y - box.bottom,
|
|
356
|
+
(p) => box.top - p.y,
|
|
357
|
+
]
|
|
358
|
+
let out: Vector2[] = [...polygon]
|
|
359
|
+
for (const inside of edges) {
|
|
360
|
+
const input = out
|
|
361
|
+
out = []
|
|
362
|
+
for (let i = 0; i < input.length; i++) {
|
|
363
|
+
const cur = input[i]
|
|
364
|
+
const prev = input[(i + input.length - 1) % input.length]
|
|
365
|
+
const dc = inside(cur)
|
|
366
|
+
const dp = inside(prev)
|
|
367
|
+
if (dc >= 0) {
|
|
368
|
+
if (dp < 0) out.push(crossing(prev, cur, dp, dc))
|
|
369
|
+
out.push(cur)
|
|
370
|
+
} else if (dp >= 0) out.push(crossing(prev, cur, dp, dc))
|
|
371
|
+
}
|
|
372
|
+
if (out.length === 0) return []
|
|
373
|
+
}
|
|
374
|
+
return out
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function crossing(a: Vector2, b: Vector2, da: number, db: number): Vector2 {
|
|
378
|
+
const t = da / (da - db)
|
|
379
|
+
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** A line chart's own look: how each series is stroked, dotted and filled. */
|
|
383
|
+
export const LINE_MARKS = {
|
|
384
|
+
strokeWidth: Types.number(4, { min: 0, max: 24, step: 0.5, unit: "px" }),
|
|
385
|
+
curve: Types.oneOf(["smooth", "linear", "step"] as const),
|
|
386
|
+
dots: Types.boolean(false),
|
|
387
|
+
/** Each dot's diameter. */
|
|
388
|
+
dotSize: Types.number(12, { min: 0, max: 48, step: 1, unit: "px" }),
|
|
389
|
+
/** Fill under each line: none, the series colour, or the colour fading to nothing at the baseline. */
|
|
390
|
+
area: Types.oneOf(["none", "solid", "gradient"] as const),
|
|
391
|
+
/** The fill's opacity at the line. */
|
|
392
|
+
areaOpacity: Types.number(0.4, { min: 0, max: 1, step: 0.05 }),
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export const LINE_LABELS = {
|
|
396
|
+
/** Read as the chart is built: each point's value printed above or below it, or none. */
|
|
397
|
+
values: Types.oneOf(["none", "above", "below"] as const, { animatable: false }),
|
|
379
398
|
}
|
|
@@ -185,6 +185,9 @@ function extentOf(values: number[]): Range {
|
|
|
185
185
|
return lo <= hi ? [lo, hi] : [0, 1]
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/** Up to this many samples in view, every one is a tick. */
|
|
189
|
+
export const EVERY_SAMPLE = 12
|
|
190
|
+
|
|
188
191
|
/**
|
|
189
192
|
* Ticks spanning `range`, each a sample, nearest to where an evenly spaced tick
|
|
190
193
|
* would go. They must be samples (a caption between two has nothing to say) and
|
|
@@ -201,6 +204,8 @@ function sampledTicks(
|
|
|
201
204
|
const inside = positions.filter((v) => v >= min && v <= max)
|
|
202
205
|
const unique = [...new Set(inside)].sort((a, b) => a - b)
|
|
203
206
|
if (unique.length === 0) return markerValues([min, max], divisions)
|
|
207
|
+
// Few and even enough to caption every one: a month left out reads as missing data.
|
|
208
|
+
if (unique.length <= EVERY_SAMPLE && evenlySpaced(unique)) return unique
|
|
204
209
|
|
|
205
210
|
const nearest = (target: number) =>
|
|
206
211
|
unique.reduce((best, v) =>
|
|
@@ -215,6 +220,14 @@ function sampledTicks(
|
|
|
215
220
|
return out
|
|
216
221
|
}
|
|
217
222
|
|
|
223
|
+
/** Whether no two neighbouring samples sit much closer than the average gap: months do, a burst of days between weeks of nothing does not. */
|
|
224
|
+
function evenlySpaced(sorted: readonly number[]): boolean {
|
|
225
|
+
if (sorted.length < 3) return true
|
|
226
|
+
const mean = (sorted[sorted.length - 1] - sorted[0]) / (sorted.length - 1)
|
|
227
|
+
for (let i = 1; i < sorted.length; i++) if (sorted[i] - sorted[i - 1] < mean * 0.75) return false
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
|
|
218
231
|
/** The cell of the row a tick came from, or `fallback` when it is not a sample. */
|
|
219
232
|
function labelBySample(
|
|
220
233
|
positions: number[],
|
package/src/nodes.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { LineChart } from "./line-chart";
|
|
2
2
|
import { BarChart } from "./bar-chart";
|
|
3
3
|
import { PieChart } from "./pie-chart";
|
|
4
|
+
import { RadarChart } from "./radar-chart";
|
|
5
|
+
import { RadialChart } from "./radial-chart";
|
|
4
6
|
|
|
5
7
|
// Typed by name, so the declaration names each component rather than spelling out its type through core.
|
|
6
|
-
export const NODES: (typeof LineChart | typeof BarChart | typeof PieChart)[] = [
|
|
8
|
+
export const NODES: (typeof LineChart | typeof BarChart | typeof PieChart | typeof RadarChart | typeof RadialChart)[] = [
|
|
9
|
+
LineChart,
|
|
10
|
+
BarChart,
|
|
11
|
+
PieChart,
|
|
12
|
+
RadarChart,
|
|
13
|
+
RadialChart,
|
|
14
|
+
];
|
package/src/pie-chart/index.ts
CHANGED
|
@@ -1,26 +1,5 @@
|
|
|
1
1
|
export { PieChart, isSliceSelected } from "./pie-chart"
|
|
2
2
|
export type { PieChartValues } from "./pie-chart"
|
|
3
3
|
export { PieRing } from "./pie-ring"
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
6
|
-
DEFAULT_PIE_CHART_THEME,
|
|
7
|
-
lerpPieChartTheme,
|
|
8
|
-
pieArcs,
|
|
9
|
-
relaxLabels,
|
|
10
|
-
resolvePieChartTheme,
|
|
11
|
-
resolvePieSlices,
|
|
12
|
-
ringPoint,
|
|
13
|
-
sliceTotal,
|
|
14
|
-
sliceValue,
|
|
15
|
-
} from "./shared"
|
|
16
|
-
export type {
|
|
17
|
-
CenterConfig,
|
|
18
|
-
LabelConfig,
|
|
19
|
-
PieArc,
|
|
20
|
-
PieChartConfig,
|
|
21
|
-
PieChartTheme,
|
|
22
|
-
PieLabelMode,
|
|
23
|
-
PieLabelPlacement,
|
|
24
|
-
PieSlice,
|
|
25
|
-
RingConfig,
|
|
26
|
-
} from "./shared"
|
|
4
|
+
export { PIE_STYLE_GROUPS, pieArcs, relaxLabels, resolvePieSlices, sliceTotal, sliceValue } from "./shared"
|
|
5
|
+
export type { LabelConfig, PieArc, PieLabelMode, PieLabelPlacement, PieStyle, RingConfig } from "./shared"
|
|
@@ -10,30 +10,35 @@ import {
|
|
|
10
10
|
// Named so this component's emitted type reaches `Node2D` and its props through the sdk, not core.
|
|
11
11
|
type Node2D,
|
|
12
12
|
type Node2DProps as _Node2DProps,
|
|
13
|
+
type TextStyle as _TextStyle,
|
|
14
|
+
type Fill as _Fill,
|
|
15
|
+
type FillResolved as _FillResolved,
|
|
13
16
|
} from "@motionscript/sdk"
|
|
14
17
|
|
|
15
18
|
import { asParts, fresh } from "../kit/entries"
|
|
16
19
|
import { beats, type AnyTimeline, type Beat } from "../kit/motion"
|
|
17
|
-
import { PieLegend } from "
|
|
20
|
+
import { PieLegend } from "../kit/pie-legend"
|
|
18
21
|
import { PieRing } from "./pie-ring"
|
|
22
|
+
import { CHART_STYLE_FAMILY, CHART_STYLE_PRESETS, migrateChartStyle, paletteFill } from "../kit/style"
|
|
19
23
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
PIE_SPOTLIGHT_DIM,
|
|
25
|
+
PIE_STYLE_GROUPS,
|
|
26
|
+
centerConfigOf,
|
|
27
|
+
labelConfigOf,
|
|
28
|
+
legendOf,
|
|
23
29
|
resolvePieSlices,
|
|
30
|
+
ringConfigOf,
|
|
24
31
|
selectionFade,
|
|
25
|
-
type PieChartConfig,
|
|
26
|
-
type PieChartTheme,
|
|
27
32
|
type PieLabelMode,
|
|
28
|
-
type PieLabelPlacement,
|
|
29
33
|
type PieSlice,
|
|
34
|
+
type PieStyle,
|
|
30
35
|
} from "./shared"
|
|
31
36
|
|
|
32
37
|
export interface PieChartValues {
|
|
33
38
|
readonly data: DataRecord[]
|
|
34
39
|
readonly categoryField: string
|
|
35
40
|
readonly valueField: string
|
|
36
|
-
readonly
|
|
41
|
+
readonly style: PieStyle
|
|
37
42
|
readonly pops: number[]
|
|
38
43
|
}
|
|
39
44
|
|
|
@@ -44,7 +49,12 @@ const LABEL_SHARE = 0.35
|
|
|
44
49
|
const SLICE_ARG = Types.number(0, { min: 0, step: 1 })
|
|
45
50
|
|
|
46
51
|
function slicesOf(s: PieChartValues): PieSlice[] {
|
|
47
|
-
return resolvePieSlices(
|
|
52
|
+
return resolvePieSlices(
|
|
53
|
+
s.data,
|
|
54
|
+
s.categoryField,
|
|
55
|
+
s.valueField,
|
|
56
|
+
s.data.map((_, i) => paletteFill(s.style, i))
|
|
57
|
+
)
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
function popAt(s: PieChartValues, index: number): number {
|
|
@@ -60,8 +70,7 @@ export function isSliceSelected(s: PieChartValues, index: number): boolean {
|
|
|
60
70
|
function sliceDims(s: PieChartValues): number[] {
|
|
61
71
|
let focus = 0
|
|
62
72
|
for (let i = 0; i < s.data.length; i++) focus = Math.max(focus, popAt(s, i))
|
|
63
|
-
|
|
64
|
-
return s.data.map((_, i) => selectionFade(focus, dim, popAt(s, i)))
|
|
73
|
+
return s.data.map((_, i) => selectionFade(focus, PIE_SPOTLIGHT_DIM, popAt(s, i)))
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
/** Every slice's pop eased to what `pop` asks for. A move that changes nothing still takes its slot. */
|
|
@@ -94,16 +103,14 @@ function partsOf(children: readonly ChildView[]) {
|
|
|
94
103
|
export const PieChart = component2d({
|
|
95
104
|
key: "pieChart",
|
|
96
105
|
state: {
|
|
97
|
-
data: fresh<DataRecord[]>(() => []),
|
|
106
|
+
data: fresh<DataRecord[]>(() => [], { kind: "dataset" }),
|
|
98
107
|
/** Slice caption, used on the labels and the legend. */
|
|
99
108
|
categoryField: "",
|
|
100
109
|
/** Slice magnitude: the wedge's share of the ring. */
|
|
101
110
|
valueField: "",
|
|
102
|
-
|
|
111
|
+
style: Types.style(CHART_STYLE_FAMILY, PIE_STYLE_GROUPS, { presets: CHART_STYLE_PRESETS }),
|
|
103
112
|
/** What the ring's labels say. */
|
|
104
113
|
labelMode: Types.custom<PieLabelMode>("percent"),
|
|
105
|
-
/** Where they sit: inside their wedge, outside on a leader, or nowhere. */
|
|
106
|
-
labelPlacement: Types.custom<PieLabelPlacement>("leader"),
|
|
107
114
|
/** Gap between the ring and the legend, in px. */
|
|
108
115
|
gap: 40,
|
|
109
116
|
/** Text set in the ring's hole. */
|
|
@@ -112,8 +119,6 @@ export const PieChart = component2d({
|
|
|
112
119
|
centerCaption: "",
|
|
113
120
|
/** How far each slice is pushed out, `0`-`1` per index. Here, not on the ring, because the wedges and the legend both read it. */
|
|
114
121
|
pops: fresh<number[]>(() => []),
|
|
115
|
-
/** Read as the chart is built: the legend is a part the chart builds or doesn't. */
|
|
116
|
-
showLegend: Types.boolean(true, { animatable: false }),
|
|
117
122
|
/** Read as the chart is built: whether the hole's summary is drawn at rest. A scene that wants it to land later sets `false` and runs `revealCenter`. */
|
|
118
123
|
showCenter: Types.boolean(true, { animatable: false }),
|
|
119
124
|
},
|
|
@@ -123,41 +128,55 @@ export const PieChart = component2d({
|
|
|
123
128
|
height: "fill",
|
|
124
129
|
pops: (state) => slicesOf(state).map(() => 0),
|
|
125
130
|
},
|
|
131
|
+
migrate: { style: migrateChartStyle("pie") },
|
|
126
132
|
allowedChildren: [],
|
|
127
133
|
// The structure (a label and legend row per row) is read once; slices, paint and selection are bound, so a theme tween re-paints and a `set` of `data` re-cuts.
|
|
128
134
|
children: (state) => {
|
|
129
|
-
const
|
|
135
|
+
const style = () => state.style
|
|
130
136
|
const slices = () => slicesOf(state)
|
|
131
137
|
const parts: Node2D[] = [
|
|
132
138
|
new PieRing({
|
|
133
139
|
width: "fill",
|
|
134
140
|
height: "fill",
|
|
135
141
|
slices,
|
|
136
|
-
ring: () =>
|
|
137
|
-
label: () =>
|
|
138
|
-
centerStyle: () =>
|
|
142
|
+
ring: () => ringConfigOf(style()),
|
|
143
|
+
label: () => labelConfigOf(style()),
|
|
144
|
+
centerStyle: () => centerConfigOf(style()),
|
|
139
145
|
labelMode: state.labelMode,
|
|
140
|
-
labelPlacement: state.labelPlacement,
|
|
146
|
+
labelPlacement: state.style.labelPlacement,
|
|
141
147
|
pops: () => state.pops,
|
|
142
|
-
dimOpacity:
|
|
148
|
+
dimOpacity: PIE_SPOTLIGHT_DIM,
|
|
143
149
|
centerLabel: state.centerLabel,
|
|
144
150
|
centerCaption: state.centerCaption,
|
|
145
151
|
centerReveal: state.showCenter === false ? 0 : 1,
|
|
146
152
|
}) as unknown as Node2D,
|
|
147
153
|
]
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
154
|
+
const position = state.style.legend
|
|
155
|
+
if (position !== "none") {
|
|
156
|
+
const side = position === "right"
|
|
157
|
+
const legend = new PieLegend({
|
|
158
|
+
slices,
|
|
159
|
+
legend: () => legendOf(style()),
|
|
160
|
+
dims: () => sliceDims(state),
|
|
161
|
+
flow: side ? "vertical" : "horizontal",
|
|
162
|
+
align: "center",
|
|
163
|
+
// Spacing is fixed as built.
|
|
164
|
+
gap: side ? legendOf(state.style).itemGap / 3 : legendOf(state.style).itemGap,
|
|
165
|
+
padding: 0,
|
|
166
|
+
}) as unknown as Node2D
|
|
167
|
+
if (position === "top") parts.unshift(legend)
|
|
168
|
+
else parts.push(legend)
|
|
159
169
|
}
|
|
160
|
-
const row = new Rect({
|
|
170
|
+
const row = new Rect({
|
|
171
|
+
flow: position === "right" || position === "none" ? "horizontal" : "vertical",
|
|
172
|
+
width: "fill",
|
|
173
|
+
height: "fill",
|
|
174
|
+
gap: state.gap,
|
|
175
|
+
padding: 24,
|
|
176
|
+
align: "center",
|
|
177
|
+
fill: () => state.style.background,
|
|
178
|
+
children: parts,
|
|
179
|
+
})
|
|
161
180
|
return asParts([row as unknown as Node2D])
|
|
162
181
|
},
|
|
163
182
|
commands: {
|