@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
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { Rect, Types, component2d, easeOut, } from "@motionscript/sdk";
|
|
1
|
+
import { Rect, Text, Types, clamp, component2d, easeOut, } from "@motionscript/sdk";
|
|
2
2
|
import { formatNumber } from "@motionscript/sdk/component";
|
|
3
|
-
import { cartesianFrame,
|
|
4
|
-
import { ChartBorder } from "../kit/chart-border.js";
|
|
3
|
+
import { boundText, cartesianFrame, chartViews, columnGhost, fade, frameGap, headroom, laidOutSize, overhang } from "../kit/cartesian.js";
|
|
5
4
|
import { ChartCanvas } from "../kit/chart-canvas.js";
|
|
6
5
|
import { ChartLegend } from "../kit/chart-legend.js";
|
|
7
6
|
import { SERIES_ARG, asParts, fresh } from "../kit/entries.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { SPOTLIGHT_DIM, formatMarker, lerpAxisDomain, lerpAxisEntry, markerStep, markerValues,
|
|
11
|
-
import {
|
|
7
|
+
import { HorizontalAxis } from "../kit/horizontal-axis.js";
|
|
8
|
+
import { DEFAULT_MARKERS, ENTER_SHARE, beats, legendAway } from "../kit/motion.js";
|
|
9
|
+
import { SPOTLIGHT_DIM, formatMarker, lerpAxisDomain, lerpAxisEntry, markerStep, markerValues, niceRange, niceTicks, resolveAxisDomain, settleAxisDomain, } from "../kit/shared.js";
|
|
10
|
+
import { CARTESIAN_GRID, CHART_COLORS, CHART_LEGEND, CHART_STYLE_FAMILY, CHART_STYLE_PRESETS, CHART_TEXT, axisConfigOf, legendConfigOf, seriesFill, valueText, } from "../kit/style.js";
|
|
11
|
+
import { VerticalAxis, axisPads } from "../kit/vertical-axis.js";
|
|
12
|
+
import { ChartArea, ChartLine, traceX } from "./chart-line.js";
|
|
12
13
|
import { ChartRegion } from "./chart-region.js";
|
|
13
|
-
import {
|
|
14
|
-
import { DEFAULT_CHART_THEME, lerpChartTheme, resolveChartTheme, resolveRegionBox, resolveYScale, seriesPoints, } from "./shared.js";
|
|
14
|
+
import { LINE_LABELS, LINE_MARKS, resolveRegionBox, stackExtent, stackSeries, } from "./shared.js";
|
|
15
15
|
import { resolveXScale } from "./x-scale.js";
|
|
16
|
+
const LINE_STYLE = Types.style(CHART_STYLE_FAMILY, { colors: CHART_COLORS, text: CHART_TEXT, grid: CARTESIAN_GRID, marks: LINE_MARKS, labels: LINE_LABELS, legend: CHART_LEGEND }, { presets: CHART_STYLE_PRESETS });
|
|
16
17
|
/** One argument, two controls: the commands take a `PlotRegion`, so the x and y spans a host draws are declared inside it. */
|
|
17
18
|
const REGION_ARG = Types.custom(undefined, {
|
|
18
19
|
kind: "region",
|
|
@@ -21,52 +22,86 @@ const REGION_ARG = Types.custom(undefined, {
|
|
|
21
22
|
y: { kind: "range", default: false, defaultMin: 0, defaultMax: 100 },
|
|
22
23
|
},
|
|
23
24
|
});
|
|
25
|
+
/** The share of the y axis a percent stack reads to. */
|
|
26
|
+
const PERCENT = [0, 100];
|
|
24
27
|
/** What the x column is (a number, an instant, or a place in the file), and so how rows are positioned, ticked and captioned. */
|
|
25
28
|
function columnOf(s, format = s.xAxis.format) {
|
|
26
29
|
return resolveXScale(s.data, s.xField, format);
|
|
27
30
|
}
|
|
31
|
+
/** Every series as drawn; `weighted` false reads each at full share, as the scale does so the ticks hold still while a series sinks out. */
|
|
32
|
+
function stacksOf(s, weighted = true) {
|
|
33
|
+
return stackSeries(s.data, columnOf(s), s.series, s.stack, weighted ? s.weights : undefined);
|
|
34
|
+
}
|
|
28
35
|
/** The explicit range and ticks, else the column's extent and ticks: only a numeric column has round numbers between its samples. */
|
|
29
36
|
function drawnX(s, domain) {
|
|
30
37
|
const column = columnOf(s, domain.format);
|
|
31
38
|
const range = domain.range ?? column.extent;
|
|
32
39
|
return { ...domain, range, ticks: domain.ticks ?? column.ticks(range, DEFAULT_MARKERS - 1) };
|
|
33
40
|
}
|
|
41
|
+
/** The y range the data asks for: a percent stack reads to 100; areas and stacks keep zero on the scale. */
|
|
42
|
+
function autoY(s, divisions) {
|
|
43
|
+
const [lo, hi] = stackExtent(stacksOf(s, false));
|
|
44
|
+
if (s.stack === "percent" && lo >= 0)
|
|
45
|
+
return PERCENT;
|
|
46
|
+
const zero = s.stack !== "none" || (s.style?.area ?? "none") !== "none";
|
|
47
|
+
return niceRange(zero ? [Math.min(0, lo), Math.max(0, hi)] : [lo, hi], divisions);
|
|
48
|
+
}
|
|
34
49
|
/** Auto-ranging steps into one fewer segment than the marker count, so the tidy bounds frame the ticks drawn. */
|
|
35
50
|
function drawnY(s, domain) {
|
|
36
51
|
const divisions = (domain.ticks?.length ?? DEFAULT_MARKERS) - 1;
|
|
37
|
-
const range =
|
|
38
|
-
|
|
52
|
+
const range = domain.range ?? autoY(s, divisions);
|
|
53
|
+
const even = domain.range !== undefined || (s.stack === "percent" && range === PERCENT);
|
|
54
|
+
return { ...domain, range, ticks: domain.ticks ?? (even ? markerValues(range, DEFAULT_MARKERS - 1) : niceTicks(range, DEFAULT_MARKERS - 1)) };
|
|
39
55
|
}
|
|
40
56
|
/** The last domains worked out from each data array, and what else they were read from. */
|
|
41
57
|
const domainsDrawn = new WeakMap();
|
|
42
58
|
/** Both axes as drawn now. */
|
|
43
59
|
export function lineChartDomains(s) {
|
|
44
|
-
const
|
|
45
|
-
const kept = domainsDrawn.get(data);
|
|
46
|
-
if (kept && kept.
|
|
60
|
+
const key = [s.xField, s.xAxis, s.yAxis, s.series, s.stack, s.style?.area];
|
|
61
|
+
const kept = domainsDrawn.get(s.data);
|
|
62
|
+
if (kept && kept.key.every((value, i) => value === key[i]))
|
|
47
63
|
return kept.domains;
|
|
48
|
-
}
|
|
49
64
|
const domains = {
|
|
50
|
-
x: settleAxisDomain(xAxis, (d) => drawnX(s, d)),
|
|
51
|
-
y: settleAxisDomain(yAxis, (d) => drawnY(s, d)),
|
|
65
|
+
x: settleAxisDomain(s.xAxis, (d) => drawnX(s, d)),
|
|
66
|
+
y: settleAxisDomain(s.yAxis, (d) => drawnY(s, d)),
|
|
52
67
|
};
|
|
53
|
-
domainsDrawn.set(data, {
|
|
68
|
+
domainsDrawn.set(s.data, { key, domains });
|
|
54
69
|
return domains;
|
|
55
70
|
}
|
|
56
|
-
/**
|
|
57
|
-
function
|
|
58
|
-
const series = s.series[index];
|
|
59
|
-
if (!series)
|
|
60
|
-
return [];
|
|
61
|
-
const points = seriesPoints(s.data, columnOf(s), series);
|
|
62
|
-
if (points.length === 0)
|
|
63
|
-
return [];
|
|
64
|
-
const { width: w, height: h } = plot;
|
|
71
|
+
/** Data to the plot's centred pixel space (origin at its centre, y-up). */
|
|
72
|
+
function mapper(domains, plot) {
|
|
65
73
|
const [xMin, xMax] = domains.x.range;
|
|
66
74
|
const [yMin, yMax] = domains.y.range;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
return {
|
|
76
|
+
x: (x) => (xMax <= xMin ? 0 : ((x - xMin) / (xMax - xMin) - 0.5) * plot.width),
|
|
77
|
+
y: (y) => (yMax === yMin ? 0 : ((y - yMin) / (yMax - yMin) - 0.5) * plot.height),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const NO_PIXELS = { points: [], base: [], values: [] };
|
|
81
|
+
/** Series `index` in the plot's centred pixel space. */
|
|
82
|
+
function seriesPixels(s, domains, index, plot) {
|
|
83
|
+
const stack = stacksOf(s)[index];
|
|
84
|
+
if (!stack)
|
|
85
|
+
return NO_PIXELS;
|
|
86
|
+
const map = mapper(domains, plot);
|
|
87
|
+
const toPx = ([x, y]) => ({ x: map.x(x), y: map.y(y) });
|
|
88
|
+
return {
|
|
89
|
+
points: stack.points.map(toPx),
|
|
90
|
+
base: stack.base ? stack.base.map(toPx) : [],
|
|
91
|
+
values: stack.points.map(([, y], k) => y - (stack.base?.[k]?.[1] ?? 0)),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Where the areas fill down to: zero, held inside the scale. */
|
|
95
|
+
function baselineValue(domains) {
|
|
96
|
+
const [lo, hi] = domains.y.range;
|
|
97
|
+
return clamp(0, Math.min(lo, hi), Math.max(lo, hi));
|
|
98
|
+
}
|
|
99
|
+
/** Whether anything plotted runs past the scale, so the plot clips it: a zoom, or an author's range narrower than the data. */
|
|
100
|
+
function overflows(s, domains) {
|
|
101
|
+
const [ylo, yhi] = stackExtent(stacksOf(s, false));
|
|
102
|
+
const [xlo, xhi] = columnOf(s).extent;
|
|
103
|
+
const within = (v, [a, b]) => v >= Math.min(a, b) - 1e-9 && v <= Math.max(a, b) + 1e-9;
|
|
104
|
+
return !(within(ylo, domains.y.range) && within(yhi, domains.y.range) && within(xlo, domains.x.range) && within(xhi, domains.x.range));
|
|
70
105
|
}
|
|
71
106
|
/** `region` in centred pixel space against the live scales, so it travels with a zoom. */
|
|
72
107
|
function regionBox(s, domains, plot) {
|
|
@@ -97,19 +132,25 @@ function axesAt(from, to, t) {
|
|
|
97
132
|
...(to.y && { yAxis: lerpAxisDomain(from.y, to.y, t) }),
|
|
98
133
|
};
|
|
99
134
|
}
|
|
135
|
+
/** A value as the y axis writes it: the author's pattern, a share in a percent stack, else the bare reading at the ticks' precision. */
|
|
136
|
+
function yLabel(s, value, ticks) {
|
|
137
|
+
const format = s.yAxis.format;
|
|
138
|
+
if (format !== undefined)
|
|
139
|
+
return formatNumber(value, format);
|
|
140
|
+
const text = formatMarker(value, markerStep(ticks));
|
|
141
|
+
return s.stack === "percent" ? `${text}%` : text;
|
|
142
|
+
}
|
|
143
|
+
function weightOf(s, index) {
|
|
144
|
+
return Math.max(0, s.weights[index] ?? 1);
|
|
145
|
+
}
|
|
100
146
|
/** The parts a line chart's commands drive, found through its children. */
|
|
101
147
|
function partsOf(children) {
|
|
102
|
-
const
|
|
103
|
-
if (!frame)
|
|
104
|
-
return null;
|
|
105
|
-
const hAxis = frame.below.find(HorizontalAxis.is);
|
|
106
|
-
if (!VerticalAxis.is(frame.axis) || !hAxis)
|
|
107
|
-
return null;
|
|
148
|
+
const views = chartViews(children);
|
|
108
149
|
return {
|
|
109
|
-
vAxis:
|
|
110
|
-
hAxis,
|
|
111
|
-
lines:
|
|
112
|
-
legend:
|
|
150
|
+
vAxis: views.find(VerticalAxis.is) ?? null,
|
|
151
|
+
hAxis: views.find(HorizontalAxis.is) ?? null,
|
|
152
|
+
lines: views.filter(ChartLine.is),
|
|
153
|
+
legend: views.find(ChartLegend.is) ?? null,
|
|
113
154
|
};
|
|
114
155
|
}
|
|
115
156
|
function clearing(timeline, state) {
|
|
@@ -121,21 +162,38 @@ function clearing(timeline, state) {
|
|
|
121
162
|
// `region` is held until the retraction lands, or there would be nothing left to retract.
|
|
122
163
|
return timeline.animate((t) => (t >= 1 ? { region: null, regionReveal: 0 } : { region, regionReveal: from * (1 - t) }));
|
|
123
164
|
}
|
|
124
|
-
|
|
165
|
+
/**
|
|
166
|
+
* Show or hide series `index` with its legend entry. Apart, the line retraces;
|
|
167
|
+
* in a stack it sinks into the series below as its share falls to nothing, so
|
|
168
|
+
* the ones above settle rather than leaving a gap.
|
|
169
|
+
*/
|
|
170
|
+
function toggling(timeline, state, children, index, show) {
|
|
125
171
|
const parts = partsOf(children);
|
|
126
|
-
const line = parts
|
|
127
|
-
if (!
|
|
172
|
+
const line = parts.lines[index];
|
|
173
|
+
if (!line)
|
|
128
174
|
return timeline.wait(timeline.duration);
|
|
175
|
+
const legend = parts.legend ? [parts.legend.run(show ? "showSeries" : "hideSeries", { index })] : [];
|
|
176
|
+
if (state.stack === "none") {
|
|
177
|
+
return timeline.together([line.run("enter", show ? { from: 0, to: 1 } : { from: 1, to: 0 }), ...legend]);
|
|
178
|
+
}
|
|
179
|
+
const from = state.series.map((_, i) => weightOf(state, i));
|
|
180
|
+
const start = from[index] ?? 1;
|
|
181
|
+
const target = show ? 1 : 0;
|
|
129
182
|
return timeline.together([
|
|
130
|
-
|
|
131
|
-
|
|
183
|
+
timeline.animate((t) => {
|
|
184
|
+
const weights = [...from];
|
|
185
|
+
weights[index] = start + (target - start) * timeline.ease(t);
|
|
186
|
+
return { weights };
|
|
187
|
+
}, { eased: false }),
|
|
188
|
+
line.run("hold", { shown: show }),
|
|
189
|
+
...legend,
|
|
132
190
|
]);
|
|
133
191
|
}
|
|
134
192
|
/**
|
|
135
|
-
* A multi-series line chart composed of {@link ChartCanvas},
|
|
136
|
-
* {@link
|
|
137
|
-
* {@link ChartLegend}. Every part reads the chart's entries
|
|
138
|
-
* maps against the plot panel's own laid-out box.
|
|
193
|
+
* A multi-series line or area chart composed of {@link ChartCanvas}, the value
|
|
194
|
+
* and x axes, {@link ChartRegion}, a {@link ChartArea} and a {@link ChartLine}
|
|
195
|
+
* per series, and {@link ChartLegend}. Every part reads the chart's entries
|
|
196
|
+
* through bindings and maps against the plot panel's own laid-out box.
|
|
139
197
|
*
|
|
140
198
|
* Resting state is the finished chart; `enter` arms every part on its first
|
|
141
199
|
* frame. A `to` of an axis `range` or `ticks` tweens from the domain as drawn
|
|
@@ -148,114 +206,204 @@ export const LineChart = component2d({
|
|
|
148
206
|
// Not `x`: every node already has an `x`, its position.
|
|
149
207
|
xField: "",
|
|
150
208
|
series: fresh(() => [], { kind: "series" }),
|
|
209
|
+
/** `stacked`: each series sits on the sum of those before it; `percent`: every row reads as shares of 100. */
|
|
210
|
+
stack: Types.oneOf(["none", "stacked", "percent"]),
|
|
151
211
|
xAxis: Types.custom({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
|
|
152
212
|
yAxis: Types.custom({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
|
|
153
|
-
|
|
213
|
+
style: LINE_STYLE,
|
|
154
214
|
/** The spotlit slice in data units, or `null`. The box snaps; {@link regionReveal} animates it. */
|
|
155
215
|
region: Types.custom(null),
|
|
156
|
-
/** How far the `region` call-out is drawn in, `[0, 1]`: rules grow, wash fades up, out-of-region stretches fade
|
|
216
|
+
/** How far the `region` call-out is drawn in, `[0, 1]`: rules grow, wash fades up, out-of-region stretches fade. */
|
|
157
217
|
regionReveal: 0,
|
|
158
|
-
/** Read as the chart is built: the legend is a part the chart builds or doesn't. */
|
|
159
|
-
showLegend: Types.boolean(true, { animatable: false }),
|
|
160
218
|
/** The domain `zoomTo` first left, so `resetZoom` restores it, auto-derived ticks included. */
|
|
161
219
|
home: Types.custom(null, { private: true }),
|
|
220
|
+
/** Each series' share of a stack, seeded from `enabled`: a weight rather than a switch, so a hidden series sinks out. */
|
|
221
|
+
weights: Types.custom([], { private: true }),
|
|
222
|
+
/** Whether the plot clips its marks, from a zoom's first frame until a reset lands. */
|
|
223
|
+
clipped: Types.boolean(false, { private: true }),
|
|
162
224
|
},
|
|
163
225
|
// A node made of parts that fill would otherwise hug them.
|
|
164
|
-
defaults: {
|
|
226
|
+
defaults: {
|
|
227
|
+
width: "fill",
|
|
228
|
+
height: "fill",
|
|
229
|
+
weights: (state) => state.series.map((s) => ((s.enabled ?? true) ? 1 : 0)),
|
|
230
|
+
},
|
|
165
231
|
allowedChildren: [],
|
|
166
232
|
children: (state) => {
|
|
167
|
-
const
|
|
233
|
+
const style = () => state.style;
|
|
168
234
|
const domains = () => lineChartDomains(state);
|
|
169
235
|
let plot;
|
|
170
236
|
const plotSize = () => laidOutSize(plot);
|
|
237
|
+
const pixels = (i) => seriesPixels(state, domains(), i, plotSize());
|
|
171
238
|
const enabled = state.series.map((s) => s.enabled ?? true);
|
|
239
|
+
const built = state.style;
|
|
240
|
+
const yConfig = () => axisConfigOf(style(), "y");
|
|
241
|
+
const xConfig = () => axisConfigOf(style(), "x");
|
|
242
|
+
const yCaptions = () => {
|
|
243
|
+
const ticks = domains().y.ticks;
|
|
244
|
+
return ticks.map((value) => yLabel(state, value, ticks));
|
|
245
|
+
};
|
|
172
246
|
// Bound, not snapshotted, so a rescale re-labels and re-places the row as it
|
|
173
247
|
// tweens. The marker *count* is read once, which is why `zoomTo` keeps it.
|
|
174
|
-
const vAxis =
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
248
|
+
const vAxis = built.showYAxis
|
|
249
|
+
? new VerticalAxis({
|
|
250
|
+
range: () => domains().y.range,
|
|
251
|
+
markers: () => domains().y.ticks,
|
|
252
|
+
captions: yCaptions,
|
|
253
|
+
axis: yConfig,
|
|
254
|
+
label: state.yAxis.label ?? "",
|
|
255
|
+
width: "hug",
|
|
256
|
+
height: "fill",
|
|
257
|
+
})
|
|
258
|
+
: undefined;
|
|
259
|
+
const hAxis = built.showXAxis
|
|
260
|
+
? new HorizontalAxis({
|
|
261
|
+
range: () => domains().x.range,
|
|
262
|
+
markers: () => domains().x.ticks,
|
|
263
|
+
// A month or category tick is captioned with its cell (`Jan`), not the instant it parsed to.
|
|
264
|
+
captions: () => {
|
|
265
|
+
const column = columnOf(state);
|
|
266
|
+
return domains().x.ticks.map((value) => column.label(value));
|
|
267
|
+
},
|
|
268
|
+
axis: xConfig,
|
|
269
|
+
label: state.xAxis.label ?? "",
|
|
270
|
+
overhang: () => ({ left: overhang(style()), right: overhang(style()) }),
|
|
271
|
+
width: "fill",
|
|
272
|
+
height: "hug",
|
|
273
|
+
})
|
|
274
|
+
: undefined;
|
|
275
|
+
const ghost = vAxis && hAxis
|
|
276
|
+
? columnGhost({
|
|
277
|
+
captions: yCaptions,
|
|
278
|
+
count: lineChartDomains(state).y.ticks.length,
|
|
279
|
+
text: () => yConfig().labelStyle,
|
|
280
|
+
title: state.yAxis.label ?? "",
|
|
281
|
+
titleText: () => yConfig().titleStyle,
|
|
282
|
+
tickPad: () => axisPads(yConfig(), "right").tick,
|
|
283
|
+
titlePad: () => axisPads(yConfig(), "right").title,
|
|
284
|
+
})
|
|
285
|
+
: undefined;
|
|
286
|
+
const spotlight = () => regionBox(state, domains(), plotSize());
|
|
287
|
+
const dimOpacity = () => 1 - (1 - SPOTLIGHT_DIM) * state.regionReveal;
|
|
200
288
|
const lines = state.series.map((s, i) => new ChartLine({
|
|
201
|
-
points: () =>
|
|
202
|
-
|
|
289
|
+
points: () => pixels(i).points,
|
|
290
|
+
curve: () => style().curve,
|
|
291
|
+
stroke: () => ({ weight: style().strokeWidth, fill: seriesFill(style(), s, i), align: "center", cap: "round", join: "round" }),
|
|
292
|
+
dotSize: () => (style().dots ? style().dotSize : 0),
|
|
293
|
+
dotFill: () => seriesFill(style(), s, i),
|
|
203
294
|
enabled: enabled[i],
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
dimOpacity: () => 1 - (1 - theme().region.dimOpacity) * state.regionReveal,
|
|
295
|
+
spotlight,
|
|
296
|
+
dimOpacity,
|
|
207
297
|
}));
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
298
|
+
const areas = state.series.map((s, i) => new ChartArea({
|
|
299
|
+
points: () => pixels(i).points,
|
|
300
|
+
base: () => pixels(i).base,
|
|
301
|
+
baseline: () => mapper(domains(), plotSize()).y(baselineValue(domains())),
|
|
302
|
+
curve: () => style().curve,
|
|
303
|
+
area: () => style().area,
|
|
304
|
+
fill: () => seriesFill(style(), s, i),
|
|
305
|
+
areaOpacity: () => style().areaOpacity,
|
|
306
|
+
// The line's own trace, switch and fade, so the two arrive, leave and dim as one.
|
|
307
|
+
progress: () => lines[i].progress,
|
|
308
|
+
enabled: () => lines[i].enabled,
|
|
309
|
+
opacity: () => lines[i].opacity,
|
|
310
|
+
spotlight,
|
|
311
|
+
dimOpacity,
|
|
312
|
+
}));
|
|
313
|
+
const labels = [];
|
|
314
|
+
if (built.values !== "none") {
|
|
315
|
+
const below = built.values === "below";
|
|
316
|
+
state.series.forEach((_, i) => {
|
|
317
|
+
state.data.forEach((_row, k) => {
|
|
318
|
+
const line = lines[i];
|
|
319
|
+
const label = new Text({
|
|
320
|
+
...boundText(() => ({ ...valueText(style()), fontSize: Math.round(style().fontSize * 0.8) })),
|
|
321
|
+
text: () => {
|
|
322
|
+
const value = pixels(i).values[k];
|
|
323
|
+
return value === undefined ? "" : yLabel(state, value, domains().y.ticks);
|
|
324
|
+
},
|
|
325
|
+
// Held inside the plot, so a figure on an end point does not run into the axis beside it.
|
|
326
|
+
x: () => {
|
|
327
|
+
const half = (plotSize().width - label.layoutBounds.width) / 2;
|
|
328
|
+
return clamp(pixels(i).points[k]?.x ?? 0, -Math.max(0, half), Math.max(0, half));
|
|
329
|
+
},
|
|
330
|
+
y: () => {
|
|
331
|
+
const point = pixels(i).points[k];
|
|
332
|
+
const clear = label.layoutBounds.height / 2 + (style().dots ? style().dotSize / 2 : style().strokeWidth / 2) + Math.round(style().fontSize * 0.25);
|
|
333
|
+
return (point?.y ?? 0) + (below ? -clear : clear);
|
|
334
|
+
},
|
|
335
|
+
// In once the trace has passed its point.
|
|
336
|
+
opacity: () => {
|
|
337
|
+
const point = pixels(i).points[k];
|
|
338
|
+
if (!line.enabled || !point)
|
|
339
|
+
return 0;
|
|
340
|
+
const reach = traceX(pixels(i).points, line.progress);
|
|
341
|
+
return line.opacity * (reach === Infinity ? 1 : clamp((reach - point.x) / 24, 0, 1));
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
labels.push(label);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
218
347
|
}
|
|
348
|
+
const legendAt = built.legend;
|
|
349
|
+
const legend = legendAt !== "none"
|
|
350
|
+
? new ChartLegend({
|
|
351
|
+
series: state.series,
|
|
352
|
+
swatches: () => state.series.map((s, i) => seriesFill(style(), s, i)),
|
|
353
|
+
legend: () => legendConfigOf(style()),
|
|
354
|
+
flow: legendAt === "right" ? "vertical" : "horizontal",
|
|
355
|
+
})
|
|
356
|
+
: undefined;
|
|
219
357
|
plot = new Rect({
|
|
220
|
-
clip:
|
|
358
|
+
clip: () => state.clipped || overflows(state, domains()),
|
|
221
359
|
children: [
|
|
222
360
|
new ChartCanvas({
|
|
223
|
-
fill: () =>
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
yGridStroke: () => theme().yAxis.lineStyle,
|
|
361
|
+
fill: () => style().background,
|
|
362
|
+
xGridStroke: () => style().verticalGrid,
|
|
363
|
+
yGridStroke: () => style().horizontalGrid,
|
|
227
364
|
xMarkers: () => domains().x.ticks,
|
|
228
365
|
yMarkers: () => domains().y.ticks,
|
|
229
366
|
xExtent: () => domains().x.range,
|
|
230
367
|
yExtent: () => domains().y.range,
|
|
368
|
+
axisStroke: () => style().axisStroke,
|
|
369
|
+
xAxisAt: () => baselineValue(domains()),
|
|
370
|
+
yAxisAt: () => domains().x.range[0],
|
|
231
371
|
}),
|
|
232
|
-
// Under the
|
|
372
|
+
// Under the marks, so the highlight does not tint the stretch it picks out.
|
|
233
373
|
new ChartRegion({
|
|
234
|
-
box:
|
|
235
|
-
stroke: () =>
|
|
236
|
-
fill: () =>
|
|
374
|
+
box: spotlight,
|
|
375
|
+
stroke: () => ({ weight: 2, fill: fade(style().foreground, 0.7), dash: [14, 12], cap: "butt", align: "center" }),
|
|
376
|
+
fill: () => fade(style().foreground, 0.06),
|
|
237
377
|
reveal: () => state.regionReveal,
|
|
238
378
|
}),
|
|
379
|
+
...areas,
|
|
239
380
|
...lines,
|
|
240
|
-
|
|
381
|
+
...labels,
|
|
241
382
|
],
|
|
242
383
|
});
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
384
|
+
return asParts([
|
|
385
|
+
cartesianFrame({
|
|
386
|
+
left: vAxis,
|
|
387
|
+
ghost,
|
|
388
|
+
plot,
|
|
389
|
+
bottom: hAxis,
|
|
390
|
+
legend,
|
|
391
|
+
legendAt,
|
|
392
|
+
gap: () => frameGap(style()),
|
|
393
|
+
padding: () => ({
|
|
394
|
+
top: headroom(style()),
|
|
395
|
+
right: hAxis ? overhang(style()) : 0,
|
|
396
|
+
left: hAxis && !vAxis ? overhang(style()) : 0,
|
|
397
|
+
bottom: 0,
|
|
398
|
+
}),
|
|
399
|
+
}),
|
|
400
|
+
]);
|
|
253
401
|
},
|
|
254
402
|
commands: {
|
|
255
403
|
/**
|
|
256
|
-
* Axes stagger in, lines trace left to right, the legend
|
|
257
|
-
* `duration` is the whole entrance; a chart with a `region`
|
|
258
|
-
* call-out in the final quarter.
|
|
404
|
+
* Axes stagger in, lines (and their areas) trace left to right, the legend
|
|
405
|
+
* fades up last. `duration` is the whole entrance; a chart with a `region`
|
|
406
|
+
* lands its call-out in the final quarter.
|
|
259
407
|
*/
|
|
260
408
|
enter: {
|
|
261
409
|
args: {},
|
|
@@ -264,8 +412,6 @@ export const LineChart = component2d({
|
|
|
264
412
|
easing: easeOut(),
|
|
265
413
|
run: (timeline, state, children) => {
|
|
266
414
|
const parts = partsOf(children);
|
|
267
|
-
if (!parts)
|
|
268
|
-
return timeline.wait(timeline.duration);
|
|
269
415
|
const duration = timeline.duration;
|
|
270
416
|
const revealing = hasCallout(state) && state.regionReveal === 0;
|
|
271
417
|
const body = revealing ? duration * 0.75 : duration;
|
|
@@ -273,17 +419,20 @@ export const LineChart = component2d({
|
|
|
273
419
|
const traced = body * ENTER_SHARE.marks;
|
|
274
420
|
const { legend } = parts;
|
|
275
421
|
const at0 = (piece) => ({ at: 0, duration: 0, piece });
|
|
422
|
+
const away = legendAway(state.style.legend);
|
|
276
423
|
// The first frame arms what every later beat moves: a beat ahead of the
|
|
277
424
|
// playhead writes nothing. A beat with nothing to do still takes its share.
|
|
278
425
|
return beats(timeline, [
|
|
279
426
|
...(revealing ? [at0(timeline.animate(() => ({ regionReveal: 0 })))] : []),
|
|
280
427
|
...parts.lines.map((line) => at0(line.to({ progress: 0 }))),
|
|
281
|
-
...(legend ? [at0(legend.to({ opacity: 0,
|
|
282
|
-
{ at: 0, duration: axes, piece: parts.vAxis.run("enter", { slide: 24 }) },
|
|
283
|
-
{ at: 0, duration: axes, piece: parts.hAxis.run("enter", { slide: 24 }) },
|
|
428
|
+
...(legend ? [at0(legend.to({ opacity: 0, ...away }))] : []),
|
|
429
|
+
...(parts.vAxis ? [{ at: 0, duration: axes, piece: parts.vAxis.run("enter", { slide: 24 }) }] : []),
|
|
430
|
+
...(parts.hAxis ? [{ at: 0, duration: axes, piece: parts.hAxis.run("enter", { slide: 24 }) }] : []),
|
|
284
431
|
...parts.lines.map((line) => ({ at: axes, duration: traced, piece: line.run("enter") })),
|
|
285
|
-
...(legend ? [{ at: axes + traced, duration: body * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, y: 0 }) }] : []),
|
|
432
|
+
...(legend ? [{ at: axes + traced, duration: body * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, x: 0, y: 0 }) }] : []),
|
|
286
433
|
...(revealing ? [{ at: body, duration: duration * 0.25, piece: timeline.animate((t) => ({ regionReveal: t })) }] : []),
|
|
434
|
+
// Takes the whole slot even with no parts to move.
|
|
435
|
+
{ at: 0, duration, piece: timeline.wait(duration) },
|
|
287
436
|
]);
|
|
288
437
|
},
|
|
289
438
|
},
|
|
@@ -330,7 +479,7 @@ export const LineChart = component2d({
|
|
|
330
479
|
...(region.x && { x: { ...from.x, ...rescale(region.x, from.x.ticks) } }),
|
|
331
480
|
...(region.y && { y: { ...from.y, ...rescale(region.y, from.y.ticks) } }),
|
|
332
481
|
};
|
|
333
|
-
return timeline.animate((t) => ({ home, ...axesAt(from, to, t) }));
|
|
482
|
+
return timeline.animate((t) => ({ home, clipped: true, ...axesAt(from, to, t) }));
|
|
334
483
|
},
|
|
335
484
|
},
|
|
336
485
|
/** Back to the domain the first zoom left; a hold if it never zoomed. */
|
|
@@ -342,33 +491,32 @@ export const LineChart = component2d({
|
|
|
342
491
|
if (!home)
|
|
343
492
|
return timeline.wait(timeline.duration);
|
|
344
493
|
const from = lineChartDomains(state);
|
|
345
|
-
return timeline.animate((t) => ({ home: null, ...axesAt(from, home, t) }));
|
|
494
|
+
return timeline.animate((t) => ({ home: null, clipped: t < 1, ...axesAt(from, home, t) }));
|
|
346
495
|
},
|
|
347
496
|
},
|
|
348
|
-
/**
|
|
497
|
+
/** Bring series `index` in, with its legend entry growing in: traced, or in a stack risen out of the series below. */
|
|
349
498
|
showSeries: {
|
|
350
499
|
args: { seriesIndex: SERIES_ARG },
|
|
351
500
|
easing: easeOut(),
|
|
352
|
-
run: (timeline,
|
|
501
|
+
run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, true),
|
|
353
502
|
},
|
|
354
|
-
/**
|
|
503
|
+
/** The mirror of `showSeries`. */
|
|
355
504
|
hideSeries: {
|
|
356
505
|
args: { seriesIndex: SERIES_ARG },
|
|
357
506
|
easing: easeOut(),
|
|
358
|
-
run: (timeline,
|
|
507
|
+
run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, false),
|
|
359
508
|
},
|
|
360
|
-
/** Fade every other series, line and legend entry, to focus on `index`. The legend dims in place, so the row does not reflow. */
|
|
509
|
+
/** Fade every other series, line, area and legend entry, to focus on `index`. The legend dims in place, so the row does not reflow. */
|
|
361
510
|
spotlightSeries: {
|
|
362
511
|
args: { seriesIndex: SERIES_ARG },
|
|
363
512
|
easing: easeOut(),
|
|
364
513
|
run: (timeline, _state, children) => {
|
|
365
514
|
const parts = partsOf(children);
|
|
366
|
-
if (!parts)
|
|
367
|
-
return timeline.wait(timeline.duration);
|
|
368
515
|
const index = timeline.args.seriesIndex;
|
|
369
516
|
return timeline.together([
|
|
370
517
|
...parts.lines.map((line, i) => line.to({ opacity: i === index ? 1 : SPOTLIGHT_DIM })),
|
|
371
518
|
...(parts.legend ? [parts.legend.run("spotlightSeries", { index })] : []),
|
|
519
|
+
timeline.wait(timeline.duration),
|
|
372
520
|
]);
|
|
373
521
|
},
|
|
374
522
|
},
|