@motionscript/charts 0.1.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 +38 -0
- package/README.md +98 -28
- package/dist/bar-chart/bar-chart.d.ts +242 -119
- package/dist/bar-chart/bar-chart.d.ts.map +1 -1
- package/dist/bar-chart/bar-chart.js +231 -260
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/category-axis.d.ts +16 -38
- package/dist/bar-chart/category-axis.d.ts.map +1 -1
- package/dist/bar-chart/category-axis.js +59 -68
- package/dist/bar-chart/category-axis.js.map +1 -1
- package/dist/bar-chart/chart-bars.d.ts +60 -91
- package/dist/bar-chart/chart-bars.d.ts.map +1 -1
- package/dist/bar-chart/chart-bars.js +88 -125
- package/dist/bar-chart/chart-bars.js.map +1 -1
- package/dist/bar-chart/index.d.ts +5 -13
- package/dist/bar-chart/index.d.ts.map +1 -1
- package/dist/bar-chart/index.js +2 -9
- package/dist/bar-chart/index.js.map +1 -1
- package/dist/bar-chart/shared.d.ts +40 -132
- package/dist/bar-chart/shared.d.ts.map +1 -1
- package/dist/bar-chart/shared.js +70 -158
- 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 +8 -17
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +8 -17
- 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 -35
- package/dist/kit/cartesian.d.ts.map +1 -1
- package/dist/kit/cartesian.js +80 -42
- package/dist/kit/cartesian.js.map +1 -1
- package/dist/kit/chart-canvas.d.ts +37 -40
- package/dist/kit/chart-canvas.d.ts.map +1 -1
- package/dist/kit/chart-canvas.js +53 -47
- package/dist/kit/chart-canvas.js.map +1 -1
- package/dist/kit/chart-legend.d.ts +16 -59
- package/dist/kit/chart-legend.d.ts.map +1 -1
- package/dist/kit/chart-legend.js +38 -76
- package/dist/kit/chart-legend.js.map +1 -1
- package/dist/kit/entries.d.ts +2 -0
- package/dist/kit/entries.d.ts.map +1 -1
- package/dist/kit/entries.js +2 -0
- package/dist/kit/entries.js.map +1 -1
- package/dist/kit/horizontal-axis.d.ts +77 -0
- package/dist/kit/horizontal-axis.d.ts.map +1 -0
- package/dist/kit/horizontal-axis.js +99 -0
- package/dist/kit/horizontal-axis.js.map +1 -0
- package/dist/kit/motion.d.ts +16 -3
- package/dist/kit/motion.d.ts.map +1 -1
- package/dist/kit/motion.js +15 -1
- package/dist/kit/motion.js.map +1 -1
- package/dist/{pie-chart → kit}/pie-legend.d.ts +14 -29
- package/dist/kit/pie-legend.d.ts.map +1 -0
- package/dist/kit/pie-legend.js +93 -0
- 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 +60 -214
- package/dist/kit/shared.d.ts.map +1 -1
- package/dist/kit/shared.js +91 -291
- 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 +36 -45
- package/dist/kit/vertical-axis.d.ts.map +1 -1
- package/dist/kit/vertical-axis.js +69 -81
- package/dist/kit/vertical-axis.js.map +1 -1
- package/dist/line-chart/chart-line.d.ts +107 -40
- package/dist/line-chart/chart-line.d.ts.map +1 -1
- package/dist/line-chart/chart-line.js +172 -89
- package/dist/line-chart/chart-line.js.map +1 -1
- package/dist/line-chart/chart-region.d.ts +14 -27
- package/dist/line-chart/chart-region.d.ts.map +1 -1
- package/dist/line-chart/chart-region.js +13 -28
- package/dist/line-chart/chart-region.js.map +1 -1
- package/dist/line-chart/index.d.ts +5 -18
- package/dist/line-chart/index.d.ts.map +1 -1
- package/dist/line-chart/index.js +3 -16
- package/dist/line-chart/index.js.map +1 -1
- package/dist/line-chart/line-chart.d.ts +272 -119
- package/dist/line-chart/line-chart.d.ts.map +1 -1
- package/dist/line-chart/line-chart.js +309 -257
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/line-chart/shared.d.ts +73 -130
- package/dist/line-chart/shared.d.ts.map +1 -1
- package/dist/line-chart/shared.js +195 -156
- package/dist/line-chart/shared.js.map +1 -1
- package/dist/line-chart/x-scale.d.ts +16 -55
- package/dist/line-chart/x-scale.d.ts.map +1 -1
- package/dist/line-chart/x-scale.js +44 -111
- package/dist/line-chart/x-scale.js.map +1 -1
- package/dist/nodes.d.ts +3 -13
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +9 -13
- package/dist/nodes.js.map +1 -1
- package/dist/pie-chart/index.d.ts +3 -15
- package/dist/pie-chart/index.d.ts.map +1 -1
- package/dist/pie-chart/index.js +1 -11
- package/dist/pie-chart/index.js.map +1 -1
- package/dist/pie-chart/pie-chart.d.ts +147 -85
- package/dist/pie-chart/pie-chart.d.ts.map +1 -1
- package/dist/pie-chart/pie-chart.js +65 -86
- package/dist/pie-chart/pie-chart.js.map +1 -1
- package/dist/pie-chart/pie-ring.d.ts +65 -196
- package/dist/pie-chart/pie-ring.d.ts.map +1 -1
- package/dist/pie-chart/pie-ring.js +154 -292
- package/dist/pie-chart/pie-ring.js.map +1 -1
- package/dist/pie-chart/shared.d.ts +78 -172
- package/dist/pie-chart/shared.d.ts.map +1 -1
- package/dist/pie-chart/shared.js +54 -177
- 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 +274 -315
- package/src/bar-chart/category-axis.ts +53 -88
- package/src/bar-chart/chart-bars.ts +108 -205
- package/src/bar-chart/index.ts +8 -19
- package/src/bar-chart/shared.ts +81 -309
- package/src/engine.ts +8 -17
- package/src/index.ts +2 -0
- package/src/kit/cartesian.ts +118 -55
- package/src/kit/chart-canvas.ts +50 -80
- package/src/kit/chart-legend.ts +36 -97
- package/src/kit/entries.ts +4 -1
- package/src/kit/horizontal-axis.ts +114 -0
- package/src/kit/motion.ts +18 -2
- package/src/{pie-chart → kit}/pie-legend.ts +25 -65
- package/src/kit/polar.ts +54 -0
- package/src/kit/shared.ts +104 -487
- package/src/kit/style.ts +257 -0
- package/src/kit/vertical-axis.ts +81 -115
- package/src/line-chart/chart-line.ts +185 -91
- package/src/line-chart/chart-region.ts +14 -31
- package/src/line-chart/index.ts +14 -22
- package/src/line-chart/line-chart.ts +377 -315
- package/src/line-chart/shared.ts +229 -304
- package/src/line-chart/x-scale.ts +44 -121
- package/src/nodes.ts +9 -13
- package/src/pie-chart/index.ts +3 -35
- package/src/pie-chart/pie-chart.ts +83 -134
- package/src/pie-chart/pie-ring.ts +162 -368
- package/src/pie-chart/shared.ts +92 -379
- 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-TJNHLQGV.js +0 -2
- package/dist/browser/chunks/chunk-TJNHLQGV.js.map +0 -7
- package/dist/kit/chart-border.d.ts +0 -27
- package/dist/kit/chart-border.d.ts.map +0 -1
- package/dist/kit/chart-border.js +0 -28
- package/dist/kit/chart-border.js.map +0 -1
- package/dist/line-chart/horizontal-axis.d.ts +0 -79
- package/dist/line-chart/horizontal-axis.d.ts.map +0 -1
- package/dist/line-chart/horizontal-axis.js +0 -129
- 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 +0 -117
- package/dist/pie-chart/pie-legend.js.map +0 -1
- package/src/kit/chart-border.ts +0 -50
- package/src/line-chart/horizontal-axis.ts +0 -151
|
@@ -1,23 +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
|
-
import { asParts, fresh } from "../kit/entries.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { SPOTLIGHT_DIM, formatMarker, lerpAxisDomain, lerpAxisEntry, markerStep, markerValues,
|
|
11
|
-
import {
|
|
6
|
+
import { SERIES_ARG, asParts, fresh } from "../kit/entries.js";
|
|
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
|
-
|
|
17
|
-
* The argument every per-series command takes: an index into `series`. Its own
|
|
18
|
-
* kind rather than `number` so a host can offer the series by name.
|
|
19
|
-
*/
|
|
20
|
-
const SERIES_ARG = Types.custom(0, { kind: "series", min: 0, step: 1 });
|
|
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 });
|
|
21
17
|
/** One argument, two controls: the commands take a `PlotRegion`, so the x and y spans a host draws are declared inside it. */
|
|
22
18
|
const REGION_ARG = Types.custom(undefined, {
|
|
23
19
|
kind: "region",
|
|
@@ -26,355 +22,424 @@ const REGION_ARG = Types.custom(undefined, {
|
|
|
26
22
|
y: { kind: "range", default: false, defaultMin: 0, defaultMax: 100 },
|
|
27
23
|
},
|
|
28
24
|
});
|
|
29
|
-
/**
|
|
30
|
-
const
|
|
31
|
-
/**
|
|
32
|
-
const DEFAULT_MARKERS = 5;
|
|
33
|
-
/**
|
|
34
|
-
* How `enter` divides its duration between its three beats: the axes arriving,
|
|
35
|
-
* the lines tracing, and the legend coming up. Shares of the whole, so they must
|
|
36
|
-
* sum to 1 — the entrance takes exactly the time it was given.
|
|
37
|
-
*/
|
|
38
|
-
const AXES_SHARE = 0.35;
|
|
39
|
-
const LINES_SHARE = 0.5;
|
|
40
|
-
const LEGEND_SHARE = 0.15;
|
|
41
|
-
/**
|
|
42
|
-
* What the x column *is* — a number, an instant, or a place in the file — and
|
|
43
|
-
* with it how each row is positioned, ticked and captioned. See `x-scale.ts`.
|
|
44
|
-
*/
|
|
25
|
+
/** The share of the y axis a percent stack reads to. */
|
|
26
|
+
const PERCENT = [0, 100];
|
|
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. */
|
|
45
28
|
function columnOf(s, format = s.xAxis.format) {
|
|
46
29
|
return resolveXScale(s.data, s.xField, format);
|
|
47
30
|
}
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*/
|
|
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
|
+
}
|
|
35
|
+
/** The explicit range and ticks, else the column's extent and ticks: only a numeric column has round numbers between its samples. */
|
|
53
36
|
function drawnX(s, domain) {
|
|
54
37
|
const column = columnOf(s, domain.format);
|
|
55
38
|
const range = domain.range ?? column.extent;
|
|
56
39
|
return { ...domain, range, ticks: domain.ticks ?? column.ticks(range, DEFAULT_MARKERS - 1) };
|
|
57
40
|
}
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
}
|
|
49
|
+
/** Auto-ranging steps into one fewer segment than the marker count, so the tidy bounds frame the ticks drawn. */
|
|
63
50
|
function drawnY(s, domain) {
|
|
64
51
|
const divisions = (domain.ticks?.length ?? DEFAULT_MARKERS) - 1;
|
|
65
|
-
const range =
|
|
66
|
-
|
|
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)) };
|
|
67
55
|
}
|
|
68
56
|
/** The last domains worked out from each data array, and what else they were read from. */
|
|
69
57
|
const domainsDrawn = new WeakMap();
|
|
70
|
-
/** Both axes as
|
|
58
|
+
/** Both axes as drawn now. */
|
|
71
59
|
export function lineChartDomains(s) {
|
|
72
|
-
const
|
|
73
|
-
const kept = domainsDrawn.get(data);
|
|
74
|
-
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]))
|
|
75
63
|
return kept.domains;
|
|
76
|
-
}
|
|
77
64
|
const domains = {
|
|
78
|
-
x: settleAxisDomain(xAxis, (d) => drawnX(s, d)),
|
|
79
|
-
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)),
|
|
80
67
|
};
|
|
81
|
-
domainsDrawn.set(data, {
|
|
68
|
+
domainsDrawn.set(s.data, { key, domains });
|
|
82
69
|
return domains;
|
|
83
70
|
}
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
* the plot centre, y-up): x runs `[-w/2, +w/2]`, y runs `[-h/2, +h/2]`.
|
|
87
|
-
*/
|
|
88
|
-
function seriesPixels(s, domains, index, plot) {
|
|
89
|
-
const series = s.series[index];
|
|
90
|
-
if (!series)
|
|
91
|
-
return [];
|
|
92
|
-
const points = seriesPoints(s.data, columnOf(s), series);
|
|
93
|
-
if (points.length === 0)
|
|
94
|
-
return [];
|
|
95
|
-
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) {
|
|
96
73
|
const [xMin, xMax] = domains.x.range;
|
|
97
74
|
const [yMin, yMax] = domains.y.range;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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));
|
|
102
98
|
}
|
|
103
|
-
/**
|
|
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));
|
|
105
|
+
}
|
|
106
|
+
/** `region` in centred pixel space against the live scales, so it travels with a zoom. */
|
|
104
107
|
function regionBox(s, domains, plot) {
|
|
105
108
|
const { width: w, height: h } = plot;
|
|
106
109
|
return resolveRegionBox(s.region, { left: -w / 2, right: w / 2, bottom: -h / 2, top: h / 2 }, domains.x.range, domains.y.range);
|
|
107
110
|
}
|
|
108
|
-
/**
|
|
109
|
-
* Whether a call-out is authored at all. Not whether it resolves to pixels: a
|
|
110
|
-
* command is built before the first layout, and a region on a chart not yet
|
|
111
|
-
* measured is a real one. A region bounding neither axis is none, as `null` is.
|
|
112
|
-
*/
|
|
111
|
+
/** Whether a call-out is authored, not whether it resolves to pixels: a command is built before the first layout. A region bounding neither axis is none. */
|
|
113
112
|
function hasCallout(s) {
|
|
114
113
|
const region = s.region;
|
|
115
114
|
return region != null && (region.x !== undefined || region.y !== undefined);
|
|
116
115
|
}
|
|
117
|
-
/** The `region`
|
|
116
|
+
/** The `region` argument, or `{}`: a command is added before its arguments are filled in. */
|
|
118
117
|
function regionArg(region) {
|
|
119
118
|
return region != null && typeof region === "object" ? region : {};
|
|
120
119
|
}
|
|
121
|
-
/** A range
|
|
120
|
+
/** A range ordered low to high. */
|
|
122
121
|
function span([a, b]) {
|
|
123
122
|
return a <= b ? [a, b] : [b, a];
|
|
124
123
|
}
|
|
125
|
-
/**
|
|
126
|
-
* Rescale an axis to `range`, keeping the tick *count* it already has, so each
|
|
127
|
-
* marker slides from its old value to its new one instead of the row popping.
|
|
128
|
-
*/
|
|
124
|
+
/** Rescale to `range` keeping the tick count, so each marker slides rather than the row popping. */
|
|
129
125
|
function rescale(range, ticks) {
|
|
130
126
|
const to = span(range);
|
|
131
127
|
return { range: to, ticks: markerValues(to, Math.max(2, ticks.length) - 1) };
|
|
132
128
|
}
|
|
133
|
-
/** The axis writes of a move from `from` toward `to` at `t`. */
|
|
134
129
|
function axesAt(from, to, t) {
|
|
135
130
|
return {
|
|
136
131
|
...(to.x && { xAxis: lerpAxisDomain(from.x, to.x, t) }),
|
|
137
132
|
...(to.y && { yAxis: lerpAxisDomain(from.y, to.y, t) }),
|
|
138
133
|
};
|
|
139
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
|
+
}
|
|
140
146
|
/** The parts a line chart's commands drive, found through its children. */
|
|
141
147
|
function partsOf(children) {
|
|
142
|
-
const
|
|
143
|
-
if (!frame)
|
|
144
|
-
return null;
|
|
145
|
-
const hAxis = frame.below.find(HorizontalAxis.is);
|
|
146
|
-
if (!VerticalAxis.is(frame.axis) || !hAxis)
|
|
147
|
-
return null;
|
|
148
|
+
const views = chartViews(children);
|
|
148
149
|
return {
|
|
149
|
-
vAxis:
|
|
150
|
-
hAxis,
|
|
151
|
-
lines:
|
|
152
|
-
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,
|
|
153
154
|
};
|
|
154
155
|
}
|
|
155
|
-
/** Drop the call-out: the rules retract, the wash fades, and every series comes back to full. */
|
|
156
156
|
function clearing(timeline, state) {
|
|
157
157
|
const from = state.regionReveal;
|
|
158
158
|
const region = state.region;
|
|
159
|
-
// Nothing to retract, but the call still takes its slot
|
|
159
|
+
// Nothing to retract, but the call still takes its slot.
|
|
160
160
|
if (from === 0)
|
|
161
161
|
return timeline.animate(() => ({ region: null, regionReveal: 0 }), { eased: false });
|
|
162
162
|
// `region` is held until the retraction lands, or there would be nothing left to retract.
|
|
163
163
|
return timeline.animate((t) => (t >= 1 ? { region: null, regionReveal: 0 } : { region, regionReveal: from * (1 - t) }));
|
|
164
164
|
}
|
|
165
|
-
/**
|
|
166
|
-
|
|
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) {
|
|
167
171
|
const parts = partsOf(children);
|
|
168
|
-
const line = parts
|
|
169
|
-
if (!
|
|
172
|
+
const line = parts.lines[index];
|
|
173
|
+
if (!line)
|
|
170
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;
|
|
171
182
|
return timeline.together([
|
|
172
|
-
|
|
173
|
-
|
|
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,
|
|
174
190
|
]);
|
|
175
191
|
}
|
|
176
192
|
/**
|
|
177
|
-
* A multi-series line chart
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
* - {@link VerticalAxis} / {@link HorizontalAxis} — columns/rows of real text
|
|
182
|
-
* markers that stagger in, each carrying its own axis caption.
|
|
183
|
-
* - {@link ChartRegion} — the optional call-out marking a spotlit slice of the
|
|
184
|
-
* plot: dashed rules on its bounded edges, and every series faded outside it.
|
|
185
|
-
* - {@link ChartLine} (one per series) — the traced polylines over the canvas.
|
|
186
|
-
* - {@link ChartLegend} — a row of entries below the chart, one per series.
|
|
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.
|
|
187
197
|
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
* changes.
|
|
192
|
-
*
|
|
193
|
-
* **Resting state is the finished chart**: an editor paints a node's Initial
|
|
194
|
-
* state on a paused canvas. `enter` arms every part on its first frame.
|
|
195
|
-
*
|
|
196
|
-
* Past the entrance, `showSeries`/`hideSeries`/`spotlightSeries` pick out a
|
|
197
|
-
* *series*, `spotlightRegion` marks a *slice of the plot* without changing the
|
|
198
|
-
* scale, and `zoomTo` rescales the axes so a slice fills the plot. A spotlit
|
|
199
|
-
* region resolves against the live scales, so it travels with a zoom. A `to` of
|
|
200
|
-
* an axis `range` or `ticks` tweens from the domain as drawn (see
|
|
201
|
-
* {@link lerpAxisEntry}), so an auto axis rescales continuously.
|
|
198
|
+
* Resting state is the finished chart; `enter` arms every part on its first
|
|
199
|
+
* frame. A `to` of an axis `range` or `ticks` tweens from the domain as drawn
|
|
200
|
+
* (see {@link lerpAxisEntry}), so an auto axis rescales continuously.
|
|
202
201
|
*/
|
|
203
202
|
export const LineChart = component2d({
|
|
204
203
|
key: "lineChart",
|
|
205
204
|
state: {
|
|
206
205
|
data: fresh(() => [], { kind: "dataset" }),
|
|
206
|
+
// Not `x`: every node already has an `x`, its position.
|
|
207
207
|
xField: "",
|
|
208
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"]),
|
|
209
211
|
xAxis: Types.custom({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
|
|
210
212
|
yAxis: Types.custom({}, { resolve: resolveAxisDomain, lerp: lerpAxisEntry }),
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* The spotlit slice of the plot in data units, or `null` for none. The box
|
|
214
|
-
* snaps; the animation is {@link regionReveal} drawing the rules on.
|
|
215
|
-
*/
|
|
213
|
+
style: LINE_STYLE,
|
|
214
|
+
/** The spotlit slice in data units, or `null`. The box snaps; {@link regionReveal} animates it. */
|
|
216
215
|
region: Types.custom(null),
|
|
217
|
-
/**
|
|
218
|
-
* How far the `region` call-out is drawn in, `[0, 1]`: the rules grow out and
|
|
219
|
-
* the wash fades up as it climbs, while every line's out-of-region stretch
|
|
220
|
-
* fades to the theme's `region.dimOpacity`.
|
|
221
|
-
*/
|
|
216
|
+
/** How far the `region` call-out is drawn in, `[0, 1]`: rules grow, wash fades up, out-of-region stretches fade. */
|
|
222
217
|
regionReveal: 0,
|
|
223
|
-
/**
|
|
224
|
-
showLegend: Types.boolean(true, { animatable: false }),
|
|
225
|
-
/**
|
|
226
|
-
* The domain `zoomTo` first left, so `resetZoom` restores the chart as it
|
|
227
|
-
* stood, auto-derived ticks included. Cleared by `resetZoom`, so the next zoom
|
|
228
|
-
* remembers wherever the chart rests then.
|
|
229
|
-
*/
|
|
218
|
+
/** The domain `zoomTo` first left, so `resetZoom` restores it, auto-derived ticks included. */
|
|
230
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 }),
|
|
224
|
+
},
|
|
225
|
+
// A node made of parts that fill would otherwise hug them.
|
|
226
|
+
defaults: {
|
|
227
|
+
width: "fill",
|
|
228
|
+
height: "fill",
|
|
229
|
+
weights: (state) => state.series.map((s) => ((s.enabled ?? true) ? 1 : 0)),
|
|
231
230
|
},
|
|
232
|
-
// Its parts fill it; a node made of them would hug them.
|
|
233
|
-
defaults: { width: "fill", height: "fill" },
|
|
234
231
|
allowedChildren: [],
|
|
235
232
|
children: (state) => {
|
|
236
|
-
const
|
|
233
|
+
const style = () => state.style;
|
|
237
234
|
const domains = () => lineChartDomains(state);
|
|
238
235
|
let plot;
|
|
239
236
|
const plotSize = () => laidOutSize(plot);
|
|
237
|
+
const pixels = (i) => seriesPixels(state, domains(), i, plotSize());
|
|
240
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
|
+
};
|
|
241
246
|
// Bound, not snapshotted, so a rescale re-labels and re-places the row as it
|
|
242
247
|
// tweens. The marker *count* is read once, which is why `zoomTo` keeps it.
|
|
243
|
-
const vAxis =
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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;
|
|
271
288
|
const lines = state.series.map((s, i) => new ChartLine({
|
|
272
|
-
points: () =>
|
|
273
|
-
|
|
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),
|
|
274
294
|
enabled: enabled[i],
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
spotlight: () => regionBox(state, domains(), plotSize()),
|
|
278
|
-
// Ramped by the reveal that draws the rules: one signal, both halves.
|
|
279
|
-
dimOpacity: () => 1 - (1 - theme().region.dimOpacity) * state.regionReveal,
|
|
295
|
+
spotlight,
|
|
296
|
+
dimOpacity,
|
|
280
297
|
}));
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
+
});
|
|
294
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;
|
|
295
357
|
plot = new Rect({
|
|
296
|
-
clip:
|
|
358
|
+
clip: () => state.clipped || overflows(state, domains()),
|
|
297
359
|
children: [
|
|
298
360
|
new ChartCanvas({
|
|
299
|
-
fill: () =>
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
yGridStroke: () => theme().yAxis.lineStyle,
|
|
303
|
-
// A grid line under every axis marker; the canvas skips the edges.
|
|
361
|
+
fill: () => style().background,
|
|
362
|
+
xGridStroke: () => style().verticalGrid,
|
|
363
|
+
yGridStroke: () => style().horizontalGrid,
|
|
304
364
|
xMarkers: () => domains().x.ticks,
|
|
305
365
|
yMarkers: () => domains().y.ticks,
|
|
306
366
|
xExtent: () => domains().x.range,
|
|
307
367
|
yExtent: () => domains().y.range,
|
|
368
|
+
axisStroke: () => style().axisStroke,
|
|
369
|
+
xAxisAt: () => baselineValue(domains()),
|
|
370
|
+
yAxisAt: () => domains().x.range[0],
|
|
308
371
|
}),
|
|
309
|
-
//
|
|
310
|
-
// tinted by the highlight meant to pick it out.
|
|
372
|
+
// Under the marks, so the highlight does not tint the stretch it picks out.
|
|
311
373
|
new ChartRegion({
|
|
312
|
-
box:
|
|
313
|
-
stroke: () =>
|
|
314
|
-
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),
|
|
315
377
|
reveal: () => state.regionReveal,
|
|
316
378
|
}),
|
|
379
|
+
...areas,
|
|
317
380
|
...lines,
|
|
318
|
-
|
|
381
|
+
...labels,
|
|
319
382
|
],
|
|
320
383
|
});
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
+
]);
|
|
331
401
|
},
|
|
332
402
|
commands: {
|
|
333
403
|
/**
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
-
* the legend dropped and transparent.
|
|
338
|
-
*
|
|
339
|
-
* `duration` is the **whole** entrance; the beats are shares of it. A chart
|
|
340
|
-
* authored with a `region` lands its call-out last, 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.
|
|
341
407
|
*/
|
|
342
408
|
enter: {
|
|
343
409
|
args: {},
|
|
344
|
-
// Held from
|
|
410
|
+
// Held from arrival, so it never shows finished ahead of its draw.
|
|
345
411
|
entrance: true,
|
|
346
412
|
easing: easeOut(),
|
|
347
413
|
run: (timeline, state, children) => {
|
|
348
414
|
const parts = partsOf(children);
|
|
349
|
-
if (!parts)
|
|
350
|
-
return timeline.wait(timeline.duration);
|
|
351
415
|
const duration = timeline.duration;
|
|
352
416
|
const revealing = hasCallout(state) && state.regionReveal === 0;
|
|
353
417
|
const body = revealing ? duration * 0.75 : duration;
|
|
354
|
-
const axes = body *
|
|
355
|
-
const traced = body *
|
|
418
|
+
const axes = body * ENTER_SHARE.axes;
|
|
419
|
+
const traced = body * ENTER_SHARE.marks;
|
|
356
420
|
const { legend } = parts;
|
|
357
421
|
const at0 = (piece) => ({ at: 0, duration: 0, piece });
|
|
422
|
+
const away = legendAway(state.style.legend);
|
|
358
423
|
// The first frame arms what every later beat moves: a beat ahead of the
|
|
359
424
|
// playhead writes nothing. A beat with nothing to do still takes its share.
|
|
360
425
|
return beats(timeline, [
|
|
361
426
|
...(revealing ? [at0(timeline.animate(() => ({ regionReveal: 0 })))] : []),
|
|
362
427
|
...parts.lines.map((line) => at0(line.to({ progress: 0 }))),
|
|
363
|
-
...(legend ? [at0(legend.to({ opacity: 0,
|
|
364
|
-
{ at: 0, duration: axes, piece: parts.vAxis.run("enter", { slide: 24 }) },
|
|
365
|
-
{ 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 }) }] : []),
|
|
366
431
|
...parts.lines.map((line) => ({ at: axes, duration: traced, piece: line.run("enter") })),
|
|
367
|
-
...(legend ? [{ at: axes + traced, duration: body *
|
|
432
|
+
...(legend ? [{ at: axes + traced, duration: body * ENTER_SHARE.legend, piece: legend.to({ opacity: 1, x: 0, y: 0 }) }] : []),
|
|
368
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) },
|
|
369
436
|
]);
|
|
370
437
|
},
|
|
371
438
|
},
|
|
372
439
|
/**
|
|
373
|
-
* Spotlight a
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
* up, the old call-out retracts over the first half and the new one draws in
|
|
377
|
-
* over the second, each half eased on its own.
|
|
440
|
+
* Spotlight a slice of the plot in data units; either axis alone is a band.
|
|
441
|
+
* Over a region already up, the old one retracts in the first half and the
|
|
442
|
+
* new one draws in over the second, each half eased on its own.
|
|
378
443
|
*/
|
|
379
444
|
spotlightRegion: {
|
|
380
445
|
args: { region: REGION_ARG },
|
|
@@ -394,18 +459,13 @@ export const LineChart = component2d({
|
|
|
394
459
|
: { region, regionReveal: timeline.ease(t * 2 - 1) }), { eased: false });
|
|
395
460
|
},
|
|
396
461
|
},
|
|
397
|
-
/** Drop the
|
|
462
|
+
/** Drop the call-out. A no-op when nothing is spotlit, which still takes its slot. */
|
|
398
463
|
clearRegion: {
|
|
399
464
|
args: {},
|
|
400
465
|
easing: easeOut(),
|
|
401
466
|
run: (timeline, state) => clearing(timeline, state),
|
|
402
467
|
},
|
|
403
|
-
/**
|
|
404
|
-
* Zoom the plot to a slice of itself, given in **data units**: the named axes
|
|
405
|
-
* rescale so that slice fills the plot, the grid, both marker rows and every
|
|
406
|
-
* series following. An omitted axis is left as it is. The first zoom
|
|
407
|
-
* remembers the domain it left, for `resetZoom`.
|
|
408
|
-
*/
|
|
468
|
+
/** Rescale the named axes so a slice (data units) fills the plot; an omitted axis is left. The first zoom remembers the domain for `resetZoom`. */
|
|
409
469
|
zoomTo: {
|
|
410
470
|
args: { region: REGION_ARG },
|
|
411
471
|
easing: easeOut(),
|
|
@@ -419,10 +479,10 @@ export const LineChart = component2d({
|
|
|
419
479
|
...(region.x && { x: { ...from.x, ...rescale(region.x, from.x.ticks) } }),
|
|
420
480
|
...(region.y && { y: { ...from.y, ...rescale(region.y, from.y.ticks) } }),
|
|
421
481
|
};
|
|
422
|
-
return timeline.animate((t) => ({ home, ...axesAt(from, to, t) }));
|
|
482
|
+
return timeline.animate((t) => ({ home, clipped: true, ...axesAt(from, to, t) }));
|
|
423
483
|
},
|
|
424
484
|
},
|
|
425
|
-
/**
|
|
485
|
+
/** Back to the domain the first zoom left; a hold if it never zoomed. */
|
|
426
486
|
resetZoom: {
|
|
427
487
|
args: {},
|
|
428
488
|
easing: easeOut(),
|
|
@@ -431,40 +491,32 @@ export const LineChart = component2d({
|
|
|
431
491
|
if (!home)
|
|
432
492
|
return timeline.wait(timeline.duration);
|
|
433
493
|
const from = lineChartDomains(state);
|
|
434
|
-
return timeline.animate((t) => ({ home: null, ...axesAt(from, home, t) }));
|
|
494
|
+
return timeline.animate((t) => ({ home: null, clipped: t < 1, ...axesAt(from, home, t) }));
|
|
435
495
|
},
|
|
436
496
|
},
|
|
437
|
-
/**
|
|
438
|
-
* Show series `index`: **trace** its line in left to right, in lockstep with
|
|
439
|
-
* its legend entry growing in.
|
|
440
|
-
*/
|
|
497
|
+
/** Bring series `index` in, with its legend entry growing in: traced, or in a stack risen out of the series below. */
|
|
441
498
|
showSeries: {
|
|
442
499
|
args: { seriesIndex: SERIES_ARG },
|
|
443
500
|
easing: easeOut(),
|
|
444
|
-
run: (timeline,
|
|
501
|
+
run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, true),
|
|
445
502
|
},
|
|
446
|
-
/**
|
|
503
|
+
/** The mirror of `showSeries`. */
|
|
447
504
|
hideSeries: {
|
|
448
505
|
args: { seriesIndex: SERIES_ARG },
|
|
449
506
|
easing: easeOut(),
|
|
450
|
-
run: (timeline,
|
|
507
|
+
run: (timeline, state, children) => toggling(timeline, state, children, timeline.args.seriesIndex, false),
|
|
451
508
|
},
|
|
452
|
-
/**
|
|
453
|
-
* Fade every other series — each line **and its legend entry** — to focus on
|
|
454
|
-
* `index`, which comes back to full. The legend dims in place, so the row
|
|
455
|
-
* doesn't reflow.
|
|
456
|
-
*/
|
|
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. */
|
|
457
510
|
spotlightSeries: {
|
|
458
511
|
args: { seriesIndex: SERIES_ARG },
|
|
459
512
|
easing: easeOut(),
|
|
460
513
|
run: (timeline, _state, children) => {
|
|
461
514
|
const parts = partsOf(children);
|
|
462
|
-
if (!parts)
|
|
463
|
-
return timeline.wait(timeline.duration);
|
|
464
515
|
const index = timeline.args.seriesIndex;
|
|
465
516
|
return timeline.together([
|
|
466
517
|
...parts.lines.map((line, i) => line.to({ opacity: i === index ? 1 : SPOTLIGHT_DIM })),
|
|
467
518
|
...(parts.legend ? [parts.legend.run("spotlightSeries", { index })] : []),
|
|
519
|
+
timeline.wait(timeline.duration),
|
|
468
520
|
]);
|
|
469
521
|
},
|
|
470
522
|
},
|