@motionscript/charts 0.1.0-alpha.0 → 0.1.0-alpha.10
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 +105 -0
- package/NOTICE +49 -0
- package/README.md +99 -0
- package/dist/bar-chart/bar-chart.d.ts +133 -228
- package/dist/bar-chart/bar-chart.d.ts.map +1 -1
- package/dist/bar-chart/bar-chart.js +386 -565
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/category-axis.d.ts +35 -41
- package/dist/bar-chart/category-axis.d.ts.map +1 -1
- package/dist/bar-chart/category-axis.js +93 -115
- package/dist/bar-chart/category-axis.js.map +1 -1
- package/dist/bar-chart/chart-bars.d.ts +50 -70
- package/dist/bar-chart/chart-bars.d.ts.map +1 -1
- package/dist/bar-chart/chart-bars.js +155 -210
- package/dist/bar-chart/chart-bars.js.map +1 -1
- package/dist/bar-chart/index.d.ts +2 -2
- package/dist/bar-chart/index.d.ts.map +1 -1
- package/dist/bar-chart/index.js +1 -1
- package/dist/bar-chart/index.js.map +1 -1
- package/dist/bar-chart/shared.d.ts +7 -7
- package/dist/bar-chart/shared.d.ts.map +1 -1
- package/dist/bar-chart/shared.js +7 -7
- package/dist/bar-chart/shared.js.map +1 -1
- package/dist/browser/chunks/chunk-TJNHLQGV.js +2 -0
- package/dist/browser/chunks/chunk-TJNHLQGV.js.map +7 -0
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +3 -3
- package/dist/browser/kit.js +1 -1
- package/dist/browser/manifest.json +4 -3
- package/dist/engine.d.ts +10 -8
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +10 -8
- package/dist/engine.js.map +1 -1
- package/dist/kit/cartesian.d.ts +44 -0
- package/dist/kit/cartesian.d.ts.map +1 -0
- package/dist/kit/cartesian.js +59 -0
- package/dist/kit/cartesian.js.map +1 -0
- package/dist/kit/chart-border.d.ts +10 -11
- package/dist/kit/chart-border.d.ts.map +1 -1
- package/dist/kit/chart-border.js +15 -32
- package/dist/kit/chart-border.js.map +1 -1
- package/dist/kit/chart-canvas.d.ts +32 -21
- package/dist/kit/chart-canvas.d.ts.map +1 -1
- package/dist/kit/chart-canvas.js +33 -90
- package/dist/kit/chart-canvas.js.map +1 -1
- package/dist/kit/chart-legend.d.ts +123 -85
- package/dist/kit/chart-legend.d.ts.map +1 -1
- package/dist/kit/chart-legend.js +137 -186
- package/dist/kit/chart-legend.js.map +1 -1
- package/dist/kit/entries.d.ts +17 -0
- package/dist/kit/entries.d.ts.map +1 -0
- package/dist/kit/entries.js +19 -0
- package/dist/kit/entries.js.map +1 -0
- package/dist/kit/motion.d.ts +48 -0
- package/dist/kit/motion.d.ts.map +1 -0
- package/dist/kit/motion.js +63 -0
- package/dist/kit/motion.js.map +1 -0
- package/dist/kit/paints.d.ts +10 -0
- package/dist/kit/paints.d.ts.map +1 -0
- package/dist/kit/paints.js +12 -0
- package/dist/kit/paints.js.map +1 -0
- package/dist/kit/shared.d.ts +61 -10
- package/dist/kit/shared.d.ts.map +1 -1
- package/dist/kit/shared.js +108 -13
- package/dist/kit/shared.js.map +1 -1
- package/dist/kit/vertical-axis.d.ts +48 -61
- package/dist/kit/vertical-axis.d.ts.map +1 -1
- package/dist/kit/vertical-axis.js +97 -132
- package/dist/kit/vertical-axis.js.map +1 -1
- package/dist/line-chart/chart-line.d.ts +60 -77
- package/dist/line-chart/chart-line.d.ts.map +1 -1
- package/dist/line-chart/chart-line.js +100 -121
- package/dist/line-chart/chart-line.js.map +1 -1
- package/dist/line-chart/chart-region.d.ts +32 -33
- package/dist/line-chart/chart-region.d.ts.map +1 -1
- package/dist/line-chart/chart-region.js +31 -65
- package/dist/line-chart/chart-region.js.map +1 -1
- package/dist/line-chart/horizontal-axis.d.ts +58 -64
- package/dist/line-chart/horizontal-axis.d.ts.map +1 -1
- package/dist/line-chart/horizontal-axis.js +94 -123
- package/dist/line-chart/horizontal-axis.js.map +1 -1
- package/dist/line-chart/index.d.ts +3 -3
- package/dist/line-chart/index.d.ts.map +1 -1
- package/dist/line-chart/index.js +2 -2
- package/dist/line-chart/index.js.map +1 -1
- package/dist/line-chart/line-chart.d.ts +156 -303
- package/dist/line-chart/line-chart.d.ts.map +1 -1
- package/dist/line-chart/line-chart.js +410 -638
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/line-chart/shared.d.ts +7 -7
- package/dist/line-chart/shared.d.ts.map +1 -1
- package/dist/line-chart/shared.js +23 -8
- package/dist/line-chart/shared.js.map +1 -1
- package/dist/line-chart/x-scale.d.ts +4 -11
- package/dist/line-chart/x-scale.d.ts.map +1 -1
- package/dist/line-chart/x-scale.js +16 -5
- package/dist/line-chart/x-scale.js.map +1 -1
- package/dist/nodes.d.ts +6 -9
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +7 -9
- package/dist/nodes.js.map +1 -1
- package/dist/pie-chart/index.d.ts +2 -2
- package/dist/pie-chart/index.d.ts.map +1 -1
- package/dist/pie-chart/index.js +1 -1
- package/dist/pie-chart/index.js.map +1 -1
- package/dist/pie-chart/pie-chart.d.ts +108 -126
- package/dist/pie-chart/pie-chart.d.ts.map +1 -1
- package/dist/pie-chart/pie-chart.js +202 -271
- package/dist/pie-chart/pie-chart.js.map +1 -1
- package/dist/pie-chart/pie-legend.d.ts +84 -31
- package/dist/pie-chart/pie-legend.d.ts.map +1 -1
- package/dist/pie-chart/pie-legend.js +93 -107
- package/dist/pie-chart/pie-legend.js.map +1 -1
- package/dist/pie-chart/pie-ring.d.ts +160 -122
- package/dist/pie-chart/pie-ring.d.ts.map +1 -1
- package/dist/pie-chart/pie-ring.js +333 -361
- package/dist/pie-chart/pie-ring.js.map +1 -1
- package/dist/pie-chart/shared.d.ts +12 -6
- package/dist/pie-chart/shared.d.ts.map +1 -1
- package/dist/pie-chart/shared.js +14 -8
- package/dist/pie-chart/shared.js.map +1 -1
- package/package.json +7 -4
- package/src/bar-chart/bar-chart.ts +424 -686
- package/src/bar-chart/category-axis.ts +96 -111
- package/src/bar-chart/chart-bars.ts +178 -201
- package/src/bar-chart/index.ts +2 -2
- package/src/bar-chart/shared.ts +7 -7
- package/src/engine.ts +10 -8
- package/src/kit/cartesian.ts +81 -0
- package/src/kit/chart-border.ts +23 -34
- package/src/kit/chart-canvas.ts +45 -80
- package/src/kit/chart-legend.ts +185 -206
- package/src/kit/entries.ts +20 -0
- package/src/kit/motion.ts +90 -0
- package/src/kit/paints.ts +15 -0
- package/src/kit/shared.ts +120 -13
- package/src/kit/vertical-axis.ts +111 -141
- package/src/line-chart/chart-line.ts +103 -155
- package/src/line-chart/chart-region.ts +36 -81
- package/src/line-chart/horizontal-axis.ts +102 -153
- package/src/line-chart/index.ts +3 -3
- package/src/line-chart/line-chart.ts +463 -759
- package/src/line-chart/shared.ts +22 -8
- package/src/line-chart/x-scale.ts +20 -5
- package/src/nodes.ts +8 -10
- package/src/pie-chart/index.ts +2 -2
- package/src/pie-chart/pie-chart.ts +225 -303
- package/src/pie-chart/pie-legend.ts +125 -115
- package/src/pie-chart/pie-ring.ts +388 -443
- package/src/pie-chart/shared.ts +14 -8
- package/dist/browser/chunks/chunk-ZMBOBHYL.js +0 -2
- package/dist/browser/chunks/chunk-ZMBOBHYL.js.map +0 -7
package/src/kit/shared.ts
CHANGED
|
@@ -12,9 +12,8 @@
|
|
|
12
12
|
* that three times would mean three answers to "how does a chart round its axis"
|
|
13
13
|
* within one app.
|
|
14
14
|
*
|
|
15
|
-
* Kept renderer-agnostic (no `Node` imports) so
|
|
16
|
-
* mappings can build a partial theme without pulling the
|
|
17
|
-
* same way `line-chart-props.ts` always has.
|
|
15
|
+
* Kept renderer-agnostic (no `Node` imports) so a host's own inspector
|
|
16
|
+
* mappings can build a partial theme without pulling the components in.
|
|
18
17
|
*
|
|
19
18
|
* **Two conventions run through every resolver and lerp below**, inherited from
|
|
20
19
|
* the shape system's `resolveStroke`/`resolveShadow`:
|
|
@@ -46,7 +45,7 @@ import {
|
|
|
46
45
|
type Stroke,
|
|
47
46
|
type StrokeResolved,
|
|
48
47
|
type TextStyle,
|
|
49
|
-
} from "@motionscript/
|
|
48
|
+
} from "@motionscript/sdk"
|
|
50
49
|
|
|
51
50
|
/**
|
|
52
51
|
* One series of a chart: a column of the table, captioned and painted.
|
|
@@ -102,7 +101,7 @@ export interface AxisDomainConfig {
|
|
|
102
101
|
}
|
|
103
102
|
|
|
104
103
|
/**
|
|
105
|
-
* An axis domain's
|
|
104
|
+
* An axis domain entry's resolve: merges the author's partial input onto
|
|
106
105
|
* `previous`, so a later partial `.set({ yAxis: {...} })` patches
|
|
107
106
|
* `label`/`range`/`ticks` independently. `range`/`ticks` staying `undefined`
|
|
108
107
|
* after the merge is the auto-derive signal the scale getters read.
|
|
@@ -120,7 +119,7 @@ export function resolveAxisDomain(
|
|
|
120
119
|
}
|
|
121
120
|
|
|
122
121
|
/**
|
|
123
|
-
* An axis domain's
|
|
122
|
+
* An axis domain entry's lerp: interpolates `range`/`ticks` numerically
|
|
124
123
|
* (pairwise by index for `ticks`), so a `to` command that restates a range
|
|
125
124
|
* rescales the plot smoothly. `label`, and either field when one side is in
|
|
126
125
|
* auto-derive mode, step at the midpoint.
|
|
@@ -163,6 +162,81 @@ function lerpOptionalTicks(
|
|
|
163
162
|
return out
|
|
164
163
|
}
|
|
165
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Where a generic `to()` of an axis domain should land, given the domain as it
|
|
167
|
+
* is *drawn* (`drawn`, with `range` and `ticks` resolved off the data), or
|
|
168
|
+
* `undefined` when the request states neither a `range` nor `ticks` and the
|
|
169
|
+
* plain property tween will do.
|
|
170
|
+
*
|
|
171
|
+
* The stored domain of an auto axis has no `range` and no `ticks`, and
|
|
172
|
+
* {@link lerpAxisDomain} steps an unset side at the midpoint, so tweening the
|
|
173
|
+
* stored value from an auto axis is a cut. The charts therefore tween between
|
|
174
|
+
* resolved domains, as their zoom does; this builds the destination. A `range`
|
|
175
|
+
* given without `ticks` keeps the tick *count* the axis already draws, so each
|
|
176
|
+
* marker slides from its old value to its new one instead of the row popping.
|
|
177
|
+
*/
|
|
178
|
+
export function axisTweenTarget(
|
|
179
|
+
drawn: AxisDomainConfig,
|
|
180
|
+
requested: AxisDomainConfig | undefined
|
|
181
|
+
): AxisDomainConfig | undefined {
|
|
182
|
+
if (requested?.range === undefined && requested?.ticks === undefined) return undefined
|
|
183
|
+
const target = resolveAxisDomain(requested, drawn)
|
|
184
|
+
if (requested.ticks === undefined && requested.range !== undefined) {
|
|
185
|
+
const count = Math.max(2, drawn.ticks?.length ?? 2)
|
|
186
|
+
target.ticks = markerValues(requested.range, count - 1)
|
|
187
|
+
}
|
|
188
|
+
return target
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* An axis domain part-way through a tween that leaves or reaches a `range` or
|
|
193
|
+
* `ticks` left to the data: both ends as stored, and how far along. Only the
|
|
194
|
+
* chart knows what an auto side draws as, so {@link settleAxisDomain} resolves
|
|
195
|
+
* it there. Its own fields are the plain {@link lerpAxisDomain} of its ends, so
|
|
196
|
+
* a mapper merging onto one still reads a domain.
|
|
197
|
+
*/
|
|
198
|
+
export interface AxisDomainBlend extends AxisDomainConfig {
|
|
199
|
+
readonly blend: { readonly from: AxisDomainConfig; readonly to: AxisDomainConfig; readonly t: number }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function blendOf(domain: AxisDomainConfig): AxisDomainBlend["blend"] | undefined {
|
|
203
|
+
return (domain as Partial<AxisDomainBlend>).blend
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* A chart's axis entry tween: what `to({ yAxis: { range } })` moves through.
|
|
208
|
+
* Between two explicit domains it is {@link lerpAxisDomain}, and a restated
|
|
209
|
+
* `range` without `ticks` spreads explicit ticks across it, keeping their count.
|
|
210
|
+
* When either end leaves `range` or `ticks` to the data, the value between is an
|
|
211
|
+
* {@link AxisDomainBlend}, which the chart draws as the blend of what each end
|
|
212
|
+
* draws: a rescale from an auto axis is continuous, not a cut at the midpoint.
|
|
213
|
+
*/
|
|
214
|
+
export function lerpAxisEntry(from: AxisDomainConfig, to: AxisDomainConfig, t: number): AxisDomainConfig {
|
|
215
|
+
// The mapper merged the request onto `from`, so a field it left unstated is `from`'s own, by identity.
|
|
216
|
+
const rangeMoved = to.range !== from.range
|
|
217
|
+
const target = rangeMoved && to.range && to.ticks && to.ticks === from.ticks
|
|
218
|
+
? { ...to, ticks: markerValues(to.range, Math.max(2, to.ticks.length) - 1) }
|
|
219
|
+
: to
|
|
220
|
+
const blended = blendOf(from) !== undefined
|
|
221
|
+
const moved = blended || rangeMoved || target.ticks !== from.ticks
|
|
222
|
+
const auto = blended || from.range === undefined || from.ticks === undefined || target.range === undefined || target.ticks === undefined
|
|
223
|
+
if (!moved || !auto) return lerpAxisDomain(from, target, t)
|
|
224
|
+
if (t === 0) return from
|
|
225
|
+
if (t === 1) return target
|
|
226
|
+
const blend: AxisDomainBlend = { ...lerpAxisDomain(from, target, t), blend: { from, to: target, t } }
|
|
227
|
+
return blend
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* `domain` as the chart draws it: `drawn` resolves a plain domain's auto `range`
|
|
232
|
+
* and `ticks` off the data, and a blend is the lerp of what its two ends draw.
|
|
233
|
+
*/
|
|
234
|
+
export function settleAxisDomain(domain: AxisDomainConfig, drawn: (domain: AxisDomainConfig) => AxisDomainConfig): AxisDomainConfig {
|
|
235
|
+
const blend = blendOf(domain)
|
|
236
|
+
if (!blend) return drawn(domain)
|
|
237
|
+
return lerpAxisDomain(settleAxisDomain(blend.from, drawn), settleAxisDomain(blend.to, drawn), blend.t)
|
|
238
|
+
}
|
|
239
|
+
|
|
166
240
|
/**
|
|
167
241
|
* The low-level colour tokens the default themes are built from — a dark plot
|
|
168
242
|
* panel with warm-grey axis furniture.
|
|
@@ -203,7 +277,8 @@ export function clampUnit(value: number): number {
|
|
|
203
277
|
// ---- Row readers ---------------------------------------------------------
|
|
204
278
|
|
|
205
279
|
/**
|
|
206
|
-
* Read a numeric field off a row, defaulting missing
|
|
280
|
+
* Read a numeric field off a row, defaulting missing, non-numeric and non-finite
|
|
281
|
+
* values to 0.
|
|
207
282
|
*
|
|
208
283
|
* A column left as strings silently plots as 0, which is why the CSV parser's
|
|
209
284
|
* numeric auto-coercion matters — a quoted or unit-suffixed column won't plot
|
|
@@ -212,7 +287,9 @@ export function clampUnit(value: number): number {
|
|
|
212
287
|
export function num(row: DataRecord, field: string | undefined): number {
|
|
213
288
|
if (field === undefined) return 0
|
|
214
289
|
const v = row[field]
|
|
215
|
-
|
|
290
|
+
// A `NaN` or infinite cell is no more a measurement than a missing one: read
|
|
291
|
+
// as 0, it cannot poison the scale and turn every point into `NaN`.
|
|
292
|
+
return typeof v === "number" && Number.isFinite(v) ? v : 0
|
|
216
293
|
}
|
|
217
294
|
|
|
218
295
|
/**
|
|
@@ -650,10 +727,40 @@ export function resolvePalettePaint(
|
|
|
650
727
|
// ---- Scales and formatting -----------------------------------------------
|
|
651
728
|
|
|
652
729
|
|
|
653
|
-
/**
|
|
654
|
-
|
|
730
|
+
/**
|
|
731
|
+
* Format a marker value: integers plain, otherwise trimmed to one decimal, or to
|
|
732
|
+
* as many as the axis's tick `step` needs to tell neighbouring ticks apart.
|
|
733
|
+
*
|
|
734
|
+
* One decimal is the floor, so a step of 0.1 or more reads exactly as it always
|
|
735
|
+
* has; a finer step (a unit axis ticked every 0.05) gets two, instead of five
|
|
736
|
+
* ticks captioned `0.1 0.1 0.2 0.2 0.3`. Trailing zeros are dropped either way.
|
|
737
|
+
*/
|
|
738
|
+
export function formatMarker(value: number, step?: number): string {
|
|
655
739
|
if (Number.isInteger(value)) return String(value)
|
|
656
|
-
|
|
740
|
+
const decimals = Math.max(1, step === undefined ? 1 : decimalsFor(step))
|
|
741
|
+
return value.toFixed(decimals).replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "")
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** Decimal places needed to write `step` exactly, capped at six. */
|
|
745
|
+
function decimalsFor(step: number): number {
|
|
746
|
+
if (!(step > 0) || !Number.isFinite(step)) return 1
|
|
747
|
+
for (let d = 0; d <= 6; d++) {
|
|
748
|
+
const scaled = step * 10 ** d
|
|
749
|
+
if (Math.abs(scaled - Math.round(scaled)) < 1e-6 * Math.max(1, scaled)) return d
|
|
750
|
+
}
|
|
751
|
+
// An irrational-looking step (a third): as many places as separate the ticks.
|
|
752
|
+
return Math.min(6, Math.max(1, Math.ceil(-Math.log10(step)) + 1))
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/** The gap between neighbouring tick values, or `undefined` for fewer than two distinct ones. */
|
|
756
|
+
export function markerStep(markers: readonly number[]): number | undefined {
|
|
757
|
+
const sorted = [...markers].filter(Number.isFinite).sort((a, b) => a - b)
|
|
758
|
+
let step: number | undefined
|
|
759
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
760
|
+
const gap = sorted[i] - sorted[i - 1]
|
|
761
|
+
if (gap > 1e-12 && (step === undefined || gap < step)) step = gap
|
|
762
|
+
}
|
|
763
|
+
return step
|
|
657
764
|
}
|
|
658
765
|
|
|
659
766
|
/** Format a fraction in `[0, 1]` as a percentage: integers plain, else one decimal. */
|
|
@@ -670,6 +777,6 @@ export function markerValues([min, max]: Range, markerCount: number): number[] {
|
|
|
670
777
|
}
|
|
671
778
|
|
|
672
779
|
// Lifted into the engine, because a graph and a diagram want them too — see
|
|
673
|
-
// `@motionscript/
|
|
780
|
+
// `@motionscript/sdk/component`. Re-exported so a chart, and anything that
|
|
674
781
|
// copied one, still reaches them where it always did.
|
|
675
|
-
export { niceStep, enterBudget } from "@motionscript/
|
|
782
|
+
export { niceStep, enterBudget } from "@motionscript/sdk/component"
|
package/src/kit/vertical-axis.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
-
command,
|
|
3
|
-
Node2D,
|
|
4
2
|
Rect,
|
|
5
3
|
RotatedBox,
|
|
6
4
|
Text,
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Types,
|
|
6
|
+
component2d,
|
|
9
7
|
insetsOps,
|
|
10
8
|
textOps,
|
|
11
|
-
type
|
|
12
|
-
type
|
|
9
|
+
type NodeOf,
|
|
10
|
+
type Node2D,
|
|
13
11
|
type Node2DProps,
|
|
14
|
-
} from "@motionscript/
|
|
12
|
+
} from "@motionscript/sdk"
|
|
15
13
|
|
|
14
|
+
import { asParts, fresh } from "./entries"
|
|
15
|
+
import { armed, arrival, arrivalOf, arrivalsEntry, beats, frameEase, type Arrival } from "./motion"
|
|
16
16
|
import {
|
|
17
17
|
DEFAULT_Y_AXIS,
|
|
18
18
|
enterBudget,
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
type AxisConfig,
|
|
21
21
|
type Range,
|
|
22
22
|
} from "./shared"
|
|
23
|
-
import { at, compose, type Seekable } from "@motionscript/core/component"
|
|
24
23
|
|
|
25
24
|
export interface VerticalAxisProps extends Node2DProps {
|
|
26
25
|
/** `[min, max]` of the vertical scale the markers are positioned within. */
|
|
@@ -31,6 +30,12 @@ export interface VerticalAxisProps extends Node2DProps {
|
|
|
31
30
|
* the ends.
|
|
32
31
|
*/
|
|
33
32
|
markers: number[]
|
|
33
|
+
/**
|
|
34
|
+
* How each marker is captioned, one per entry of `markers` and in its order.
|
|
35
|
+
* Empty (the default), or short, captions a marker with its bare numeric
|
|
36
|
+
* reading; a chart with a number format on this axis writes them through it.
|
|
37
|
+
*/
|
|
38
|
+
captions: string[]
|
|
34
39
|
/** Marker/caption text style, grid stroke, and gutters — see {@link AxisConfig}. */
|
|
35
40
|
axis: AxisConfig
|
|
36
41
|
/**
|
|
@@ -44,9 +49,15 @@ export interface VerticalAxisProps extends Node2DProps {
|
|
|
44
49
|
/** How far a marker starts to the right of its slot when it enters, in px. */
|
|
45
50
|
const SLIDE = 24
|
|
46
51
|
|
|
47
|
-
/** How a tick value is captioned — see {@link
|
|
52
|
+
/** How a tick value is captioned — see {@link VerticalAxisProps.captions}. */
|
|
48
53
|
export type MarkerLabel = (value: number) => string
|
|
49
54
|
|
|
55
|
+
/** What a marker's caption reads: the one listed for its value, else its bare reading. */
|
|
56
|
+
export function markerCaption(s: { readonly markers: readonly number[]; readonly captions: readonly string[] }, value: number): string {
|
|
57
|
+
const index = s.markers.indexOf(value)
|
|
58
|
+
return (index >= 0 ? s.captions[index] : undefined) ?? formatMarker(value)
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
/**
|
|
51
62
|
* The numeric vertical axis shared by the cartesian charts: a **column of
|
|
52
63
|
* {@link Text} nodes**, one per marker value, running top-to-bottom (highest
|
|
@@ -60,73 +71,54 @@ export type MarkerLabel = (value: number) => string
|
|
|
60
71
|
* tweens `range`/`markers` live. Only the marker **count** is fixed at
|
|
61
72
|
* construction.
|
|
62
73
|
*
|
|
63
|
-
*
|
|
74
|
+
* `enter` staggers them in **bottom-to-top**: each marker starts pushed a
|
|
64
75
|
* little to the right and transparent, then fades and slides left into place.
|
|
65
76
|
* The resting state is the landed one, so a chart that is never told to enter
|
|
66
77
|
* still reads as a finished chart.
|
|
67
78
|
*/
|
|
68
|
-
export
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
* than the column of text nodes built for it — a slot with no value renders as
|
|
97
|
-
* empty text rather than repeating its neighbour.
|
|
98
|
-
*/
|
|
99
|
-
private valueAt(index: number): number | undefined {
|
|
100
|
-
return this.sortedMarkers[index]
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Tick `i`'s fraction across the live `range`, 0 at the bottom edge. */
|
|
104
|
-
private fracAt(index: number): number {
|
|
105
|
-
const v = this.valueAt(index)
|
|
106
|
-
if (v === undefined) return 0
|
|
107
|
-
const [min, max] = this.range
|
|
108
|
-
return max <= min ? 0 : (v - min) / (max - min)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
constructor(props?: NodeConfig<VerticalAxis, VerticalAxisProps>) {
|
|
112
|
-
super(props)
|
|
113
|
-
|
|
114
|
-
// Composed from plain props (range/markers/label) — no context needed, so
|
|
115
|
-
// it's built here. super() applied the props, so the reads below resolve.
|
|
116
|
-
const tickPad = insetsOps.resolve(this.axis.tickPadding).right
|
|
117
|
-
const titlePad = insetsOps.resolve(this.axis.titlePadding).right
|
|
79
|
+
export const VerticalAxis = component2d({
|
|
80
|
+
key: "verticalAxis",
|
|
81
|
+
state: {
|
|
82
|
+
range: fresh<Range>(() => [0, 1]),
|
|
83
|
+
markers: fresh<number[]>(() => []),
|
|
84
|
+
captions: fresh<string[]>(() => []),
|
|
85
|
+
axis: Types.custom<AxisConfig>(DEFAULT_Y_AXIS),
|
|
86
|
+
label: "",
|
|
87
|
+
arrivals: arrivalsEntry(),
|
|
88
|
+
},
|
|
89
|
+
// A plain node filled its parent; one made of its markers would hug them.
|
|
90
|
+
defaults: { width: "fill", height: "fill" },
|
|
91
|
+
children: (state) => {
|
|
92
|
+
// Composed from plain entries (range/markers/label), so it's built once here.
|
|
93
|
+
const tickPad = insetsOps.resolve(state.axis.tickPadding).right
|
|
94
|
+
const titlePad = insetsOps.resolve(state.axis.titlePadding).right
|
|
95
|
+
|
|
96
|
+
const row: Node2D[] = []
|
|
97
|
+
// The row's parent is the axis: its box is what the markers are placed in.
|
|
98
|
+
const box = () => (row[0]?.parent as Node2D | null | undefined)?.layoutBounds ?? { width: 0, height: 0 }
|
|
99
|
+
/** The live tick values, top → bottom (highest first) — re-read every frame. */
|
|
100
|
+
const sorted = () => [...state.markers].sort((a, b) => b - a)
|
|
101
|
+
/** Tick `i`'s fraction across the live `range`, 0 at the bottom edge. */
|
|
102
|
+
const fracAt = (value: number | undefined) => {
|
|
103
|
+
if (value === undefined) return 0
|
|
104
|
+
const [min, max] = state.range
|
|
105
|
+
return max <= min ? 0 : (value - min) / (max - min)
|
|
106
|
+
}
|
|
118
107
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
108
|
+
// One text node per marker, ordered top → bottom. A slot with no value (the
|
|
109
|
+
// live list is shorter than the column built for it) renders as empty text
|
|
110
|
+
// rather than repeating its neighbour.
|
|
111
|
+
const markers = state.markers.map((_, i) => {
|
|
112
|
+
const marker: Node2D = new Text({
|
|
113
|
+
...textOps.styleProps(state.axis.labelStyle),
|
|
122
114
|
text: () => {
|
|
123
|
-
const
|
|
124
|
-
return
|
|
115
|
+
const value = sorted()[i]
|
|
116
|
+
return value === undefined ? "" : markerCaption(state, value)
|
|
125
117
|
},
|
|
126
118
|
y: () => {
|
|
127
119
|
// Node2D space is y-up, so the fraction runs from the bottom.
|
|
128
|
-
const height =
|
|
129
|
-
const centred = (
|
|
120
|
+
const height = box().height
|
|
121
|
+
const centred = (fracAt(sorted()[i]) - 0.5) * height
|
|
130
122
|
// Held inside the axis's top edge — the backstop for a chart whose
|
|
131
123
|
// headroom is smaller than its marker text is tall, which a theme with
|
|
132
124
|
// much larger markers can arrange. Only the top, because what lies
|
|
@@ -134,91 +126,69 @@ export class VerticalAxis extends Node2D<VerticalAxisProps> {
|
|
|
134
126
|
const limit = height / 2 - marker.layoutBounds.height / 2
|
|
135
127
|
return Math.min(centred, limit)
|
|
136
128
|
},
|
|
129
|
+
opacity: () => arrivalOf(state.arrivals, i).opacity,
|
|
130
|
+
x: () => arrivalOf(state.arrivals, i).shift,
|
|
137
131
|
})
|
|
138
132
|
return marker
|
|
139
133
|
})
|
|
140
134
|
|
|
141
135
|
const children: Node2D[] = []
|
|
142
|
-
if (
|
|
143
|
-
this.labelNode = new Text({
|
|
144
|
-
...textOps.styleProps(this.axis.titleStyle),
|
|
145
|
-
text: this.label,
|
|
146
|
-
})
|
|
136
|
+
if (state.label !== "") {
|
|
147
137
|
// Node2D space is y-up and rotation is clockwise, so -90° reads bottom-to-top.
|
|
148
138
|
children.push(
|
|
149
139
|
new RotatedBox({
|
|
150
140
|
angle: -90,
|
|
151
141
|
width: "hug",
|
|
152
142
|
height: "hug",
|
|
153
|
-
children:
|
|
143
|
+
children: new Text({
|
|
144
|
+
...textOps.styleProps(state.axis.titleStyle),
|
|
145
|
+
text: state.label,
|
|
146
|
+
opacity: () => arrivalOf(state.arrivals, "label").opacity,
|
|
147
|
+
x: () => arrivalOf(state.arrivals, "label").shift,
|
|
148
|
+
}),
|
|
154
149
|
})
|
|
155
150
|
)
|
|
156
151
|
}
|
|
157
|
-
children.push(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
200
|
-
const { step, per, caption } = enterBudget(
|
|
201
|
-
args.duration,
|
|
202
|
-
nodes.length,
|
|
203
|
-
this.labelNode !== null,
|
|
204
|
-
stagger
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
this.arm(slide)
|
|
208
|
-
|
|
209
|
-
// Bottom marker leads: markerNodes is top → bottom, so walk it in reverse.
|
|
210
|
-
const markers = nodes.map((node, i) => {
|
|
211
|
-
const order = nodes.length - 1 - i // bottom-most = 0
|
|
212
|
-
return at(order * step, node.to({ data: { opacity: 1, x: 0 }, duration: per, easing }))
|
|
213
|
-
})
|
|
214
|
-
const lastMarkerEnds = (nodes.length - 1) * step + per
|
|
215
|
-
|
|
216
|
-
// Once every marker has landed, the caption slides in from the right.
|
|
217
|
-
return compose([
|
|
218
|
-
...markers,
|
|
219
|
-
...(this.labelNode
|
|
220
|
-
? [at(lastMarkerEnds, this.labelNode.to({ data: { opacity: 1, x: 0 }, duration: caption, easing }))]
|
|
221
|
-
: []),
|
|
222
|
-
])
|
|
223
|
-
}
|
|
224
|
-
}
|
|
152
|
+
children.push(new Rect({ height: "fill", width: "hug", padding: { right: tickPad }, children: markers }))
|
|
153
|
+
row.push(new Rect({ flow: "horizontal", gap: titlePad, children }))
|
|
154
|
+
return asParts(row)
|
|
155
|
+
},
|
|
156
|
+
commands: {
|
|
157
|
+
/**
|
|
158
|
+
* Stagger the markers in bottom to top, then slide the caption in from the
|
|
159
|
+
* right after them. Its first frame arms them all: shifted right by `slide`
|
|
160
|
+
* and transparent.
|
|
161
|
+
*
|
|
162
|
+
* The timeline's duration is the **whole** entrance, not one marker's slide:
|
|
163
|
+
* the stagger and the caption are carved out of it rather than added on top.
|
|
164
|
+
* See {@link enterBudget} for why that distinction matters here.
|
|
165
|
+
*
|
|
166
|
+
* `stagger` is seconds between successive markers (derived when omitted);
|
|
167
|
+
* `slide` is the starting rightward offset in px.
|
|
168
|
+
*/
|
|
169
|
+
enter: {
|
|
170
|
+
args: {
|
|
171
|
+
stagger: Types.custom<number | undefined>(undefined),
|
|
172
|
+
slide: Types.number(SLIDE),
|
|
173
|
+
},
|
|
174
|
+
run: (timeline, state) => {
|
|
175
|
+
const { stagger, slide } = timeline.args
|
|
176
|
+
const count = state.markers.length
|
|
177
|
+
const captioned = state.label !== ""
|
|
178
|
+
const from: Arrival = { opacity: 0, shift: slide }
|
|
179
|
+
const keys = [...state.markers.keys(), ...(captioned ? ["label"] : [])]
|
|
180
|
+
const arm = timeline.animate(() => ({ arrivals: armed(keys, from) }))
|
|
181
|
+
if (count === 0) return arm
|
|
182
|
+
const { step, per, caption } = enterBudget(timeline.duration, count, captioned, stagger)
|
|
183
|
+
const ease = frameEase(timeline)
|
|
184
|
+
// Bottom marker leads: the column is top → bottom, so walk it in reverse.
|
|
185
|
+
const markers = state.markers.map((_, i) => ({ at: (count - 1 - i) * step, duration: per, piece: arrival(timeline, i, from, ease) }))
|
|
186
|
+
// Once every marker has landed, the caption slides in from the right.
|
|
187
|
+
const label = captioned ? [{ at: (count - 1) * step + per, duration: caption, piece: arrival(timeline, "label", from, ease) }] : []
|
|
188
|
+
return timeline.together([arm, beats(timeline, [...markers, ...label])])
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
export type VerticalAxis = NodeOf<typeof VerticalAxis>
|