@rokkit/chart 1.0.0-next.148 → 1.0.0-next.150
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/dist/lib/plot/frames.d.ts +20 -18
- package/dist/lib/plot/scales.d.ts +2 -2
- package/package.json +1 -1
- package/src/AnimatedPlot.svelte +32 -12
- package/src/lib/plot/frames.js +38 -48
|
@@ -8,35 +8,37 @@
|
|
|
8
8
|
*/
|
|
9
9
|
export function extractFrames(data: Object[], timeField: string): Map<unknown, Object[]>;
|
|
10
10
|
/**
|
|
11
|
-
* Ensures all (
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Ensures all frame values (byField) appear for every (x, color?) combination.
|
|
12
|
+
* Uses dataset alignBy to fill missing frame-value combos with y=0 so bars
|
|
13
|
+
* animate smoothly rather than disappearing between frames.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* @param {
|
|
15
|
+
* Call after pre-aggregation. The result can be split directly by extractFrames
|
|
16
|
+
* with no further per-frame normalization needed.
|
|
17
|
+
*
|
|
18
|
+
* @param {Object[]} data - pre-aggregated rows, one per (x, color?, byField)
|
|
19
|
+
* @param {{ x?: string, y: string, color?: string }} channels
|
|
20
|
+
* @param {string} byField - the frame field (e.g. 'year')
|
|
19
21
|
* @returns {Object[]}
|
|
20
22
|
*/
|
|
21
|
-
export function
|
|
22
|
-
x
|
|
23
|
+
export function completeFrames(data: Object[], channels: {
|
|
24
|
+
x?: string;
|
|
23
25
|
y: string;
|
|
24
26
|
color?: string;
|
|
25
|
-
},
|
|
27
|
+
}, byField: string): Object[];
|
|
26
28
|
/**
|
|
27
|
-
* Computes static x/y domains
|
|
28
|
-
* These domains stay constant throughout the animation so
|
|
29
|
-
*
|
|
29
|
+
* Computes static x/y domains from the full (pre-split) data array.
|
|
30
|
+
* These domains stay constant throughout the animation so values are
|
|
31
|
+
* always comparable across frames.
|
|
30
32
|
*
|
|
31
|
-
* NOTE: y domain is pinned to [0, max] — assumes bar chart semantics
|
|
32
|
-
*
|
|
33
|
-
* be negative
|
|
33
|
+
* NOTE: y domain is pinned to [0, max] — assumes bar chart semantics.
|
|
34
|
+
* Pass an explicit yDomain override for scatter/line charts where y can
|
|
35
|
+
* be negative.
|
|
34
36
|
*
|
|
35
|
-
* @param {
|
|
37
|
+
* @param {Object[]} data - full dataset (before frame extraction)
|
|
36
38
|
* @param {{ x: string, y: string }} channels
|
|
37
39
|
* @returns {{ xDomain: unknown[], yDomain: [number, number] }}
|
|
38
40
|
*/
|
|
39
|
-
export function computeStaticDomains(
|
|
41
|
+
export function computeStaticDomains(data: Object[], channels: {
|
|
40
42
|
x: string;
|
|
41
43
|
y: string;
|
|
42
44
|
}): {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export function inferFieldType(data: any, field: any): "
|
|
2
|
-
export function inferOrientation(xType: any, yType: any): "
|
|
1
|
+
export function inferFieldType(data: any, field: any): "continuous" | "band";
|
|
2
|
+
export function inferOrientation(xType: any, yType: any): "none" | "vertical" | "horizontal";
|
|
3
3
|
export function buildUnifiedXScale(datasets: any, field: any, width: any, opts?: {}): any;
|
|
4
4
|
export function buildUnifiedYScale(datasets: any, field: any, height: any, opts?: {}): any;
|
|
5
5
|
export function inferColorScaleType(data: any, field: any, spec?: {}): any;
|
package/package.json
CHANGED
package/src/AnimatedPlot.svelte
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { onMount, onDestroy } from 'svelte'
|
|
3
|
-
import { extractFrames,
|
|
3
|
+
import { extractFrames, completeFrames, computeStaticDomains } from './lib/plot/frames.js'
|
|
4
|
+
import { applyGeomStat } from './lib/plot/stat.js'
|
|
4
5
|
import Timeline from './Plot/Timeline.svelte'
|
|
5
6
|
import PlotChart from './Plot.svelte'
|
|
6
7
|
|
|
@@ -37,16 +38,37 @@
|
|
|
37
38
|
children
|
|
38
39
|
} = $props()
|
|
39
40
|
|
|
40
|
-
//
|
|
41
|
-
|
|
41
|
+
// Pre-aggregate and complete frames when any geom has a non-identity stat:
|
|
42
|
+
// 1. applyGeomStat: aggregate data by (x, color?, by) → one row per combination
|
|
43
|
+
// 2. completeFrames: alignBy(by) ensures all frame values appear for every (x, color?)
|
|
44
|
+
// group, filling missing rows with y=0 so bars animate smoothly
|
|
45
|
+
// Geoms are returned with stat: 'identity' so PlotChart renders the pre-aggregated values as-is.
|
|
46
|
+
const prepared = $derived.by(() => {
|
|
47
|
+
const firstNonIdentity = geoms.find((g) => g.stat && g.stat !== 'identity')
|
|
48
|
+
if (!firstNonIdentity) return { data, geoms }
|
|
49
|
+
|
|
50
|
+
const aggChannels = { y }
|
|
51
|
+
if (x) aggChannels.x = x
|
|
52
|
+
if (color) aggChannels.color = color
|
|
53
|
+
aggChannels.frame = animate.by // 'frame' is not a value channel, so it becomes a group-by
|
|
54
|
+
|
|
55
|
+
const aggregated = applyGeomStat(
|
|
56
|
+
data,
|
|
57
|
+
{ stat: firstNonIdentity.stat, channels: aggChannels },
|
|
58
|
+
helpers
|
|
59
|
+
)
|
|
60
|
+
const completeData = completeFrames(aggregated, { x, y, color }, animate.by)
|
|
61
|
+
|
|
62
|
+
return { data: completeData, geoms: geoms.map((g) => ({ ...g, stat: 'identity' })) }
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Extract frames and compute stable domains from the full prepared dataset
|
|
66
|
+
const rawFrames = $derived(extractFrames(prepared.data, animate.by))
|
|
42
67
|
const frameKeys = $derived([...rawFrames.keys()])
|
|
43
68
|
|
|
44
69
|
const channels = $derived({ x, y, color })
|
|
45
|
-
const allXValues = $derived(x ? [...new Set(data.map((d) => d[x]))] : [])
|
|
46
|
-
const allColorValues = $derived(color ? [...new Set(data.map((d) => d[color]))] : null)
|
|
47
|
-
|
|
48
70
|
const staticDomains = $derived(
|
|
49
|
-
x && y ? computeStaticDomains(
|
|
71
|
+
x && y ? computeStaticDomains(prepared.data, channels) : { xDomain: undefined, yDomain: undefined }
|
|
50
72
|
)
|
|
51
73
|
|
|
52
74
|
// Playback state
|
|
@@ -54,12 +76,10 @@
|
|
|
54
76
|
let playing = $state(false)
|
|
55
77
|
let speed = $state(1)
|
|
56
78
|
|
|
57
|
-
// Current frame data
|
|
79
|
+
// Current frame data — already complete (all x/color combos present)
|
|
58
80
|
const currentFrameData = $derived.by(() => {
|
|
59
81
|
const key = frameKeys[currentIndex]
|
|
60
|
-
|
|
61
|
-
if (!x || !y) return raw
|
|
62
|
-
return normalizeFrame(raw, { x, y, color }, allXValues, allColorValues)
|
|
82
|
+
return rawFrames.get(key) ?? []
|
|
63
83
|
})
|
|
64
84
|
|
|
65
85
|
// Reduced motion preference
|
|
@@ -154,7 +174,7 @@
|
|
|
154
174
|
const frameSpec = $derived({
|
|
155
175
|
data: currentFrameData,
|
|
156
176
|
x, y, color,
|
|
157
|
-
geoms,
|
|
177
|
+
geoms: prepared.geoms,
|
|
158
178
|
xDomain: staticDomains.xDomain,
|
|
159
179
|
yDomain: staticDomains.yDomain
|
|
160
180
|
})
|
package/src/lib/plot/frames.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { extent } from 'd3-array'
|
|
2
|
+
import { dataset } from '@rokkit/data'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Extracts animation frames from data, keyed by time field value.
|
|
@@ -19,72 +20,61 @@ export function extractFrames(data, timeField) {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
|
-
* Ensures all (
|
|
23
|
-
*
|
|
24
|
-
*
|
|
23
|
+
* Ensures all frame values (byField) appear for every (x, color?) combination.
|
|
24
|
+
* Uses dataset alignBy to fill missing frame-value combos with y=0 so bars
|
|
25
|
+
* animate smoothly rather than disappearing between frames.
|
|
25
26
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* @param {
|
|
27
|
+
* Call after pre-aggregation. The result can be split directly by extractFrames
|
|
28
|
+
* with no further per-frame normalization needed.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object[]} data - pre-aggregated rows, one per (x, color?, byField)
|
|
31
|
+
* @param {{ x?: string, y: string, color?: string }} channels
|
|
32
|
+
* @param {string} byField - the frame field (e.g. 'year')
|
|
30
33
|
* @returns {Object[]}
|
|
31
34
|
*/
|
|
32
|
-
export function
|
|
35
|
+
export function completeFrames(data, channels, byField) {
|
|
33
36
|
const { x: xf, y: yf, color: cf } = channels
|
|
37
|
+
const groupFields = [xf, ...(cf ? [cf] : [])].filter(Boolean)
|
|
34
38
|
|
|
35
|
-
if (
|
|
36
|
-
throw new Error('normalizeFrame: allColorValues must be provided when color channel is set')
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Build lookup of existing (x, color?) keys
|
|
40
|
-
const existing = new Set(
|
|
41
|
-
frameData.map((d) => (cf ? `${d[xf]}::${d[cf]}` : String(d[xf])))
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
const filled = [...frameData]
|
|
39
|
+
if (groupFields.length === 0) return data
|
|
45
40
|
|
|
46
|
-
const
|
|
41
|
+
const nested = dataset(data)
|
|
42
|
+
.groupBy(...groupFields)
|
|
43
|
+
.alignBy(byField)
|
|
44
|
+
.usingTemplate({ [yf]: 0 })
|
|
45
|
+
.rollup()
|
|
46
|
+
.select()
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (cf && colorVal !== null) row[cf] = colorVal
|
|
54
|
-
filled.push(row)
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return filled
|
|
48
|
+
return nested.flatMap((row) => {
|
|
49
|
+
const groupKey = groupFields.reduce((acc, f) => ({ ...acc, [f]: row[f] }), {})
|
|
50
|
+
// strip the actual_flag marker added by alignBy
|
|
51
|
+
return row.children.map(({ actual_flag: _af, ...child }) => ({ ...groupKey, ...child }))
|
|
52
|
+
})
|
|
60
53
|
}
|
|
61
54
|
|
|
62
55
|
/**
|
|
63
|
-
* Computes static x/y domains
|
|
64
|
-
* These domains stay constant throughout the animation so
|
|
65
|
-
*
|
|
56
|
+
* Computes static x/y domains from the full (pre-split) data array.
|
|
57
|
+
* These domains stay constant throughout the animation so values are
|
|
58
|
+
* always comparable across frames.
|
|
66
59
|
*
|
|
67
|
-
* NOTE: y domain is pinned to [0, max] — assumes bar chart semantics
|
|
68
|
-
*
|
|
69
|
-
* be negative
|
|
60
|
+
* NOTE: y domain is pinned to [0, max] — assumes bar chart semantics.
|
|
61
|
+
* Pass an explicit yDomain override for scatter/line charts where y can
|
|
62
|
+
* be negative.
|
|
70
63
|
*
|
|
71
|
-
* @param {
|
|
64
|
+
* @param {Object[]} data - full dataset (before frame extraction)
|
|
72
65
|
* @param {{ x: string, y: string }} channels
|
|
73
66
|
* @returns {{ xDomain: unknown[], yDomain: [number, number] }}
|
|
74
67
|
*/
|
|
75
|
-
export function computeStaticDomains(
|
|
68
|
+
export function computeStaticDomains(data, channels) {
|
|
76
69
|
const { x: xf, y: yf } = channels
|
|
77
|
-
const allData = [...frames.values()].flat()
|
|
78
|
-
|
|
79
|
-
const sampleX = allData[0]?.[xf]
|
|
80
|
-
const xIsCategorical = typeof sampleX === 'string'
|
|
81
70
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
71
|
+
const sampleX = data[0]?.[xf]
|
|
72
|
+
const xDomain = typeof sampleX === 'string'
|
|
73
|
+
? [...new Set(data.map((d) => d[xf]))]
|
|
74
|
+
: extent(data, (d) => Number(d[xf]))
|
|
85
75
|
|
|
86
|
-
const [, yMax] = extent(
|
|
87
|
-
const yDomain = [0, yMax ?? 0]
|
|
76
|
+
const [, yMax] = extent(data, (d) => Number(d[yf]))
|
|
77
|
+
const yDomain = [0, yMax ?? 0]
|
|
88
78
|
|
|
89
79
|
return { xDomain, yDomain }
|
|
90
80
|
}
|