@fjader/svelte 0.2.3 → 0.2.5
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/README.md +3 -1
- package/dist/animation/EntranceGate.d.ts +52 -0
- package/dist/animation/EntranceGate.js +93 -0
- package/dist/animation/TransitionClock.d.ts +61 -30
- package/dist/animation/TransitionClock.js +67 -29
- package/dist/animation/transition-log.d.ts +41 -0
- package/dist/animation/transition-log.js +57 -0
- package/dist/components/axes/AxisBottom.svelte +27 -8
- package/dist/components/axes/AxisBottom.svelte.d.ts +10 -0
- package/dist/components/axes/AxisCenter.svelte +4 -2
- package/dist/components/axes/AxisLeft.svelte +81 -10
- package/dist/components/axes/AxisLeft.svelte.d.ts +17 -0
- package/dist/components/axes/BandAxisTicks.svelte +4 -3
- package/dist/components/axes/axis-title.d.ts +30 -0
- package/dist/components/axes/axis-title.js +77 -0
- package/dist/components/charts/ChartCell.svelte +33 -6
- package/dist/components/charts/ChartCell.svelte.d.ts +9 -0
- package/dist/components/charts/ChartContainer.svelte +55 -5
- package/dist/components/charts/ChartContainer.svelte.d.ts +14 -1
- package/dist/components/charts/ChartHover.svelte.d.ts +14 -0
- package/dist/components/charts/ChartHover.svelte.js +22 -0
- package/dist/components/charts/ChartVisibility.svelte.d.ts +43 -0
- package/dist/components/charts/ChartVisibility.svelte.js +67 -0
- package/dist/components/guides/Grid.svelte +12 -1
- package/dist/components/guides/legend/ContinuousColorLegend.svelte +7 -1
- package/dist/components/guides/legend/OrdinalColorLegend.svelte +8 -5
- package/dist/components/marks/AreaMarkX.svelte +18 -4
- package/dist/components/marks/AreaMarkY.svelte +18 -4
- package/dist/components/marks/BarMarkX.svelte +20 -4
- package/dist/components/marks/BarMarkY.svelte +20 -4
- package/dist/components/marks/DotsMark.svelte +8 -3
- package/dist/components/marks/LineMark.svelte +13 -6
- package/dist/components/marks/ParallelCoordinatesMark.svelte +11 -2
- package/dist/components/marks/ParallelCoordinatesMark.svelte.d.ts +4 -0
- package/dist/components/marks/RectMark.svelte +8 -3
- package/dist/components/marks/RectXMark.svelte +9 -3
- package/dist/components/marks/RectYMark.svelte +9 -3
- package/dist/components/marks/RuleMarkX.svelte +31 -25
- package/dist/components/marks/RuleMarkY.svelte +31 -25
- package/dist/components/marks/TextMark.svelte +52 -14
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.d.ts +22 -1
- package/dist/components/marks/lifecycle/MarkLifecycleV2.svelte.js +579 -66
- package/dist/components/marks/lifecycle/domain-queries.d.ts +13 -2
- package/dist/components/marks/lifecycle/domain-queries.js +43 -11
- package/dist/components/marks/lifecycle/domain-registration.d.ts +6 -1
- package/dist/components/marks/lifecycle/domain-registration.js +17 -20
- package/dist/components/marks/lifecycle/other-rows.svelte.d.ts +112 -0
- package/dist/components/marks/lifecycle/other-rows.svelte.js +193 -0
- package/dist/components/marks/lifecycle/other-split.d.ts +80 -0
- package/dist/components/marks/lifecycle/other-split.js +112 -0
- package/dist/components/marks/lifecycle/query-builder.d.ts +53 -3
- package/dist/components/marks/lifecycle/query-builder.js +46 -23
- package/dist/components/marks/utils/MarkColor.svelte.d.ts +3 -1
- package/dist/components/marks/utils/MarkColor.svelte.js +33 -3
- package/dist/components/marks/utils/MarkSeries.svelte.d.ts +14 -1
- package/dist/components/marks/utils/MarkSeries.svelte.js +25 -1
- package/dist/components/marks/utils/MarkSize.svelte.js +4 -0
- package/dist/components/marks/utils/reactive-config.d.ts +1 -1
- package/dist/components/marks/utils/reactive-config.js +9 -0
- package/dist/components/marks/utils/tooltip-helpers.d.ts +4 -1
- package/dist/components/marks/utils/tooltip-helpers.js +2 -1
- package/dist/components/rendering/Areas.svelte +57 -19
- package/dist/components/rendering/Areas.svelte.d.ts +12 -0
- package/dist/components/rendering/BaseGPURenderer.svelte.d.ts +3 -1
- package/dist/components/rendering/BaseGPURenderer.svelte.js +4 -2
- package/dist/components/rendering/Dots.svelte +23 -3
- package/dist/components/rendering/Dots.svelte.d.ts +6 -0
- package/dist/components/rendering/GridLine.svelte +13 -1
- package/dist/components/rendering/GridLine.svelte.d.ts +5 -0
- package/dist/components/rendering/Lines.svelte +48 -13
- package/dist/components/rendering/Lines.svelte.d.ts +12 -0
- package/dist/components/rendering/ParallelLines.svelte +5 -5
- package/dist/components/rendering/Rects.svelte +164 -30
- package/dist/components/rendering/Rects.svelte.d.ts +23 -2
- package/dist/components/rendering/RuleX.svelte +1 -1
- package/dist/components/rendering/RuleY.svelte +1 -1
- package/dist/components/rendering/WebGPUCanvas.svelte +4 -2
- package/dist/components/rendering/activity-reads.d.ts +47 -12
- package/dist/components/rendering/activity-reads.js +61 -19
- package/dist/components/rendering/uniform-sizes.d.ts +1 -1
- package/dist/components/rendering/uniform-sizes.js +1 -1
- package/dist/config/ChartConfig.svelte.d.ts +10 -1
- package/dist/config/ChartConfig.svelte.js +17 -0
- package/dist/gpu/GPUCoordinator.svelte.d.ts +31 -1
- package/dist/gpu/GPUCoordinator.svelte.js +164 -7
- package/dist/gpu/MorphedChannels.d.ts +11 -28
- package/dist/gpu/MorphedChannels.js +72 -8
- package/dist/gpu/ScaleTemplateSystem.d.ts +20 -2
- package/dist/gpu/ScaleTemplateSystem.js +83 -6
- package/dist/index.d.ts +19 -4
- package/dist/index.js +22 -4
- package/dist/interactors/Interval1D.svelte +6 -1
- package/dist/interactors/Interval2D.svelte +6 -1
- package/dist/interactors/ParallelAxisSource.svelte +10 -1
- package/dist/interactors/Toggle.svelte +32 -3
- package/dist/interactors/Tooltip.svelte +138 -41
- package/dist/interactors/TooltipManager.svelte.d.ts +20 -5
- package/dist/interactors/TooltipManager.svelte.js +46 -12
- package/dist/interactors/other-tooltip.d.ts +16 -0
- package/dist/interactors/other-tooltip.js +22 -0
- package/dist/query-engine/CrossfilterSelection.svelte.d.ts +4 -2
- package/dist/query-engine/CrossfilterSelection.svelte.js +2 -0
- package/dist/query-engine/FieldActivity.svelte.d.ts +49 -1
- package/dist/query-engine/FieldActivity.svelte.js +175 -5
- package/dist/query-engine/FilterGroup.svelte.d.ts +23 -1
- package/dist/query-engine/FilterGroup.svelte.js +43 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.d.ts +31 -3
- package/dist/query-engine/FilterGroupRegistry.svelte.js +62 -15
- package/dist/query-engine/MarkHandle.svelte.d.ts +28 -1
- package/dist/query-engine/MarkHandle.svelte.js +33 -1
- package/dist/query-engine/QueryEngine.d.ts +225 -5
- package/dist/query-engine/QueryEngine.js +697 -47
- package/dist/query-engine/categorical-identity.d.ts +41 -0
- package/dist/query-engine/categorical-identity.js +145 -65
- package/dist/query-engine/debug/query-log.d.ts +39 -0
- package/dist/query-engine/debug/query-log.js +90 -0
- package/dist/query-engine/duckdb.d.ts +55 -8
- package/dist/query-engine/duckdb.js +82 -30
- package/dist/query-engine/index.d.ts +7 -4
- package/dist/query-engine/index.js +3 -3
- package/dist/query-engine/preagg/BuildQueue.d.ts +33 -13
- package/dist/query-engine/preagg/BuildQueue.js +51 -15
- package/dist/query-engine/preagg/Preagg.d.ts +46 -3
- package/dist/query-engine/preagg/Preagg.js +159 -31
- package/dist/query-engine/preagg/plan.d.ts +33 -1
- package/dist/query-engine/preagg/plan.js +158 -14
- package/dist/query-engine/sliced-lane.d.ts +108 -0
- package/dist/query-engine/sliced-lane.js +299 -0
- package/dist/query-engine/source-routing.d.ts +106 -0
- package/dist/query-engine/source-routing.js +189 -0
- package/dist/query-engine/toggle-predicate.d.ts +21 -0
- package/dist/query-engine/toggle-predicate.js +29 -1
- package/dist/scales/ActivityTable.d.ts +5 -1
- package/dist/scales/ActivityTable.js +12 -3
- package/dist/scales/BandScaleState.svelte.d.ts +22 -9
- package/dist/scales/BandScaleState.svelte.js +39 -21
- package/dist/scales/ChartScales.svelte.d.ts +3 -0
- package/dist/scales/ChartScales.svelte.js +26 -8
- package/dist/scales/ColorScaleState.svelte.d.ts +18 -2
- package/dist/scales/ColorScaleState.svelte.js +138 -26
- package/dist/scales/LinearScaleState.svelte.d.ts +7 -10
- package/dist/scales/LinearScaleState.svelte.js +21 -39
- package/dist/scales/ScaleGeneration.svelte.d.ts +48 -0
- package/dist/scales/ScaleGeneration.svelte.js +42 -0
- package/dist/scales/ScaleRegistry.svelte.js +3 -1
- package/dist/scales/SlotTransition.d.ts +15 -1
- package/dist/scales/SlotTransition.js +33 -4
- package/dist/scales/TimeScaleState.svelte.d.ts +7 -3
- package/dist/scales/TimeScaleState.svelte.js +7 -7
- package/dist/scales/color-utils.d.ts +0 -3
- package/dist/scales/color-utils.js +0 -3
- package/dist/scales/entered.d.ts +8 -0
- package/dist/scales/entered.js +10 -0
- package/dist/spec/LazyQueryEngine.js +2 -19
- package/dist/spec/MarkRegistry.d.ts +3 -1
- package/dist/spec/MarkRegistry.js +13 -1
- package/dist/spec/PlotFromSpec.svelte +1 -0
- package/dist/spec/export.js +11 -2
- package/dist/spec/types.d.ts +43 -13
- package/dist/sql/bin.d.ts +25 -0
- package/dist/sql/bin.js +108 -5
- package/dist/theme.css +5 -4
- package/dist/transforms/TransformCompiler.d.ts +47 -2
- package/dist/transforms/TransformCompiler.js +70 -1
- package/dist/transforms/index.js +176 -42
- package/dist/types.d.ts +177 -2
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -16,12 +16,14 @@ pip install fjader # Python: marimo and Jupyter
|
|
|
16
16
|
pip install "fjader[streamlit]" # Python: Streamlit
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
+
`@uwdata/mosaic-sql` is a peer dependency (0.34 or 0.35), so your app and fjader share one copy. Two copies would not recognise each other's expressions: an aggregate such as `sum(...)` written with one would be misread by the other.
|
|
20
|
+
|
|
19
21
|
fjader needs Chrome 149 or later, or another Chromium browser on that version. It relies on two recent features other browsers don't ship yet: WebGPU's `immediate_address_space`, through which every chart passes its scales to the shaders (so every zoom and pan uses it), and the `Temporal` API, used for calendar offsets. There is no fallback renderer.
|
|
20
22
|
|
|
21
23
|
Three things to know before using it in a deployed app:
|
|
22
24
|
|
|
23
25
|
- **Data goes to the browser in full.** The Python widget and the Streamlit component send every column of a DataFrame to the browser, where DuckDB-WASM queries it. Anyone viewing the app can extract it, so pass only the columns you mean to show. In Jupyter, saved widget state can also store the data in the notebook file.
|
|
24
|
-
- **DuckDB-WASM loads from jsDelivr at runtime.**
|
|
26
|
+
- **DuckDB-WASM loads from jsDelivr at runtime by default.** A Svelte app can serve the files itself with `initFjader({ bundles })`, or pass a DuckDB instance it already has with `initFjader(connectionFrom(conn))`. `Fjader.render()`, the Python widget and Streamlit always load from the CDN, so locked-down networks that block it cannot render charts with them.
|
|
25
27
|
- **Specs are trusted input.** A spec's `{ "sql": "…" }` field runs as raw SQL in the viewer's DuckDB-WASM. Don't build specs from untrusted input.
|
|
26
28
|
|
|
27
29
|
## Svelte
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds a chart's entrance until its marks have their first data.
|
|
3
|
+
*
|
|
4
|
+
* Without it the entrance started on the FIRST mark to land, which had two
|
|
5
|
+
* visible costs. Later marks widened the domain while the chart was entering,
|
|
6
|
+
* so its axes animated to the wider extent: a small version of the camera
|
|
7
|
+
* move the entrance replaced. And a mark landing mid-entrance joined it part
|
|
8
|
+
* way. Held, every mark's extent is in before anything shows, the scales snap
|
|
9
|
+
* to the union, and the chart enters once, whole.
|
|
10
|
+
*
|
|
11
|
+
* A data mark HOLDS when it mounts and RELEASES when its first result has
|
|
12
|
+
* registered its domains, when its query fails, or when it unmounts. A mark
|
|
13
|
+
* the author knows to be slow can opt out (`holdsEntrance={false}`): it then
|
|
14
|
+
* joins the entrance where it is, or appears after it.
|
|
15
|
+
*
|
|
16
|
+
* Nothing needs hiding while a chart waits: before its entrance starts the
|
|
17
|
+
* chart's entrance value is 0, so marks that have landed draw at zero height
|
|
18
|
+
* or zero opacity.
|
|
19
|
+
*
|
|
20
|
+
* OPEN is one-way. The gate opens when its last hold is released, when the
|
|
21
|
+
* coordinator starts the entrance (a chart that never had a hold), or on a
|
|
22
|
+
* timeout. The timeout is armed by the FIRST release rather than the first
|
|
23
|
+
* hold: the wait for DuckDB to start, which can take seconds on a cold page,
|
|
24
|
+
* is not a slow mark, and a chart that has not answered at all has nothing to
|
|
25
|
+
* show yet anyway. A gate stuck closed would leave a chart invisible, which is
|
|
26
|
+
* far worse than a late mark appearing after the rest.
|
|
27
|
+
*/
|
|
28
|
+
/** How long the rest of a chart waits for its slowest mark, from its fastest. */
|
|
29
|
+
export declare const ENTRANCE_HOLD_TIMEOUT_MS = 1000;
|
|
30
|
+
export declare class EntranceGate {
|
|
31
|
+
#private;
|
|
32
|
+
/** True once the gate has opened. One-way. */
|
|
33
|
+
get opened(): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the chart may enter: open, or nothing is holding it. The second
|
|
36
|
+
* covers a chart with no data marks, which the coordinator then opens.
|
|
37
|
+
*/
|
|
38
|
+
get canEnter(): boolean;
|
|
39
|
+
/** A mark waits on its first data. Ignored once the gate has opened. */
|
|
40
|
+
hold(id: string): void;
|
|
41
|
+
/** A mark has its first data, failed, or left. Unknown ids are ignored. */
|
|
42
|
+
release(id: string): void;
|
|
43
|
+
/** Open now, and tell whoever is waiting. Idempotent. */
|
|
44
|
+
open(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Run `fn` when the gate opens: at once if it already has. Returns an
|
|
47
|
+
* unsubscribe, for a listener torn down first.
|
|
48
|
+
*/
|
|
49
|
+
onOpen(fn: () => void): () => void;
|
|
50
|
+
/** Drop the timer and listeners, for a chart being torn down. */
|
|
51
|
+
destroy(): void;
|
|
52
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Holds a chart's entrance until its marks have their first data.
|
|
3
|
+
*
|
|
4
|
+
* Without it the entrance started on the FIRST mark to land, which had two
|
|
5
|
+
* visible costs. Later marks widened the domain while the chart was entering,
|
|
6
|
+
* so its axes animated to the wider extent: a small version of the camera
|
|
7
|
+
* move the entrance replaced. And a mark landing mid-entrance joined it part
|
|
8
|
+
* way. Held, every mark's extent is in before anything shows, the scales snap
|
|
9
|
+
* to the union, and the chart enters once, whole.
|
|
10
|
+
*
|
|
11
|
+
* A data mark HOLDS when it mounts and RELEASES when its first result has
|
|
12
|
+
* registered its domains, when its query fails, or when it unmounts. A mark
|
|
13
|
+
* the author knows to be slow can opt out (`holdsEntrance={false}`): it then
|
|
14
|
+
* joins the entrance where it is, or appears after it.
|
|
15
|
+
*
|
|
16
|
+
* Nothing needs hiding while a chart waits: before its entrance starts the
|
|
17
|
+
* chart's entrance value is 0, so marks that have landed draw at zero height
|
|
18
|
+
* or zero opacity.
|
|
19
|
+
*
|
|
20
|
+
* OPEN is one-way. The gate opens when its last hold is released, when the
|
|
21
|
+
* coordinator starts the entrance (a chart that never had a hold), or on a
|
|
22
|
+
* timeout. The timeout is armed by the FIRST release rather than the first
|
|
23
|
+
* hold: the wait for DuckDB to start, which can take seconds on a cold page,
|
|
24
|
+
* is not a slow mark, and a chart that has not answered at all has nothing to
|
|
25
|
+
* show yet anyway. A gate stuck closed would leave a chart invisible, which is
|
|
26
|
+
* far worse than a late mark appearing after the rest.
|
|
27
|
+
*/
|
|
28
|
+
/** How long the rest of a chart waits for its slowest mark, from its fastest. */
|
|
29
|
+
export const ENTRANCE_HOLD_TIMEOUT_MS = 1000;
|
|
30
|
+
export class EntranceGate {
|
|
31
|
+
#holds = new Set();
|
|
32
|
+
#opened = false;
|
|
33
|
+
#timer = null;
|
|
34
|
+
#listeners = new Set();
|
|
35
|
+
/** True once the gate has opened. One-way. */
|
|
36
|
+
get opened() {
|
|
37
|
+
return this.#opened;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether the chart may enter: open, or nothing is holding it. The second
|
|
41
|
+
* covers a chart with no data marks, which the coordinator then opens.
|
|
42
|
+
*/
|
|
43
|
+
get canEnter() {
|
|
44
|
+
return this.#opened || this.#holds.size === 0;
|
|
45
|
+
}
|
|
46
|
+
/** A mark waits on its first data. Ignored once the gate has opened. */
|
|
47
|
+
hold(id) {
|
|
48
|
+
if (this.#opened)
|
|
49
|
+
return;
|
|
50
|
+
this.#holds.add(id);
|
|
51
|
+
}
|
|
52
|
+
/** A mark has its first data, failed, or left. Unknown ids are ignored. */
|
|
53
|
+
release(id) {
|
|
54
|
+
if (!this.#holds.delete(id))
|
|
55
|
+
return;
|
|
56
|
+
if (this.#holds.size === 0)
|
|
57
|
+
this.open();
|
|
58
|
+
else
|
|
59
|
+
this.#timer ??= setTimeout(() => this.open(), ENTRANCE_HOLD_TIMEOUT_MS);
|
|
60
|
+
}
|
|
61
|
+
/** Open now, and tell whoever is waiting. Idempotent. */
|
|
62
|
+
open() {
|
|
63
|
+
if (this.#opened)
|
|
64
|
+
return;
|
|
65
|
+
this.#opened = true;
|
|
66
|
+
this.#holds.clear();
|
|
67
|
+
if (this.#timer !== null)
|
|
68
|
+
clearTimeout(this.#timer);
|
|
69
|
+
this.#timer = null;
|
|
70
|
+
for (const fn of this.#listeners)
|
|
71
|
+
fn();
|
|
72
|
+
this.#listeners.clear();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Run `fn` when the gate opens: at once if it already has. Returns an
|
|
76
|
+
* unsubscribe, for a listener torn down first.
|
|
77
|
+
*/
|
|
78
|
+
onOpen(fn) {
|
|
79
|
+
if (this.#opened) {
|
|
80
|
+
fn();
|
|
81
|
+
return () => { };
|
|
82
|
+
}
|
|
83
|
+
this.#listeners.add(fn);
|
|
84
|
+
return () => this.#listeners.delete(fn);
|
|
85
|
+
}
|
|
86
|
+
/** Drop the timer and listeners, for a chart being torn down. */
|
|
87
|
+
destroy() {
|
|
88
|
+
if (this.#timer !== null)
|
|
89
|
+
clearTimeout(this.#timer);
|
|
90
|
+
this.#timer = null;
|
|
91
|
+
this.#listeners.clear();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,3 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stiffness for a data-update transition, and how to think about changing it.
|
|
3
|
+
*
|
|
4
|
+
* The spring is critically damped, so it never overshoots and the only knob is
|
|
5
|
+
* how fast it converges: `omega = sqrt(stiffness)`, and it is within about a
|
|
6
|
+
* percent of its destination after roughly `5 / omega` seconds. So 120 settles
|
|
7
|
+
* in ~0.46s, 240 in ~0.32s, 400 in ~0.25s. Quadrupling the number halves the
|
|
8
|
+
* time; it is not a linear dial.
|
|
9
|
+
*
|
|
10
|
+
* ONE NUMBER FOR SLOT AND VALUE, deliberately. A bar's position and its height
|
|
11
|
+
* share a clock so they arrive together — separate springs desynchronise on a
|
|
12
|
+
* retarget and the bar reaches its rank visibly before or after it finishes
|
|
13
|
+
* growing. Wanting them to differ is a design change, not a constant.
|
|
14
|
+
*
|
|
15
|
+
* Note this is also the floor on how responsive a drag feels: `t` is
|
|
16
|
+
* normalised, so settle DURATION is independent of distance and a one-pixel
|
|
17
|
+
* correction takes as long as a full reorder.
|
|
18
|
+
*/
|
|
19
|
+
export declare const TRANSITION_STIFFNESS = 240;
|
|
20
|
+
/**
|
|
21
|
+
* Stiffness for an entrance FADE: softer than a transition, since opacity has
|
|
22
|
+
* no distance to travel and a fast fade reads as a pop. From rest it is a
|
|
23
|
+
* gentle S-curve: 30% at 100 ms, 64% at 200 ms, 96% by 450 ms.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ENTRANCE_FADE_STIFFNESS = 120;
|
|
26
|
+
/**
|
|
27
|
+
* Stiffness for an entrance's GROWTH, launched at its own speed (see
|
|
28
|
+
* `restartEaseOut`): 1 − e^(−ωt), 67% at 100 ms, 89% at 200 ms, 96% by 300 ms.
|
|
29
|
+
* Slower than a data transition: growing from nothing is the chart's first
|
|
30
|
+
* statement of magnitude, and at TRANSITION_STIFFNESS it was over before the
|
|
31
|
+
* eye had settled on it.
|
|
32
|
+
*/
|
|
33
|
+
export declare const ENTRANCE_GROW_STIFFNESS = 120;
|
|
34
|
+
/** Sub-pixel, so rest is declared when the difference stops being visible. */
|
|
35
|
+
export declare const TRANSITION_PRECISION = 0.0005;
|
|
36
|
+
/**
|
|
37
|
+
* The animation loop, as a clock sees it. The coordinator's fixed-timestep loop
|
|
38
|
+
* is the one implementation; structurally typed so this module does not import
|
|
39
|
+
* the GPU layer.
|
|
40
|
+
*/
|
|
41
|
+
export interface ClockLoop {
|
|
42
|
+
addAnimatedSource(source: TransitionClock): void;
|
|
43
|
+
removeAnimatedSource(source: TransitionClock): void;
|
|
44
|
+
}
|
|
1
45
|
/**
|
|
2
46
|
* One data-update transition: a spring driving a normalised `t` from 0 to 1.
|
|
3
47
|
*
|
|
@@ -36,36 +80,6 @@
|
|
|
36
80
|
* restarting it every frame makes this consumer re-fold every frame, so it
|
|
37
81
|
* converges by decelerating rather than along the curve the spring intended.
|
|
38
82
|
*/
|
|
39
|
-
/**
|
|
40
|
-
* Stiffness for a data-update transition, and how to think about changing it.
|
|
41
|
-
*
|
|
42
|
-
* The spring is critically damped, so it never overshoots and the only knob is
|
|
43
|
-
* how fast it converges: `omega = sqrt(stiffness)`, and it is within about a
|
|
44
|
-
* percent of its destination after roughly `5 / omega` seconds. So 120 settles
|
|
45
|
-
* in ~0.46s, 240 in ~0.32s, 400 in ~0.25s. Quadrupling the number halves the
|
|
46
|
-
* time; it is not a linear dial.
|
|
47
|
-
*
|
|
48
|
-
* ONE NUMBER FOR SLOT AND VALUE, deliberately. A bar's position and its height
|
|
49
|
-
* share a clock so they arrive together — separate springs desynchronise on a
|
|
50
|
-
* retarget and the bar reaches its rank visibly before or after it finishes
|
|
51
|
-
* growing. Wanting them to differ is a design change, not a constant.
|
|
52
|
-
*
|
|
53
|
-
* Note this is also the floor on how responsive a drag feels: `t` is
|
|
54
|
-
* normalised, so settle DURATION is independent of distance and a one-pixel
|
|
55
|
-
* correction takes as long as a full reorder.
|
|
56
|
-
*/
|
|
57
|
-
export declare const TRANSITION_STIFFNESS = 240;
|
|
58
|
-
/** Sub-pixel, so rest is declared when the difference stops being visible. */
|
|
59
|
-
export declare const TRANSITION_PRECISION = 0.0005;
|
|
60
|
-
/**
|
|
61
|
-
* The animation loop, as a clock sees it. The coordinator's fixed-timestep loop
|
|
62
|
-
* is the one implementation; structurally typed so this module does not import
|
|
63
|
-
* the GPU layer.
|
|
64
|
-
*/
|
|
65
|
-
export interface ClockLoop {
|
|
66
|
-
addAnimatedSource(source: TransitionClock): void;
|
|
67
|
-
removeAnimatedSource(source: TransitionClock): void;
|
|
68
|
-
}
|
|
69
83
|
export declare class TransitionClock {
|
|
70
84
|
#private;
|
|
71
85
|
/**
|
|
@@ -106,6 +120,17 @@ export declare class TransitionClock {
|
|
|
106
120
|
* returns — the same end state the loop would reach, reached now.
|
|
107
121
|
*/
|
|
108
122
|
restart(): void;
|
|
123
|
+
/**
|
|
124
|
+
* Begin from 0 at the spring's own speed, ω, for an ENTRANCE.
|
|
125
|
+
*
|
|
126
|
+
* From rest a critically damped spring starts slowly: its position grows as
|
|
127
|
+
* t² for the first frames, about 18% after 50 ms at the default stiffness.
|
|
128
|
+
* Between two states that is the right shape; for something appearing it
|
|
129
|
+
* reads as lag. Launched at ω the same spring follows 1 − e^(−ωt) exactly:
|
|
130
|
+
* an exponential ease-out, 54% after 50 ms, still never overshooting, and
|
|
131
|
+
* settling in the same time.
|
|
132
|
+
*/
|
|
133
|
+
restartEaseOut(): void;
|
|
109
134
|
/**
|
|
110
135
|
* Leave the loop without settling, for an owner being torn down. Nothing
|
|
111
136
|
* reads the clock afterwards, so there is nothing to land.
|
|
@@ -153,6 +178,12 @@ export declare class TransitionClock {
|
|
|
153
178
|
* then tells the owner — which may restart it, and rejoin, from `onRest`.
|
|
154
179
|
*/
|
|
155
180
|
_markAtRest(): void;
|
|
181
|
+
/**
|
|
182
|
+
* End the transition now, as the loop would at rest: off the loop, `t` on
|
|
183
|
+
* 1, `onRest` called. For an update that lands without animating (a
|
|
184
|
+
* catch-up result), including one that lands mid-flight.
|
|
185
|
+
*/
|
|
186
|
+
finish(): void;
|
|
156
187
|
/**
|
|
157
188
|
* Whether the loop can stop driving this.
|
|
158
189
|
*
|
|
@@ -1,4 +1,47 @@
|
|
|
1
1
|
import { Spring } from './Spring';
|
|
2
|
+
/**
|
|
3
|
+
* Stiffness for a data-update transition, and how to think about changing it.
|
|
4
|
+
*
|
|
5
|
+
* The spring is critically damped, so it never overshoots and the only knob is
|
|
6
|
+
* how fast it converges: `omega = sqrt(stiffness)`, and it is within about a
|
|
7
|
+
* percent of its destination after roughly `5 / omega` seconds. So 120 settles
|
|
8
|
+
* in ~0.46s, 240 in ~0.32s, 400 in ~0.25s. Quadrupling the number halves the
|
|
9
|
+
* time; it is not a linear dial.
|
|
10
|
+
*
|
|
11
|
+
* ONE NUMBER FOR SLOT AND VALUE, deliberately. A bar's position and its height
|
|
12
|
+
* share a clock so they arrive together — separate springs desynchronise on a
|
|
13
|
+
* retarget and the bar reaches its rank visibly before or after it finishes
|
|
14
|
+
* growing. Wanting them to differ is a design change, not a constant.
|
|
15
|
+
*
|
|
16
|
+
* Note this is also the floor on how responsive a drag feels: `t` is
|
|
17
|
+
* normalised, so settle DURATION is independent of distance and a one-pixel
|
|
18
|
+
* correction takes as long as a full reorder.
|
|
19
|
+
*/
|
|
20
|
+
export const TRANSITION_STIFFNESS = 240;
|
|
21
|
+
/**
|
|
22
|
+
* Stiffness for an entrance FADE: softer than a transition, since opacity has
|
|
23
|
+
* no distance to travel and a fast fade reads as a pop. From rest it is a
|
|
24
|
+
* gentle S-curve: 30% at 100 ms, 64% at 200 ms, 96% by 450 ms.
|
|
25
|
+
*/
|
|
26
|
+
export const ENTRANCE_FADE_STIFFNESS = 120;
|
|
27
|
+
/**
|
|
28
|
+
* Stiffness for an entrance's GROWTH, launched at its own speed (see
|
|
29
|
+
* `restartEaseOut`): 1 − e^(−ωt), 67% at 100 ms, 89% at 200 ms, 96% by 300 ms.
|
|
30
|
+
* Slower than a data transition: growing from nothing is the chart's first
|
|
31
|
+
* statement of magnitude, and at TRANSITION_STIFFNESS it was over before the
|
|
32
|
+
* eye had settled on it.
|
|
33
|
+
*/
|
|
34
|
+
export const ENTRANCE_GROW_STIFFNESS = 120;
|
|
35
|
+
/** Sub-pixel, so rest is declared when the difference stops being visible. */
|
|
36
|
+
export const TRANSITION_PRECISION = 0.0005;
|
|
37
|
+
/**
|
|
38
|
+
* The empty redraw list, shared.
|
|
39
|
+
*
|
|
40
|
+
* A clock whose owner supplies no charts is asked for this on every frame the
|
|
41
|
+
* loop drives it, so a fresh array here was an allocation per frame for a
|
|
42
|
+
* reason that never changes. Never mutated: the loop only reads it.
|
|
43
|
+
*/
|
|
44
|
+
const NO_CHARTS = [];
|
|
2
45
|
/**
|
|
3
46
|
* One data-update transition: a spring driving a normalised `t` from 0 to 1.
|
|
4
47
|
*
|
|
@@ -37,35 +80,6 @@ import { Spring } from './Spring';
|
|
|
37
80
|
* restarting it every frame makes this consumer re-fold every frame, so it
|
|
38
81
|
* converges by decelerating rather than along the curve the spring intended.
|
|
39
82
|
*/
|
|
40
|
-
/**
|
|
41
|
-
* Stiffness for a data-update transition, and how to think about changing it.
|
|
42
|
-
*
|
|
43
|
-
* The spring is critically damped, so it never overshoots and the only knob is
|
|
44
|
-
* how fast it converges: `omega = sqrt(stiffness)`, and it is within about a
|
|
45
|
-
* percent of its destination after roughly `5 / omega` seconds. So 120 settles
|
|
46
|
-
* in ~0.46s, 240 in ~0.32s, 400 in ~0.25s. Quadrupling the number halves the
|
|
47
|
-
* time; it is not a linear dial.
|
|
48
|
-
*
|
|
49
|
-
* ONE NUMBER FOR SLOT AND VALUE, deliberately. A bar's position and its height
|
|
50
|
-
* share a clock so they arrive together — separate springs desynchronise on a
|
|
51
|
-
* retarget and the bar reaches its rank visibly before or after it finishes
|
|
52
|
-
* growing. Wanting them to differ is a design change, not a constant.
|
|
53
|
-
*
|
|
54
|
-
* Note this is also the floor on how responsive a drag feels: `t` is
|
|
55
|
-
* normalised, so settle DURATION is independent of distance and a one-pixel
|
|
56
|
-
* correction takes as long as a full reorder.
|
|
57
|
-
*/
|
|
58
|
-
export const TRANSITION_STIFFNESS = 240;
|
|
59
|
-
/** Sub-pixel, so rest is declared when the difference stops being visible. */
|
|
60
|
-
export const TRANSITION_PRECISION = 0.0005;
|
|
61
|
-
/**
|
|
62
|
-
* The empty redraw list, shared.
|
|
63
|
-
*
|
|
64
|
-
* A clock whose owner supplies no charts is asked for this on every frame the
|
|
65
|
-
* loop drives it, so a fresh array here was an allocation per frame for a
|
|
66
|
-
* reason that never changes. Never mutated: the loop only reads it.
|
|
67
|
-
*/
|
|
68
|
-
const NO_CHARTS = [];
|
|
69
83
|
export class TransitionClock {
|
|
70
84
|
#spring;
|
|
71
85
|
#precision;
|
|
@@ -149,6 +163,20 @@ export class TransitionClock {
|
|
|
149
163
|
this.#loop.addAnimatedSource(this);
|
|
150
164
|
}
|
|
151
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Begin from 0 at the spring's own speed, ω, for an ENTRANCE.
|
|
168
|
+
*
|
|
169
|
+
* From rest a critically damped spring starts slowly: its position grows as
|
|
170
|
+
* t² for the first frames, about 18% after 50 ms at the default stiffness.
|
|
171
|
+
* Between two states that is the right shape; for something appearing it
|
|
172
|
+
* reads as lag. Launched at ω the same spring follows 1 − e^(−ωt) exactly:
|
|
173
|
+
* an exponential ease-out, 54% after 50 ms, still never overshooting, and
|
|
174
|
+
* settling in the same time.
|
|
175
|
+
*/
|
|
176
|
+
restartEaseOut() {
|
|
177
|
+
this.restart();
|
|
178
|
+
this.#spring.velocity = this.#spring.omega;
|
|
179
|
+
}
|
|
152
180
|
/**
|
|
153
181
|
* Leave the loop without settling, for an owner being torn down. Nothing
|
|
154
182
|
* reads the clock afterwards, so there is nothing to land.
|
|
@@ -218,6 +246,16 @@ export class TransitionClock {
|
|
|
218
246
|
this.settle();
|
|
219
247
|
this.#onRest?.();
|
|
220
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* End the transition now, as the loop would at rest: off the loop, `t` on
|
|
251
|
+
* 1, `onRest` called. For an update that lands without animating (a
|
|
252
|
+
* catch-up result), including one that lands mid-flight.
|
|
253
|
+
*/
|
|
254
|
+
finish() {
|
|
255
|
+
if (this.#running)
|
|
256
|
+
this.#loop?.removeAnimatedSource(this);
|
|
257
|
+
this._markAtRest();
|
|
258
|
+
}
|
|
221
259
|
/**
|
|
222
260
|
* Whether the loop can stop driving this.
|
|
223
261
|
*
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A debug record of every transition that starts, and whether it animates.
|
|
3
|
+
*
|
|
4
|
+
* For test pages that check an animation claim, such as "a chart catching up
|
|
5
|
+
* on its return animates nothing of its own", without reading it off the
|
|
6
|
+
* screen: a spring that plays off screen and two springs to one place both
|
|
7
|
+
* look like nothing happened. Reported where a transition actually starts:
|
|
8
|
+
*
|
|
9
|
+
* - `domain`: a continuous scale's domain (data extents and fitDomain alike)
|
|
10
|
+
* - `morph`: a mark's morphed values (bars, dots on a band axis)
|
|
11
|
+
* - `slots`: a band axis's categories moving to new slots
|
|
12
|
+
*
|
|
13
|
+
* The cause is not recorded: the emitters do not know it, and the page does
|
|
14
|
+
* (it knows what it just did).
|
|
15
|
+
*
|
|
16
|
+
* MODULE-GLOBAL: one log per page (per module instance), not per coordinator.
|
|
17
|
+
* Two coordinators in one document (notebook cells, Streamlit iframes sharing
|
|
18
|
+
* it) see each other's records; filter by chart id. Fine for a debug hook.
|
|
19
|
+
*
|
|
20
|
+
* Free while nothing observes: each emitter asks `transitionsObserved()`
|
|
21
|
+
* before building a record.
|
|
22
|
+
*/
|
|
23
|
+
export type TransitionPart = 'domain' | 'morph' | 'slots';
|
|
24
|
+
export interface TransitionRecord {
|
|
25
|
+
part: TransitionPart;
|
|
26
|
+
/** True when it landed at once, false when it animates. */
|
|
27
|
+
snap: boolean;
|
|
28
|
+
/** The charts it moves something in. */
|
|
29
|
+
charts: readonly string[];
|
|
30
|
+
/** What moved: a scale name, a renderer label. */
|
|
31
|
+
label: string;
|
|
32
|
+
/** performance.now() */
|
|
33
|
+
time: number;
|
|
34
|
+
}
|
|
35
|
+
/** Listen to every transition started; returns the unsubscribe. */
|
|
36
|
+
export declare function observeTransitions(listener: (record: TransitionRecord) => void): () => void;
|
|
37
|
+
/** Whether anything listens: emitters check this before building a record. */
|
|
38
|
+
export declare function transitionsObserved(): boolean;
|
|
39
|
+
export declare function noteTransition(part: TransitionPart, snap: boolean,
|
|
40
|
+
/** Read untracked, now: the emitter is usually inside an effect. */
|
|
41
|
+
charts: () => readonly string[], label: string): void;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A debug record of every transition that starts, and whether it animates.
|
|
3
|
+
*
|
|
4
|
+
* For test pages that check an animation claim, such as "a chart catching up
|
|
5
|
+
* on its return animates nothing of its own", without reading it off the
|
|
6
|
+
* screen: a spring that plays off screen and two springs to one place both
|
|
7
|
+
* look like nothing happened. Reported where a transition actually starts:
|
|
8
|
+
*
|
|
9
|
+
* - `domain`: a continuous scale's domain (data extents and fitDomain alike)
|
|
10
|
+
* - `morph`: a mark's morphed values (bars, dots on a band axis)
|
|
11
|
+
* - `slots`: a band axis's categories moving to new slots
|
|
12
|
+
*
|
|
13
|
+
* The cause is not recorded: the emitters do not know it, and the page does
|
|
14
|
+
* (it knows what it just did).
|
|
15
|
+
*
|
|
16
|
+
* MODULE-GLOBAL: one log per page (per module instance), not per coordinator.
|
|
17
|
+
* Two coordinators in one document (notebook cells, Streamlit iframes sharing
|
|
18
|
+
* it) see each other's records; filter by chart id. Fine for a debug hook.
|
|
19
|
+
*
|
|
20
|
+
* Free while nothing observes: each emitter asks `transitionsObserved()`
|
|
21
|
+
* before building a record.
|
|
22
|
+
*/
|
|
23
|
+
import { untrack } from 'svelte';
|
|
24
|
+
const observers = new Set();
|
|
25
|
+
/** Noted and not yet delivered: see noteTransition. */
|
|
26
|
+
let queued = [];
|
|
27
|
+
/** Listen to every transition started; returns the unsubscribe. */
|
|
28
|
+
export function observeTransitions(listener) {
|
|
29
|
+
observers.add(listener);
|
|
30
|
+
return () => observers.delete(listener);
|
|
31
|
+
}
|
|
32
|
+
/** Whether anything listens: emitters check this before building a record. */
|
|
33
|
+
export function transitionsObserved() {
|
|
34
|
+
return observers.size > 0;
|
|
35
|
+
}
|
|
36
|
+
export function noteTransition(part, snap,
|
|
37
|
+
/** Read untracked, now: the emitter is usually inside an effect. */
|
|
38
|
+
charts, label) {
|
|
39
|
+
if (observers.size === 0)
|
|
40
|
+
return;
|
|
41
|
+
const list = untrack(() => [...charts()]);
|
|
42
|
+
queued.push({ part, snap, charts: list, label, time: performance.now() });
|
|
43
|
+
// Delivered in a microtask, never in the emitter's own call: transitions
|
|
44
|
+
// start inside effects (a renderer's write, a scale update), and a
|
|
45
|
+
// listener run there that reads state makes that effect depend on it. A
|
|
46
|
+
// page appending to a $state log then re-ran the emitting effect on every
|
|
47
|
+
// record, which logged again, and never stopped.
|
|
48
|
+
if (queued.length === 1)
|
|
49
|
+
queueMicrotask(deliver);
|
|
50
|
+
}
|
|
51
|
+
function deliver() {
|
|
52
|
+
const records = queued;
|
|
53
|
+
queued = [];
|
|
54
|
+
for (const record of records)
|
|
55
|
+
for (const observe of observers)
|
|
56
|
+
observe(record);
|
|
57
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { hasEntered } from '../../scales/entered';
|
|
2
3
|
import { getContext } from 'svelte';
|
|
3
4
|
import type { ChartContext } from '../../types';
|
|
4
5
|
import type { BandScaleState } from '../../scales/BandScaleState.svelte';
|
|
@@ -8,9 +9,24 @@
|
|
|
8
9
|
interface Props {
|
|
9
10
|
ticksNumber?: number;
|
|
10
11
|
tickFormat?: ((value: unknown) => string) | null;
|
|
12
|
+
/**
|
|
13
|
+
* Draw the scale's label as the axis title (default). `false` draws none; the
|
|
14
|
+
* label still names the scale, so charts sharing it still link their zoom.
|
|
15
|
+
*/
|
|
16
|
+
title?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Where along the axis the title sits: `'center'` (default), or at the plot's
|
|
19
|
+
* `'start'` (left) or `'end'` (right) edge.
|
|
20
|
+
*/
|
|
21
|
+
titleAnchor?: 'start' | 'center' | 'end';
|
|
11
22
|
}
|
|
12
23
|
|
|
13
|
-
let {
|
|
24
|
+
let {
|
|
25
|
+
ticksNumber = 5,
|
|
26
|
+
tickFormat = null,
|
|
27
|
+
title = true,
|
|
28
|
+
titleAnchor = 'center'
|
|
29
|
+
}: Props = $props();
|
|
14
30
|
const c = getContext<ChartContext>('chart');
|
|
15
31
|
|
|
16
32
|
// Propagate custom format to scale state once it exists (for MarginLabel, tooltips, etc.)
|
|
@@ -20,9 +36,10 @@
|
|
|
20
36
|
(s as ContinuousScaleState).setCustomFormat(tickFormat);
|
|
21
37
|
});
|
|
22
38
|
|
|
23
|
-
// Continuous scales (linear, time): use scaleState.getTicks()
|
|
39
|
+
// Continuous scales (linear, time): use scaleState.getTicks(). None until the
|
|
40
|
+
// scale has entered: before that its domain is a placeholder.
|
|
24
41
|
const continuousTicks = $derived(
|
|
25
|
-
c.xScaleState?.getTicks ? c.xScaleState.getTicks(ticksNumber) : []
|
|
42
|
+
c.xScaleState?.getTicks && hasEntered(c.xScaleState) ? c.xScaleState.getTicks(ticksNumber) : []
|
|
26
43
|
);
|
|
27
44
|
|
|
28
45
|
// Band scales: the categories on screen NOW, at their current fractional slots,
|
|
@@ -47,10 +64,6 @@
|
|
|
47
64
|
{tickFormatter(tick)}
|
|
48
65
|
</text>
|
|
49
66
|
{/each}
|
|
50
|
-
|
|
51
|
-
<text text-anchor="middle" x={c.innerWidth / 2} y={28}>
|
|
52
|
-
{c.xLabel}
|
|
53
|
-
</text>
|
|
54
67
|
{:else if c.scaleTypes.x === 'band' && c.xScale}
|
|
55
68
|
<BandAxisTicks
|
|
56
69
|
scale={c.xScaleState as BandScaleState}
|
|
@@ -60,8 +73,14 @@
|
|
|
60
73
|
tickLength={5}
|
|
61
74
|
labelGap={16}
|
|
62
75
|
/>
|
|
76
|
+
{/if}
|
|
63
77
|
|
|
64
|
-
|
|
78
|
+
{#if title && c.xScale}
|
|
79
|
+
<text
|
|
80
|
+
text-anchor={titleAnchor === 'start' ? 'start' : titleAnchor === 'end' ? 'end' : 'middle'}
|
|
81
|
+
x={titleAnchor === 'start' ? 0 : titleAnchor === 'end' ? c.innerWidth : c.innerWidth / 2}
|
|
82
|
+
y={28}
|
|
83
|
+
>
|
|
65
84
|
{c.xLabel}
|
|
66
85
|
</text>
|
|
67
86
|
{/if}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
interface Props {
|
|
2
2
|
ticksNumber?: number;
|
|
3
3
|
tickFormat?: ((value: unknown) => string) | null;
|
|
4
|
+
/**
|
|
5
|
+
* Draw the scale's label as the axis title (default). `false` draws none; the
|
|
6
|
+
* label still names the scale, so charts sharing it still link their zoom.
|
|
7
|
+
*/
|
|
8
|
+
title?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Where along the axis the title sits: `'center'` (default), or at the plot's
|
|
11
|
+
* `'start'` (left) or `'end'` (right) edge.
|
|
12
|
+
*/
|
|
13
|
+
titleAnchor?: 'start' | 'center' | 'end';
|
|
4
14
|
}
|
|
5
15
|
declare const AxisBottom: import("svelte").Component<Props, {}, "">;
|
|
6
16
|
type AxisBottom = ReturnType<typeof AxisBottom>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { hasEntered } from '../../scales/entered';
|
|
2
3
|
import { getContext } from 'svelte';
|
|
3
4
|
import type { ChartContext } from '../../types';
|
|
4
5
|
import type { BandScaleState } from '../../scales/BandScaleState.svelte';
|
|
@@ -39,9 +40,10 @@
|
|
|
39
40
|
const tickDir = $derived(labelSide === 'right' ? 1 : -1);
|
|
40
41
|
const textAnchor = $derived(labelSide === 'right' ? 'start' : 'end');
|
|
41
42
|
|
|
42
|
-
// Continuous scales (linear, time): use scaleState.getTicks()
|
|
43
|
+
// Continuous scales (linear, time): use scaleState.getTicks(). None until the
|
|
44
|
+
// scale has entered: before that its domain is a placeholder.
|
|
43
45
|
const continuousTicks = $derived(
|
|
44
|
-
c.yScaleState?.getTicks ? c.yScaleState.getTicks(ticksNumber) : []
|
|
46
|
+
c.yScaleState?.getTicks && hasEntered(c.yScaleState) ? c.yScaleState.getTicks(ticksNumber) : []
|
|
45
47
|
);
|
|
46
48
|
|
|
47
49
|
// Band scales: the categories on screen NOW, at their current fractional slots —
|