@motionscript/charts 0.1.1-alpha.0 → 0.1.1-alpha.1
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/README.md +98 -28
- package/dist/bar-chart/bar-chart.d.ts +233 -39
- package/dist/bar-chart/bar-chart.d.ts.map +1 -1
- package/dist/bar-chart/bar-chart.js +215 -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 +52 -52
- package/dist/bar-chart/chart-bars.d.ts.map +1 -1
- package/dist/bar-chart/chart-bars.js +80 -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 +36 -65
- package/dist/bar-chart/shared.d.ts.map +1 -1
- package/dist/bar-chart/shared.js +68 -117
- package/dist/bar-chart/shared.js.map +1 -1
- package/dist/browser/chunks/chunk-R3Q5EE6E.js +2 -0
- package/dist/browser/chunks/chunk-R3Q5EE6E.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 +49 -15
- package/dist/kit/cartesian.d.ts.map +1 -1
- package/dist/kit/cartesian.js +79 -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 +254 -0
- package/dist/kit/style.d.ts.map +1 -0
- package/dist/kit/style.js +197 -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 +257 -18
- package/dist/line-chart/line-chart.d.ts.map +1 -1
- package/dist/line-chart/line-chart.js +282 -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 +37 -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 +53 -61
- package/dist/pie-chart/shared.d.ts.map +1 -1
- package/dist/pie-chart/shared.js +41 -121
- 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 +218 -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 +43 -0
- package/dist/radar-chart/radar-grid.d.ts.map +1 -0
- package/dist/radar-chart/radar-grid.js +61 -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 +73 -0
- package/dist/radar-chart/shared.d.ts.map +1 -0
- package/dist/radar-chart/shared.js +123 -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 +216 -0
- package/dist/radial-chart/radial-chart.d.ts.map +1 -0
- package/dist/radial-chart/radial-chart.js +174 -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 +73 -0
- package/dist/radial-chart/shared.d.ts.map +1 -0
- package/dist/radial-chart/shared.js +79 -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 +252 -179
- package/src/bar-chart/category-axis.ts +46 -40
- package/src/bar-chart/chart-bars.ts +97 -117
- package/src/bar-chart/index.ts +7 -10
- package/src/bar-chart/shared.ts +78 -238
- package/src/engine.ts +5 -2
- package/src/index.ts +2 -0
- package/src/kit/cartesian.ts +118 -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 +257 -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 +347 -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 +52 -35
- package/src/pie-chart/pie-ring.ts +92 -71
- package/src/pie-chart/shared.ts +68 -266
- package/src/radar-chart/index.ts +4 -0
- package/src/radar-chart/radar-chart.ts +287 -0
- package/src/radar-chart/radar-grid.ts +78 -0
- package/src/radar-chart/radar-series.ts +88 -0
- package/src/radar-chart/shared.ts +146 -0
- package/src/radial-chart/index.ts +5 -0
- package/src/radial-chart/radial-chart.ts +210 -0
- package/src/radial-chart/radial-rings.ts +328 -0
- package/src/radial-chart/shared.ts +114 -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/bar-chart/shared.ts
CHANGED
|
@@ -1,91 +1,66 @@
|
|
|
1
|
-
import {
|
|
2
|
-
fillOps,
|
|
3
|
-
lerpNumber,
|
|
4
|
-
shadowOps,
|
|
5
|
-
strokeOps,
|
|
6
|
-
textOps,
|
|
7
|
-
type DataRecord,
|
|
8
|
-
type Fill,
|
|
9
|
-
type Shadow,
|
|
10
|
-
type ShadowResolved,
|
|
11
|
-
type Stroke,
|
|
12
|
-
type StrokeResolved,
|
|
13
|
-
type TextStyle,
|
|
14
|
-
} from "@motionscript/sdk"
|
|
15
|
-
import { niceStep } from "@motionscript/sdk/component"
|
|
1
|
+
import { Types, fillOps, type DataRecord, type Fill } from "@motionscript/sdk"
|
|
16
2
|
|
|
17
|
-
import {
|
|
18
|
-
CHART_PALETTE,
|
|
19
|
-
DEFAULT_LEGEND,
|
|
20
|
-
DEFAULT_PLOT_AREA,
|
|
21
|
-
DEFAULT_SERIES_FILLS,
|
|
22
|
-
DEFAULT_X_AXIS,
|
|
23
|
-
DEFAULT_Y_AXIS,
|
|
24
|
-
SPOTLIGHT_DIM,
|
|
25
|
-
lerpAxisConfig,
|
|
26
|
-
lerpLegendConfig,
|
|
27
|
-
lerpPlotArea,
|
|
28
|
-
lerpSeriesFills,
|
|
29
|
-
num,
|
|
30
|
-
resolveAxisConfig,
|
|
31
|
-
resolveLegendConfig,
|
|
32
|
-
resolvePlotArea,
|
|
33
|
-
resolveSeriesFills,
|
|
34
|
-
text,
|
|
35
|
-
type AxisConfig,
|
|
36
|
-
type ChartSeries,
|
|
37
|
-
type LegendConfig,
|
|
38
|
-
type LegendTheme,
|
|
39
|
-
type PlotAreaConfig,
|
|
40
|
-
type Range,
|
|
41
|
-
} from "../kit/shared"
|
|
3
|
+
import { niceRange, num, text, type ChartSeries, type Range } from "../kit/shared"
|
|
42
4
|
|
|
43
5
|
/** One bar per row, beside its siblings in the category slot or stacked on them: {@link ChartSeries} under the name for what it draws here. */
|
|
44
6
|
export type BarSeries = ChartSeries
|
|
45
7
|
|
|
8
|
+
/** How the series share a category: side by side, stacked, or stacked as shares of 100. */
|
|
9
|
+
export type BarStack = "none" | "stacked" | "percent"
|
|
10
|
+
|
|
11
|
+
/** Which way the bars grow: up from a category row, or right from a category column. */
|
|
12
|
+
export type BarOrientation = "vertical" | "horizontal"
|
|
13
|
+
|
|
46
14
|
/**
|
|
47
|
-
* One bar's span in data units. A span rather than a height because
|
|
48
|
-
*
|
|
15
|
+
* One bar's span in data units. A span rather than a height because every mode
|
|
16
|
+
* fits it, and a negative value just runs the other way with no special case.
|
|
49
17
|
*/
|
|
50
18
|
export interface BarSpan {
|
|
51
|
-
/** Where the base sits: 0
|
|
19
|
+
/** Where the base sits: 0 apart, the running total below (or above, for a negative) when stacked. */
|
|
52
20
|
from: number
|
|
53
21
|
to: number
|
|
54
|
-
/** `to - from
|
|
22
|
+
/** `to - from`. */
|
|
55
23
|
value: number
|
|
24
|
+
/** Whether it ends its stack on its side of zero, so its far end is rounded. */
|
|
25
|
+
outer: boolean
|
|
56
26
|
}
|
|
57
27
|
|
|
58
28
|
/**
|
|
59
|
-
* One row's spans, in series order. Stacking
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
row: DataRecord,
|
|
65
|
-
series: BarSeries[],
|
|
66
|
-
stacked: boolean
|
|
67
|
-
): BarSpan[] {
|
|
68
|
-
let running = 0
|
|
69
|
-
return series.map((s) => {
|
|
70
|
-
const value = num(row, s.field)
|
|
71
|
-
if (!stacked) return { from: 0, to: value, value }
|
|
72
|
-
const from = running
|
|
73
|
-
running += value
|
|
74
|
-
return { from, to: running, value }
|
|
75
|
-
})
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* The series ending a row's stack, which gets the rounded cap; `-1` when the row
|
|
80
|
-
* is all zero. It is the last non-zero series, so a zero one does not take the
|
|
81
|
-
* cap and leave the segment below square.
|
|
29
|
+
* One row's spans, in series order. Stacking keeps a running sum on each side
|
|
30
|
+
* of zero, so a negative value stacks downward from the baseline rather than
|
|
31
|
+
* eating into the positives. In `percent` each value is its share of the row's
|
|
32
|
+
* total magnitude. A zero value still yields a degenerate span, keeping the list
|
|
33
|
+
* index-aligned with the series. `weights` scale each series' contribution.
|
|
82
34
|
*/
|
|
83
|
-
export function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
35
|
+
export function barSpans(row: DataRecord, series: readonly BarSeries[], stack: BarStack, weights: readonly number[] = []): BarSpan[] {
|
|
36
|
+
const weight = (i: number) => Math.max(0, weights[i] ?? 1)
|
|
37
|
+
if (stack !== "stacked" && stack !== "percent") {
|
|
38
|
+
return series.map((s) => {
|
|
39
|
+
const value = num(row, s.field)
|
|
40
|
+
return { from: 0, to: value, value, outer: true }
|
|
41
|
+
})
|
|
87
42
|
}
|
|
88
|
-
|
|
43
|
+
const values = series.map((s, i) => num(row, s.field) * weight(i))
|
|
44
|
+
const total = values.reduce((sum, v) => sum + Math.abs(v), 0)
|
|
45
|
+
const scaled = stack === "percent" ? values.map((v) => (total > 0 ? (v / total) * 100 : 0)) : values
|
|
46
|
+
let lastUp = -1
|
|
47
|
+
let lastDown = -1
|
|
48
|
+
scaled.forEach((v, i) => {
|
|
49
|
+
if (v > 0) lastUp = i
|
|
50
|
+
else if (v < 0) lastDown = i
|
|
51
|
+
})
|
|
52
|
+
let up = 0
|
|
53
|
+
let down = 0
|
|
54
|
+
return scaled.map((value, i) => {
|
|
55
|
+
if (value < 0) {
|
|
56
|
+
const from = down
|
|
57
|
+
down += value
|
|
58
|
+
return { from, to: down, value, outer: i === lastDown }
|
|
59
|
+
}
|
|
60
|
+
const from = up
|
|
61
|
+
up += value
|
|
62
|
+
return { from, to: up, value, outer: i === lastUp }
|
|
63
|
+
})
|
|
89
64
|
}
|
|
90
65
|
|
|
91
66
|
export function barCategories(data: DataRecord[], field: string): string[] {
|
|
@@ -94,18 +69,14 @@ export function barCategories(data: DataRecord[], field: string): string[] {
|
|
|
94
69
|
|
|
95
70
|
/**
|
|
96
71
|
* The `[min, max]` of everything drawn, zero always included: a scale starting
|
|
97
|
-
* at 40 draws a
|
|
72
|
+
* at 40 draws a bar three times its neighbour for a 10% difference. A cropped
|
|
98
73
|
* axis takes an explicit range.
|
|
99
74
|
*/
|
|
100
|
-
export function barValueExtent(
|
|
101
|
-
data: DataRecord[],
|
|
102
|
-
series: BarSeries[],
|
|
103
|
-
stacked: boolean
|
|
104
|
-
): Range {
|
|
75
|
+
export function barValueExtent(data: DataRecord[], series: readonly BarSeries[], stack: BarStack, weights?: readonly number[]): Range {
|
|
105
76
|
let lo = 0
|
|
106
77
|
let hi = 0
|
|
107
78
|
for (const row of data) {
|
|
108
|
-
for (const span of barSpans(row, series,
|
|
79
|
+
for (const span of barSpans(row, series, stack, weights)) {
|
|
109
80
|
lo = Math.min(lo, span.from, span.to)
|
|
110
81
|
hi = Math.max(hi, span.from, span.to)
|
|
111
82
|
}
|
|
@@ -113,174 +84,43 @@ export function barValueExtent(
|
|
|
113
84
|
return lo < hi ? [lo, hi] : [0, 1]
|
|
114
85
|
}
|
|
115
86
|
|
|
116
|
-
/** The explicit `range`, else the data's extent nudged to tidy bounds; `divisions` (one fewer than the marker count) makes the drawn ticks land on them. */
|
|
87
|
+
/** The explicit `range`, else the data's extent nudged to tidy bounds (a percent stack reads to 100); `divisions` (one fewer than the marker count) makes the drawn ticks land on them. */
|
|
117
88
|
export function resolveBarScale(
|
|
118
89
|
data: DataRecord[],
|
|
119
|
-
series: BarSeries[],
|
|
120
|
-
|
|
90
|
+
series: readonly BarSeries[],
|
|
91
|
+
stack: BarStack,
|
|
121
92
|
range: Range | undefined,
|
|
122
|
-
divisions: number
|
|
93
|
+
divisions: number,
|
|
94
|
+
weights?: readonly number[]
|
|
123
95
|
): Range {
|
|
124
96
|
if (range) return range
|
|
125
|
-
const [lo, hi] = barValueExtent(data, series,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return [Math.floor(lo / step) * step, Math.ceil(hi / step) * step]
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/** How the columns are drawn and the slot divided. */
|
|
132
|
-
export interface BarStyleConfig {
|
|
133
|
-
/** Radius in px of a column's far-end corners; an interior stacked segment stays square. */
|
|
134
|
-
cornerRadius: number
|
|
135
|
-
/** Fraction of the category slot the bars fill, `[0, 1]`; the rest separates groups. */
|
|
136
|
-
slotFill: number
|
|
137
|
-
/** Gap in px between grouped bars within a slot; ignored when stacked. */
|
|
138
|
-
groupGap: number
|
|
139
|
-
stroke: Stroke
|
|
140
|
-
shadow: Shadow
|
|
97
|
+
const [lo, hi] = barValueExtent(data, series, stack, weights)
|
|
98
|
+
if (stack === "percent") return [lo < 0 ? -100 : 0, hi > 0 ? 100 : 0]
|
|
99
|
+
return niceRange([lo, hi], divisions)
|
|
141
100
|
}
|
|
142
101
|
|
|
143
|
-
/**
|
|
144
|
-
export
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
plotArea: PlotAreaConfig
|
|
153
|
-
legend: LegendConfig
|
|
154
|
-
/** The figure printed on each column. */
|
|
155
|
-
valueStyle: TextStyle
|
|
156
|
-
/** How far everything outside a spotlit series or category fades. */
|
|
157
|
-
spotlight: { dimOpacity: number }
|
|
102
|
+
/** Black or white, whichever reads on `fill`: a figure printed inside a bar. */
|
|
103
|
+
export function inkOn(fill: Fill): string {
|
|
104
|
+
const layers = fillOps.resolve(fill)
|
|
105
|
+
const top = layers[layers.length - 1]
|
|
106
|
+
if (top?.type !== "solid") return "white"
|
|
107
|
+
const [r, g, b] = top.color
|
|
108
|
+
const lift = (c: number) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4)
|
|
109
|
+
const luminance = 0.2126 * lift(r) + 0.7152 * lift(g) + 0.0722 * lift(b)
|
|
110
|
+
return luminance > 0.4 ? "#0a0a0a" : "white"
|
|
158
111
|
}
|
|
159
112
|
|
|
160
|
-
/**
|
|
161
|
-
export
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
valueStyle?: TextStyle
|
|
169
|
-
spotlight?: Partial<BarChartConfig["spotlight"]>
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export const DEFAULT_BAR_CHART_THEME: BarChartConfig = {
|
|
173
|
-
seriesFills: DEFAULT_SERIES_FILLS,
|
|
174
|
-
bar: {
|
|
175
|
-
cornerRadius: 6,
|
|
176
|
-
slotFill: 0.72,
|
|
177
|
-
groupGap: 6,
|
|
178
|
-
stroke: [],
|
|
179
|
-
shadow: [],
|
|
180
|
-
},
|
|
181
|
-
// Captions rather than numeric markers: less weight than the line chart's ticks.
|
|
182
|
-
xAxis: {
|
|
183
|
-
...DEFAULT_X_AXIS,
|
|
184
|
-
labelStyle: { ...DEFAULT_X_AXIS.labelStyle, fontWeight: 600 },
|
|
185
|
-
tickPadding: 16,
|
|
186
|
-
},
|
|
187
|
-
yAxis: DEFAULT_Y_AXIS,
|
|
188
|
-
plotArea: DEFAULT_PLOT_AREA,
|
|
189
|
-
legend: DEFAULT_LEGEND,
|
|
190
|
-
valueStyle: {
|
|
191
|
-
fontSize: 24,
|
|
192
|
-
fontWeight: 700,
|
|
193
|
-
textAlign: "center",
|
|
194
|
-
fill: CHART_PALETTE.markerColor,
|
|
195
|
-
},
|
|
196
|
-
spotlight: { dimOpacity: SPOTLIGHT_DIM },
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function resolveBarStyle(
|
|
200
|
-
bar: Partial<BarStyleConfig> | undefined,
|
|
201
|
-
previous: BarStyleConfig
|
|
202
|
-
): BarStyleConfig {
|
|
203
|
-
return {
|
|
204
|
-
cornerRadius: bar?.cornerRadius ?? previous.cornerRadius,
|
|
205
|
-
slotFill: bar?.slotFill ?? previous.slotFill,
|
|
206
|
-
groupGap: bar?.groupGap ?? previous.groupGap,
|
|
207
|
-
stroke: strokeOps.resolve(bar?.stroke ?? previous.stroke),
|
|
208
|
-
shadow: shadowOps.resolve(bar?.shadow ?? previous.shadow),
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function lerpBarStyle(
|
|
213
|
-
from: BarStyleConfig,
|
|
214
|
-
to: BarStyleConfig,
|
|
215
|
-
t: number
|
|
216
|
-
): BarStyleConfig {
|
|
217
|
-
return {
|
|
218
|
-
cornerRadius: lerpNumber(from.cornerRadius, to.cornerRadius, t),
|
|
219
|
-
slotFill: lerpNumber(from.slotFill, to.slotFill, t),
|
|
220
|
-
groupGap: lerpNumber(from.groupGap, to.groupGap, t),
|
|
221
|
-
stroke: strokeOps.lerp(
|
|
222
|
-
from.stroke as StrokeResolved[],
|
|
223
|
-
to.stroke as StrokeResolved[],
|
|
224
|
-
t
|
|
225
|
-
),
|
|
226
|
-
shadow: shadowOps.lerp(
|
|
227
|
-
from.shadow as ShadowResolved[],
|
|
228
|
-
to.shadow as ShadowResolved[],
|
|
229
|
-
t
|
|
230
|
-
),
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** The `theme` entry's resolve: merges a partial {@link BarChartTheme} onto `previous` (first resolution: the default). */
|
|
235
|
-
export function resolveBarChartTheme(
|
|
236
|
-
theme: BarChartTheme | undefined,
|
|
237
|
-
previous?: BarChartConfig
|
|
238
|
-
): BarChartConfig {
|
|
239
|
-
const base = previous ?? DEFAULT_BAR_CHART_THEME
|
|
240
|
-
return {
|
|
241
|
-
seriesFills: resolveSeriesFills(theme?.seriesFills, base.seriesFills),
|
|
242
|
-
bar: resolveBarStyle(theme?.bar, base.bar),
|
|
243
|
-
xAxis: resolveAxisConfig(theme?.xAxis, base.xAxis),
|
|
244
|
-
yAxis: resolveAxisConfig(theme?.yAxis, base.yAxis),
|
|
245
|
-
plotArea: resolvePlotArea(theme?.plotArea, base.plotArea),
|
|
246
|
-
legend: resolveLegendConfig(theme?.legend, base.legend),
|
|
247
|
-
valueStyle: textOps.resolveStyle(theme?.valueStyle, base.valueStyle),
|
|
248
|
-
spotlight: {
|
|
249
|
-
dimOpacity: theme?.spotlight?.dimOpacity ?? base.spotlight.dimOpacity,
|
|
250
|
-
},
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export function lerpBarChartTheme(
|
|
255
|
-
from: BarChartConfig,
|
|
256
|
-
to: BarChartConfig,
|
|
257
|
-
t: number
|
|
258
|
-
): BarChartConfig {
|
|
259
|
-
return {
|
|
260
|
-
seriesFills: lerpSeriesFills(from.seriesFills, to.seriesFills, t),
|
|
261
|
-
bar: lerpBarStyle(from.bar, to.bar, t),
|
|
262
|
-
xAxis: lerpAxisConfig(from.xAxis, to.xAxis, t),
|
|
263
|
-
yAxis: lerpAxisConfig(from.yAxis, to.yAxis, t),
|
|
264
|
-
plotArea: lerpPlotArea(from.plotArea, to.plotArea, t),
|
|
265
|
-
legend: lerpLegendConfig(from.legend, to.legend, t),
|
|
266
|
-
valueStyle: textOps.lerpStyle(from.valueStyle, to.valueStyle, t),
|
|
267
|
-
spotlight: {
|
|
268
|
-
dimOpacity: lerpNumber(
|
|
269
|
-
from.spotlight.dimOpacity,
|
|
270
|
-
to.spotlight.dimOpacity,
|
|
271
|
-
t
|
|
272
|
-
),
|
|
273
|
-
},
|
|
274
|
-
}
|
|
113
|
+
/** A bar chart's own look: how round and how wide the bars are. */
|
|
114
|
+
export const BAR_MARKS = {
|
|
115
|
+
/** Radius of each bar's far corners; a stacked segment inside the stack stays square. */
|
|
116
|
+
barRadius: Types.number(10, { min: 0, max: 64, step: 1, unit: "px" }),
|
|
117
|
+
/** How much of its category slot a bar group fills, the rest spacing the groups. */
|
|
118
|
+
barWidth: Types.number(0.7, { min: 0.05, max: 1, step: 0.05 }),
|
|
119
|
+
/** Space between the bars of one group, side by side. */
|
|
120
|
+
barGap: Types.number(6, { min: 0, max: 40, step: 1, unit: "px" }),
|
|
275
121
|
}
|
|
276
122
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
series: BarSeries,
|
|
281
|
-
index: number
|
|
282
|
-
): Fill {
|
|
283
|
-
if (series.fill != null) return fillOps.resolve(series.fill)
|
|
284
|
-
if (palette.length === 0) return "white"
|
|
285
|
-
return palette[index % palette.length]
|
|
123
|
+
export const BAR_LABELS = {
|
|
124
|
+
/** Read as the chart is built: each bar's value past its end, inside it, or none. */
|
|
125
|
+
values: Types.oneOf(["none", "end", "inside"] as const, { animatable: false }),
|
|
286
126
|
}
|
package/src/engine.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The chart kit: what the
|
|
2
|
+
* The chart kit: what the five charts share, and what a copied chart does not
|
|
3
3
|
* copy. A chart finds its parts by component identity (`VerticalAxis.is`), so a
|
|
4
4
|
* second copy of `VerticalAxis` would be a second component that the chart never
|
|
5
5
|
* finds, with no error. Copies import the kit from `@motionscript/charts`.
|
|
6
6
|
*/
|
|
7
7
|
export * from "./kit/shared"
|
|
8
|
+
export * from "./kit/style"
|
|
8
9
|
export * from "./kit/cartesian"
|
|
9
|
-
export * from "./kit/chart-border"
|
|
10
10
|
export * from "./kit/chart-canvas"
|
|
11
|
+
export * from "./kit/horizontal-axis"
|
|
11
12
|
export * from "./kit/chart-legend"
|
|
12
13
|
export * from "./kit/entries"
|
|
13
14
|
export * from "./kit/motion"
|
|
14
15
|
export * from "./kit/paints"
|
|
16
|
+
export * from "./kit/pie-legend"
|
|
17
|
+
export * from "./kit/polar"
|
|
15
18
|
export * from "./kit/vertical-axis"
|
|
16
19
|
export * from "@motionscript/sdk/component"
|
package/src/index.ts
CHANGED
package/src/kit/cartesian.ts
CHANGED
|
@@ -1,62 +1,96 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Rect,
|
|
3
|
+
RotatedBox,
|
|
4
|
+
Text,
|
|
5
|
+
textOps,
|
|
6
|
+
type ChildView,
|
|
7
|
+
type Fill,
|
|
8
|
+
type FillResolved,
|
|
9
|
+
type Insets,
|
|
10
|
+
type Node2D,
|
|
11
|
+
type Size2D,
|
|
12
|
+
type TextStyle,
|
|
13
|
+
} from "@motionscript/sdk"
|
|
2
14
|
|
|
3
|
-
import {
|
|
15
|
+
import type { CartesianStyle, LegendPosition } from "./style"
|
|
4
16
|
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
17
|
+
/** Space between the frame's rows and columns: the plot and the legend, the axes and the legend. */
|
|
18
|
+
export function frameGap(style: Pick<CartesianStyle, "fontSize">): number {
|
|
19
|
+
return Math.round(style.fontSize * 0.9)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Room above the plot for the top value marker, which is centred on the top grid line. */
|
|
23
|
+
export function headroom(style: Pick<CartesianStyle, "fontSize">): number {
|
|
24
|
+
return Math.ceil(style.fontSize * 0.7)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** How far an end marker of a horizontal value axis may hang past the plot: half a short caption. */
|
|
28
|
+
export function overhang(style: Pick<CartesianStyle, "fontSize">): number {
|
|
29
|
+
return Math.ceil(style.fontSize * 1.1)
|
|
30
|
+
}
|
|
11
31
|
|
|
12
32
|
interface CartesianParts {
|
|
13
|
-
|
|
33
|
+
/** The axis beside the plot, on its left. */
|
|
34
|
+
readonly left?: Node2D
|
|
35
|
+
/** An invisible twin of `left` under the plot, so the row there starts at the plot's left edge. */
|
|
36
|
+
readonly ghost?: Node2D
|
|
14
37
|
/** The box the grid and the marks share. */
|
|
15
38
|
readonly plot: Node2D
|
|
16
|
-
/**
|
|
17
|
-
readonly
|
|
18
|
-
|
|
19
|
-
readonly
|
|
39
|
+
/** The axis under the plot. */
|
|
40
|
+
readonly bottom?: Node2D
|
|
41
|
+
readonly legend?: Node2D
|
|
42
|
+
readonly legendAt: LegendPosition
|
|
43
|
+
readonly gap: () => number
|
|
44
|
+
readonly padding: () => Insets
|
|
20
45
|
}
|
|
21
46
|
|
|
22
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* The frame the line and bar charts share. A legend above or below is centred
|
|
49
|
+
* on the whole chart; one on the right is a column beside the plot. An axis the
|
|
50
|
+
* style turns off is simply not here, and the plot takes its room.
|
|
51
|
+
*/
|
|
23
52
|
export function cartesianFrame(parts: CartesianParts): Node2D {
|
|
53
|
+
const top = new Rect({ flow: "horizontal", width: "fill", height: "fill", children: [...(parts.left ? [parts.left] : []), parts.plot] })
|
|
54
|
+
const rows: Node2D[] = [top as unknown as Node2D]
|
|
55
|
+
if (parts.bottom) {
|
|
56
|
+
rows.push(new Rect({ flow: "horizontal", width: "fill", height: "hug", children: [...(parts.ghost ? [parts.ghost] : []), parts.bottom] }) as unknown as Node2D)
|
|
57
|
+
}
|
|
58
|
+
const body = new Rect({ flow: "vertical", width: "fill", height: "fill", children: rows })
|
|
59
|
+
const legend = parts.legend && parts.legendAt !== "none" ? parts.legend : undefined
|
|
60
|
+
const main = new Rect({
|
|
61
|
+
flow: "horizontal",
|
|
62
|
+
width: "fill",
|
|
63
|
+
height: "fill",
|
|
64
|
+
align: "center",
|
|
65
|
+
gap: parts.gap,
|
|
66
|
+
children: [body, ...(legend && parts.legendAt === "right" ? [legend] : [])],
|
|
67
|
+
})
|
|
68
|
+
const across = (node: Node2D) => new Rect({ flow: "horizontal", width: "fill", height: "hug", align: "center", children: [node] })
|
|
24
69
|
return new Rect({
|
|
25
70
|
flow: "vertical",
|
|
26
71
|
width: "fill",
|
|
27
72
|
height: "fill",
|
|
28
|
-
|
|
73
|
+
gap: parts.gap,
|
|
74
|
+
padding: parts.padding,
|
|
29
75
|
children: [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
width: "fill",
|
|
34
|
-
children: [parts.gutter, new Rect({ flow: "vertical", width: "fill", gap: 20, children: [...parts.below] })],
|
|
35
|
-
}),
|
|
76
|
+
...(legend && parts.legendAt === "top" ? [across(legend)] : []),
|
|
77
|
+
main,
|
|
78
|
+
...(legend && parts.legendAt === "bottom" ? [across(legend)] : []),
|
|
36
79
|
],
|
|
37
80
|
}) as unknown as Node2D
|
|
38
81
|
}
|
|
39
82
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
export function cartesianViews(children: readonly ChildView[]): CartesianViews | null {
|
|
49
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
50
|
-
const frame = children[i]
|
|
51
|
-
if (!Rect.is(frame)) continue
|
|
52
|
-
const [top, bottom] = frame.children
|
|
53
|
-
const axis = top?.children[0]
|
|
54
|
-
const plot = top?.children[1]
|
|
55
|
-
const below = bottom?.children[1]
|
|
56
|
-
if (!axis || !VerticalAxis.is(axis) || !plot || !below) continue
|
|
57
|
-
return { axis, plot: plot.children, below: below.children }
|
|
83
|
+
/** Every view under `children`, depth first in build order: the axis beside the plot comes before its twin, which holds no parts. */
|
|
84
|
+
export function chartViews(children: readonly ChildView[]): ChildView[] {
|
|
85
|
+
const out: ChildView[] = []
|
|
86
|
+
const walk = (views: readonly ChildView[]) => {
|
|
87
|
+
for (const view of views) {
|
|
88
|
+
out.push(view)
|
|
89
|
+
walk(view.children)
|
|
90
|
+
}
|
|
58
91
|
}
|
|
59
|
-
|
|
92
|
+
walk(children)
|
|
93
|
+
return out
|
|
60
94
|
}
|
|
61
95
|
|
|
62
96
|
/** A part's laid-out size; zero before it is built or laid out. */
|
|
@@ -64,3 +98,47 @@ export function laidOutSize(node: Node2D | undefined): Size2D {
|
|
|
64
98
|
const bounds = node?.layoutBounds
|
|
65
99
|
return { width: bounds?.width ?? 0, height: bounds?.height ?? 0 }
|
|
66
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* A text style as props bound to `read`, so a style tween reaches text built
|
|
104
|
+
* once. Only the keys set at build are bound: an unset family stays unset, so
|
|
105
|
+
* the host's default face still applies.
|
|
106
|
+
*/
|
|
107
|
+
export function boundText(read: () => TextStyle): Record<string, unknown> {
|
|
108
|
+
const keys = Object.keys(textOps.styleProps(read())) as (keyof TextStyle)[]
|
|
109
|
+
const props: Record<string, unknown> = {}
|
|
110
|
+
for (const key of keys) props[key] = () => read()[key]
|
|
111
|
+
return props
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface GhostSpec {
|
|
115
|
+
readonly captions: () => readonly string[]
|
|
116
|
+
/** How many captions the real axis built. */
|
|
117
|
+
readonly count: number
|
|
118
|
+
readonly text: () => TextStyle
|
|
119
|
+
readonly title: string
|
|
120
|
+
readonly titleText: () => TextStyle
|
|
121
|
+
readonly tickPad: () => number
|
|
122
|
+
readonly titlePad: () => number
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* An invisible column as wide as a vertical axis built from the same captions:
|
|
127
|
+
* the widest caption plus the tick padding, and the rotated title before it. A
|
|
128
|
+
* plain tree, so a command looking for the axis never finds this instead.
|
|
129
|
+
*/
|
|
130
|
+
export function columnGhost(spec: GhostSpec): Node2D {
|
|
131
|
+
const captions = Array.from({ length: spec.count }, (_, i) => new Text({ ...boundText(spec.text), text: () => spec.captions()[i] ?? "" }))
|
|
132
|
+
const children: Node2D[] = []
|
|
133
|
+
if (spec.title !== "") {
|
|
134
|
+
children.push(new RotatedBox({ angle: -90, width: "hug", height: "hug", children: new Text({ ...boundText(spec.titleText), text: spec.title }) }) as unknown as Node2D)
|
|
135
|
+
}
|
|
136
|
+
children.push(new Rect({ flow: "vertical", width: "hug", height: "hug", padding: () => ({ right: spec.tickPad() }), children: captions }) as unknown as Node2D)
|
|
137
|
+
return new Rect({ opacity: 0, flow: "horizontal", gap: spec.titlePad, width: "hug", height: 0, children }) as unknown as Node2D
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** `fill` with every layer's opacity scaled by `k`. */
|
|
141
|
+
export function fade(fill: Fill, k: number): Fill {
|
|
142
|
+
const layers = (Array.isArray(fill) ? fill : [fill]) as FillResolved[]
|
|
143
|
+
return layers.map((layer) => (typeof layer === "object" && layer !== null ? { ...layer, opacity: ((layer as { opacity?: number }).opacity ?? 1) * k } : layer)) as Fill
|
|
144
|
+
}
|