@dtour/viewer 0.1.0 → 0.2.0
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/Dtour.d.ts +5 -1
- package/dist/Dtour.d.ts.map +1 -1
- package/dist/DtourViewer.d.ts +4 -1
- package/dist/DtourViewer.d.ts.map +1 -1
- package/dist/components/AxisOverlay.d.ts +11 -1
- package/dist/components/AxisOverlay.d.ts.map +1 -1
- package/dist/components/CircularSlider.d.ts +21 -2
- package/dist/components/CircularSlider.d.ts.map +1 -1
- package/dist/components/DtourToolbar.d.ts +2 -1
- package/dist/components/DtourToolbar.d.ts.map +1 -1
- package/dist/components/Gallery.d.ts +3 -3
- package/dist/components/Gallery.d.ts.map +1 -1
- package/dist/components/RevertCameraButton.d.ts +6 -0
- package/dist/components/RevertCameraButton.d.ts.map +1 -0
- package/dist/components/ui/checkbox.d.ts +6 -0
- package/dist/components/ui/checkbox.d.ts.map +1 -0
- package/dist/hooks/usePlayback.d.ts +7 -5
- package/dist/hooks/usePlayback.d.ts.map +1 -1
- package/dist/hooks/useScatter.d.ts.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/layout/gallery-positions.d.ts +3 -1
- package/dist/layout/gallery-positions.d.ts.map +1 -1
- package/dist/layout/selector-size.d.ts +4 -2
- package/dist/layout/selector-size.d.ts.map +1 -1
- package/dist/lib/arcball.d.ts +21 -0
- package/dist/lib/arcball.d.ts.map +1 -0
- package/dist/lib/position-remap.d.ts +16 -0
- package/dist/lib/position-remap.d.ts.map +1 -0
- package/dist/lib/throttle-debounce.d.ts +28 -0
- package/dist/lib/throttle-debounce.d.ts.map +1 -0
- package/dist/radial-chart/RadialChart.d.ts +5 -1
- package/dist/radial-chart/RadialChart.d.ts.map +1 -1
- package/dist/spec.d.ts +32 -0
- package/dist/spec.d.ts.map +1 -1
- package/dist/state/atoms.d.ts +67 -0
- package/dist/state/atoms.d.ts.map +1 -1
- package/dist/state/spec-sync.d.ts +2 -0
- package/dist/state/spec-sync.d.ts.map +1 -1
- package/dist/viewer.css +1 -1
- package/dist/viewer.js +11620 -10118
- package/package.json +6 -1
- package/src/Dtour.tsx +82 -9
- package/src/DtourViewer.tsx +480 -100
- package/src/components/AxisOverlay.tsx +332 -182
- package/src/components/CircularSlider.tsx +363 -174
- package/src/components/DtourToolbar.tsx +121 -10
- package/src/components/Gallery.tsx +197 -39
- package/src/components/RevertCameraButton.tsx +39 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/hooks/usePlayback.ts +18 -44
- package/src/hooks/useScatter.ts +21 -5
- package/src/index.ts +16 -3
- package/src/layout/gallery-positions.ts +15 -4
- package/src/layout/selector-size.ts +24 -10
- package/src/lib/arcball.ts +119 -0
- package/src/lib/position-remap.ts +51 -0
- package/src/lib/throttle-debounce.ts +79 -0
- package/src/radial-chart/RadialChart.tsx +45 -6
- package/src/spec.ts +143 -0
- package/src/state/atoms.ts +65 -0
- package/src/state/spec-sync.ts +15 -0
- package/src/styles.css +16 -16
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { tourToVisual } from '../lib/position-remap.ts';
|
|
2
3
|
import { arcPath, keyframeAngle, rectBarPath } from './arc-path.ts';
|
|
3
4
|
import type { ParsedTrack } from './types.ts';
|
|
4
5
|
|
|
6
|
+
const START_DEG = -135;
|
|
7
|
+
|
|
5
8
|
export type RadialChartProps = {
|
|
6
9
|
tracks: ParsedTrack[];
|
|
7
10
|
keyframeCount: number;
|
|
@@ -11,6 +14,10 @@ export type RadialChartProps = {
|
|
|
11
14
|
size: number;
|
|
12
15
|
/** Inner radius = selector ring radius (selectorSize * 0.4). */
|
|
13
16
|
innerRadius: number;
|
|
17
|
+
/** Cumulative arc-lengths for geodesic tick positioning. */
|
|
18
|
+
arcLengths?: Float32Array | null;
|
|
19
|
+
/** Slider spacing mode. Default 'equal'. */
|
|
20
|
+
spacingMode?: 'equal' | 'geodesic';
|
|
14
21
|
};
|
|
15
22
|
|
|
16
23
|
const TRACK_GAP = 2;
|
|
@@ -25,6 +32,8 @@ export const RadialChart = ({
|
|
|
25
32
|
position,
|
|
26
33
|
size,
|
|
27
34
|
innerRadius,
|
|
35
|
+
arcLengths,
|
|
36
|
+
spacingMode = 'equal',
|
|
28
37
|
}: RadialChartProps) => {
|
|
29
38
|
const center = size / 2;
|
|
30
39
|
const [hover, setHover] = useState<HoverInfo | null>(null);
|
|
@@ -51,8 +60,37 @@ export const RadialChart = ({
|
|
|
51
60
|
});
|
|
52
61
|
}, [tracks, innerRadius, stacked]);
|
|
53
62
|
|
|
63
|
+
// Compute angle for keyframe index, respecting spacing mode.
|
|
64
|
+
// In geodesic mode, bars sit at arc-length positions; in equal mode, uniform.
|
|
65
|
+
const getAngle = useCallback(
|
|
66
|
+
(index: number): number => {
|
|
67
|
+
if (spacingMode === 'geodesic' && arcLengths && index < arcLengths.length) {
|
|
68
|
+
return ((arcLengths[index]! * 360 + START_DEG) * Math.PI) / 180;
|
|
69
|
+
}
|
|
70
|
+
return keyframeAngle(index, keyframeCount);
|
|
71
|
+
},
|
|
72
|
+
[spacingMode, arcLengths, keyframeCount],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Angular span for a segment from keyframe i to i+1
|
|
76
|
+
const getSegmentSpan = useCallback(
|
|
77
|
+
(index: number): number => {
|
|
78
|
+
if (spacingMode === 'geodesic' && arcLengths && arcLengths.length > 1) {
|
|
79
|
+
const n = arcLengths.length - 1;
|
|
80
|
+
const start = arcLengths[index]!;
|
|
81
|
+
const end = arcLengths[(index + 1) % (n + 1)]!;
|
|
82
|
+
const span = end > start ? end - start : 1 - start + end;
|
|
83
|
+
return span * 2 * Math.PI;
|
|
84
|
+
}
|
|
85
|
+
return (2 * Math.PI) / keyframeCount;
|
|
86
|
+
},
|
|
87
|
+
[spacingMode, arcLengths, keyframeCount],
|
|
88
|
+
);
|
|
89
|
+
|
|
54
90
|
// Flanking keyframes based on current position
|
|
55
|
-
const
|
|
91
|
+
const visualPos =
|
|
92
|
+
spacingMode === 'equal' && arcLengths ? tourToVisual(position, arcLengths) : position;
|
|
93
|
+
const fractionalIndex = visualPos * keyframeCount;
|
|
56
94
|
const leftKf = Math.floor(fractionalIndex) % keyframeCount;
|
|
57
95
|
const rightKf = (leftKf + 1) % keyframeCount;
|
|
58
96
|
|
|
@@ -85,7 +123,7 @@ export const RadialChart = ({
|
|
|
85
123
|
const barWidthPx = (tracks[0]?.barWidth as number) ?? 0;
|
|
86
124
|
|
|
87
125
|
for (let kfIdx = 0; kfIdx < keyframeCount; kfIdx++) {
|
|
88
|
-
const centerAngle =
|
|
126
|
+
const centerAngle = getAngle(kfIdx);
|
|
89
127
|
|
|
90
128
|
let stackBase = baseR;
|
|
91
129
|
for (let trackIdx = 0; trackIdx < tracks.length; trackIdx++) {
|
|
@@ -115,7 +153,7 @@ export const RadialChart = ({
|
|
|
115
153
|
}
|
|
116
154
|
}
|
|
117
155
|
return bars;
|
|
118
|
-
}, [stacked, tracks, keyframeCount, baseR, center]);
|
|
156
|
+
}, [stacked, tracks, keyframeCount, baseR, center, getAngle]);
|
|
119
157
|
|
|
120
158
|
return (
|
|
121
159
|
<div className="relative" style={{ width: size, height: size }}>
|
|
@@ -140,9 +178,10 @@ export const RadialChart = ({
|
|
|
140
178
|
{track.normalizedValues.map((normVal, kfIdx) => {
|
|
141
179
|
if (kfIdx >= keyframeCount) return null;
|
|
142
180
|
|
|
143
|
-
const centerAngle =
|
|
144
|
-
const
|
|
145
|
-
const
|
|
181
|
+
const centerAngle = getAngle(kfIdx);
|
|
182
|
+
const halfSpan = getSegmentSpan(kfIdx) / 2;
|
|
183
|
+
const angleStart = centerAngle - halfSpan + BAR_PAD_RAD;
|
|
184
|
+
const angleEnd = centerAngle + halfSpan - BAR_PAD_RAD;
|
|
146
185
|
|
|
147
186
|
const barOuter = rInner + normVal * track.height;
|
|
148
187
|
const rawValue = track.rawValues[kfIdx] as number;
|
package/src/spec.ts
CHANGED
|
@@ -22,11 +22,149 @@ export const dtourSpecSchema = z.object({
|
|
|
22
22
|
cameraZoom: z.number().positive().optional(),
|
|
23
23
|
viewMode: z.enum(['guided', 'manual', 'grand']).optional(),
|
|
24
24
|
showLegend: z.boolean().optional(),
|
|
25
|
+
showAxes: z.boolean().optional(),
|
|
26
|
+
showFrameNumbers: z.boolean().optional(),
|
|
27
|
+
showFrameLoadings: z.boolean().optional(),
|
|
28
|
+
showTourDescription: z.boolean().optional(),
|
|
29
|
+
sliderSpacing: z.enum(['equal', 'geodesic']).optional(),
|
|
25
30
|
themeMode: z.enum(['light', 'dark', 'system']).optional(),
|
|
26
31
|
});
|
|
27
32
|
|
|
28
33
|
export type DtourSpec = z.infer<typeof dtourSpecSchema>;
|
|
29
34
|
|
|
35
|
+
/** Per-frame top-2 feature correlations: [featureName, pearsonR] pairs. */
|
|
36
|
+
export type FrameLoading = [string, number];
|
|
37
|
+
|
|
38
|
+
/** Parsed contents of the Parquet "dtour" key_value_metadata entry. */
|
|
39
|
+
export type EmbeddedConfig = {
|
|
40
|
+
spec: DtourSpec;
|
|
41
|
+
colorMap?: Record<string, string>;
|
|
42
|
+
tour?: {
|
|
43
|
+
nDims: number;
|
|
44
|
+
nViews: number;
|
|
45
|
+
views: Float32Array[];
|
|
46
|
+
tourMode?: 'signed' | 'discriminative' | null;
|
|
47
|
+
frameLoadings?: FrameLoading[][];
|
|
48
|
+
/** Human-readable description of the tour (shown in description sub-bar). */
|
|
49
|
+
tourDescription?: string;
|
|
50
|
+
/** Template for per-frame tooltip, with {dim1}, {dim2}, {relation} placeholders. */
|
|
51
|
+
tourFrameDescription?: string;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const SPEC_SHAPE_KEYS = Object.keys(dtourSpecSchema.shape) as (keyof DtourSpec)[];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse the raw JSON "dtour" value from Parquet key_value_metadata.
|
|
59
|
+
* Returns null if the string is falsy or unparseable.
|
|
60
|
+
* Invalid spec fields are silently dropped.
|
|
61
|
+
*/
|
|
62
|
+
export function parseEmbeddedConfig(raw: string | undefined): EmbeddedConfig | null {
|
|
63
|
+
if (!raw) return null;
|
|
64
|
+
|
|
65
|
+
let obj: Record<string, unknown>;
|
|
66
|
+
try {
|
|
67
|
+
obj = JSON.parse(raw);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (typeof obj !== 'object' || obj === null) return null;
|
|
72
|
+
|
|
73
|
+
// Validate each spec field individually — invalid fields are dropped
|
|
74
|
+
// without affecting valid ones.
|
|
75
|
+
const spec: Record<string, unknown> = {};
|
|
76
|
+
for (const key of SPEC_SHAPE_KEYS) {
|
|
77
|
+
if (!(key in obj)) continue;
|
|
78
|
+
const fieldSchema = dtourSpecSchema.shape[key];
|
|
79
|
+
const result = fieldSchema.safeParse(obj[key]);
|
|
80
|
+
if (result.success) spec[key] = result.data;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Extract colorMap (label → hex string)
|
|
84
|
+
let colorMap: Record<string, string> | undefined;
|
|
85
|
+
if (obj.colorMap && typeof obj.colorMap === 'object' && !Array.isArray(obj.colorMap)) {
|
|
86
|
+
const cm = obj.colorMap as Record<string, unknown>;
|
|
87
|
+
const valid: Record<string, string> = {};
|
|
88
|
+
let hasEntries = false;
|
|
89
|
+
for (const [k, v] of Object.entries(cm)) {
|
|
90
|
+
if (typeof v === 'string') {
|
|
91
|
+
valid[k] = v;
|
|
92
|
+
hasEntries = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (hasEntries) colorMap = valid;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Extract tour views (base64 float32 column-major)
|
|
99
|
+
let tour: EmbeddedConfig['tour'] | undefined;
|
|
100
|
+
if (obj.tour && typeof obj.tour === 'object') {
|
|
101
|
+
const t = obj.tour as Record<string, unknown>;
|
|
102
|
+
const nDims = typeof t.nDims === 'number' ? t.nDims : 0;
|
|
103
|
+
const nViews = typeof t.nViews === 'number' ? t.nViews : 0;
|
|
104
|
+
const viewsB64 = typeof t.views === 'string' ? t.views : '';
|
|
105
|
+
if (nDims >= 2 && nViews >= 2 && viewsB64) {
|
|
106
|
+
try {
|
|
107
|
+
const binary = atob(viewsB64);
|
|
108
|
+
const bytes = new Uint8Array(binary.length);
|
|
109
|
+
for (let i = 0; i < binary.length; i++) {
|
|
110
|
+
bytes[i] = binary.charCodeAt(i);
|
|
111
|
+
}
|
|
112
|
+
const floats = new Float32Array(bytes.buffer);
|
|
113
|
+
const stride = nDims * 2;
|
|
114
|
+
if (floats.length === nViews * stride) {
|
|
115
|
+
const views: Float32Array[] = [];
|
|
116
|
+
for (let v = 0; v < nViews; v++) {
|
|
117
|
+
views.push(floats.slice(v * stride, (v + 1) * stride));
|
|
118
|
+
}
|
|
119
|
+
tour = { nDims, nViews, views };
|
|
120
|
+
|
|
121
|
+
// Parse tourMode
|
|
122
|
+
if (t.tourMode === 'signed' || t.tourMode === 'discriminative') {
|
|
123
|
+
tour.tourMode = t.tourMode;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Parse tourDescription and tourFrameDescription
|
|
127
|
+
if (typeof t.tourDescription === 'string') {
|
|
128
|
+
tour.tourDescription = t.tourDescription;
|
|
129
|
+
}
|
|
130
|
+
if (typeof t.tourFrameDescription === 'string') {
|
|
131
|
+
tour.tourFrameDescription = t.tourFrameDescription;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Parse frameLoadings: array of [[name, coeff], [name, coeff]] per view
|
|
135
|
+
if (Array.isArray(t.frameLoadings)) {
|
|
136
|
+
const fl: FrameLoading[][] = [];
|
|
137
|
+
let valid = true;
|
|
138
|
+
for (const frame of t.frameLoadings as unknown[][]) {
|
|
139
|
+
if (!Array.isArray(frame)) {
|
|
140
|
+
valid = false;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
const pairs: FrameLoading[] = [];
|
|
144
|
+
for (const pair of frame) {
|
|
145
|
+
if (
|
|
146
|
+
Array.isArray(pair) &&
|
|
147
|
+
pair.length === 2 &&
|
|
148
|
+
typeof pair[0] === 'string' &&
|
|
149
|
+
typeof pair[1] === 'number'
|
|
150
|
+
) {
|
|
151
|
+
pairs.push([pair[0] as string, pair[1] as number]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
fl.push(pairs);
|
|
155
|
+
}
|
|
156
|
+
if (valid && fl.length > 0) tour.frameLoadings = fl;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
// Invalid base64 — skip tour
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return { spec: spec as DtourSpec, colorMap, tour };
|
|
166
|
+
}
|
|
167
|
+
|
|
30
168
|
export const DTOUR_DEFAULTS: Required<DtourSpec> = {
|
|
31
169
|
tourBy: 'dimensions',
|
|
32
170
|
tourPosition: 0,
|
|
@@ -44,5 +182,10 @@ export const DTOUR_DEFAULTS: Required<DtourSpec> = {
|
|
|
44
182
|
cameraZoom: 1 / 1.5,
|
|
45
183
|
viewMode: 'guided',
|
|
46
184
|
showLegend: true,
|
|
185
|
+
showAxes: false,
|
|
186
|
+
showFrameNumbers: false,
|
|
187
|
+
showFrameLoadings: true,
|
|
188
|
+
showTourDescription: false,
|
|
189
|
+
sliderSpacing: 'equal',
|
|
47
190
|
themeMode: 'dark',
|
|
48
191
|
};
|
package/src/state/atoms.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Metadata } from '@dtour/scatter';
|
|
2
2
|
import { atom } from 'jotai';
|
|
3
|
+
import type { EmbeddedConfig, FrameLoading } from '../spec.ts';
|
|
3
4
|
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// Tour state — controls position and playback along the tour path
|
|
@@ -13,6 +14,12 @@ export const tourPlayingAtom = atom(false);
|
|
|
13
14
|
export const tourSpeedAtom = atom(1);
|
|
14
15
|
export const tourDirectionAtom = atom<1 | -1>(1);
|
|
15
16
|
|
|
17
|
+
/** Slider spacing mode: 'equal' = uniform tick spacing, 'geodesic' = arc-length proportional. */
|
|
18
|
+
export const sliderSpacingAtom = atom<'equal' | 'geodesic'>('equal');
|
|
19
|
+
|
|
20
|
+
/** Cumulative arc-lengths for the current tour bases. null when no tour is loaded. */
|
|
21
|
+
export const arcLengthsAtom = atom<Float32Array | null>(null);
|
|
22
|
+
|
|
16
23
|
// ---------------------------------------------------------------------------
|
|
17
24
|
// View state — controls preview layout and keyframe selection
|
|
18
25
|
// ---------------------------------------------------------------------------
|
|
@@ -22,6 +29,34 @@ export const previewScaleAtom = atom<1 | 0.75 | 0.5>(1);
|
|
|
22
29
|
export const previewPaddingAtom = atom(12);
|
|
23
30
|
export const selectedKeyframeAtom = atom<number | null>(null);
|
|
24
31
|
|
|
32
|
+
/** Which gallery preview is currently hovered (index), or null. */
|
|
33
|
+
export const hoveredKeyframeAtom = atom<number | null>(null);
|
|
34
|
+
|
|
35
|
+
/** Preview center positions relative to the container center, plus preview size. */
|
|
36
|
+
export const previewCentersAtom = atom<{ x: number; y: number; size: number }[]>([]);
|
|
37
|
+
|
|
38
|
+
/** Derived: nearest keyframe to the current tour position. */
|
|
39
|
+
export const currentKeyframeAtom = atom((get) => {
|
|
40
|
+
const position = get(tourPositionAtom);
|
|
41
|
+
const arcLengths = get(arcLengthsAtom);
|
|
42
|
+
const previewCount = get(previewCountAtom);
|
|
43
|
+
if (!arcLengths || arcLengths.length < 2) {
|
|
44
|
+
return Math.round(position * previewCount) % previewCount;
|
|
45
|
+
}
|
|
46
|
+
const n = arcLengths.length - 1;
|
|
47
|
+
let best = 0;
|
|
48
|
+
let bestDist = 1;
|
|
49
|
+
for (let i = 0; i < n; i++) {
|
|
50
|
+
let dist = Math.abs(position - arcLengths[i]!);
|
|
51
|
+
dist = Math.min(dist, 1 - dist);
|
|
52
|
+
if (dist < bestDist) {
|
|
53
|
+
bestDist = dist;
|
|
54
|
+
best = i;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return best;
|
|
58
|
+
});
|
|
59
|
+
|
|
25
60
|
// ---------------------------------------------------------------------------
|
|
26
61
|
// Point style — visual appearance of scatter points
|
|
27
62
|
// ---------------------------------------------------------------------------
|
|
@@ -67,6 +102,9 @@ export const guidedSuspendedAtom = atom(false);
|
|
|
67
102
|
/** Target mode after grand ease-out completes. null = not exiting. */
|
|
68
103
|
export const grandExitTargetAtom = atom<'guided' | 'manual' | null>(null);
|
|
69
104
|
|
|
105
|
+
/** True when the 3D camera is rotated away from front-on (manual mode only). */
|
|
106
|
+
export const is3dRotatedAtom = atom(false);
|
|
107
|
+
|
|
70
108
|
/**
|
|
71
109
|
* Tracks the currently-displayed projection basis (p×2 column-major).
|
|
72
110
|
* Updated by tour interpolation, manual axis dragging, and zen animation.
|
|
@@ -99,6 +137,9 @@ export const canvasSizeAtom = atom({ width: 0, height: 0 });
|
|
|
99
137
|
|
|
100
138
|
export const metadataAtom = atom<Metadata | null>(null);
|
|
101
139
|
|
|
140
|
+
/** Parsed embedded config from Parquet key_value_metadata. Reset on each data load. */
|
|
141
|
+
export const embeddedConfigAtom = atom<EmbeddedConfig | null>(null);
|
|
142
|
+
|
|
102
143
|
// ---------------------------------------------------------------------------
|
|
103
144
|
// Column visibility — which numeric dimensions participate in the tour
|
|
104
145
|
// ---------------------------------------------------------------------------
|
|
@@ -129,6 +170,30 @@ export const activeIndicesAtom = atom<number[]>((get) => {
|
|
|
129
170
|
/** User preference for showing the legend panel. */
|
|
130
171
|
export const showLegendAtom = atom(true);
|
|
131
172
|
|
|
173
|
+
/** User preference for showing axis biplot in guided mode. */
|
|
174
|
+
export const showAxesAtom = atom(false);
|
|
175
|
+
|
|
176
|
+
/** User preference for showing frame numbers on preview thumbnails. */
|
|
177
|
+
export const showFrameNumbersAtom = atom(false);
|
|
178
|
+
|
|
179
|
+
/** User preference for showing feature loading pills on preview thumbnails. */
|
|
180
|
+
export const showFrameLoadingsAtom = atom(true);
|
|
181
|
+
|
|
182
|
+
/** User preference for showing the tour description sub-bar. */
|
|
183
|
+
export const showTourDescriptionAtom = atom(false);
|
|
184
|
+
|
|
185
|
+
/** Per-frame top-2 feature correlations from embedded tour config. */
|
|
186
|
+
export const frameLoadingsAtom = atom<FrameLoading[][] | null>(null);
|
|
187
|
+
|
|
188
|
+
/** Tour mode from embedded config: null (vanilla), "signed", or "discriminative". */
|
|
189
|
+
export const tourModeAtom = atom<'signed' | 'discriminative' | null>(null);
|
|
190
|
+
|
|
191
|
+
/** Tour description string from embedded config (shown in description sub-bar). */
|
|
192
|
+
export const tourDescriptionAtom = atom<string | null>(null);
|
|
193
|
+
|
|
194
|
+
/** Per-frame tooltip template from embedded config, with {dim1}, {dim2}, {relation} placeholders. */
|
|
195
|
+
export const tourFrameDescriptionAtom = atom<string | null>(null);
|
|
196
|
+
|
|
132
197
|
/**
|
|
133
198
|
* Derived: legend is visible only when showLegend is true, metadata is loaded,
|
|
134
199
|
* AND points are colored by a known data column (numeric or categorical).
|
package/src/state/spec-sync.ts
CHANGED
|
@@ -12,7 +12,12 @@ import {
|
|
|
12
12
|
previewCountAtom,
|
|
13
13
|
previewPaddingAtom,
|
|
14
14
|
previewScaleAtom,
|
|
15
|
+
showAxesAtom,
|
|
16
|
+
showFrameLoadingsAtom,
|
|
17
|
+
showFrameNumbersAtom,
|
|
15
18
|
showLegendAtom,
|
|
19
|
+
showTourDescriptionAtom,
|
|
20
|
+
sliderSpacingAtom,
|
|
16
21
|
themeModeAtom,
|
|
17
22
|
tourByAtom,
|
|
18
23
|
tourDirectionAtom,
|
|
@@ -61,6 +66,11 @@ const SPEC_ATOM_MAP = {
|
|
|
61
66
|
cameraZoom: entry(cameraZoomAtom),
|
|
62
67
|
viewMode: entry(viewModeAtom),
|
|
63
68
|
showLegend: entry(showLegendAtom),
|
|
69
|
+
showAxes: entry(showAxesAtom),
|
|
70
|
+
showFrameNumbers: entry(showFrameNumbersAtom),
|
|
71
|
+
showFrameLoadings: entry(showFrameLoadingsAtom),
|
|
72
|
+
showTourDescription: entry(showTourDescriptionAtom),
|
|
73
|
+
sliderSpacing: entry(sliderSpacingAtom),
|
|
64
74
|
themeMode: entry(themeModeAtom),
|
|
65
75
|
} as const;
|
|
66
76
|
|
|
@@ -105,6 +115,11 @@ export function initStoreFromSpec(
|
|
|
105
115
|
spec: DtourSpec | undefined,
|
|
106
116
|
): void {
|
|
107
117
|
if (!spec) return;
|
|
118
|
+
applySpecToStore(store, spec);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Write spec values into the jotai store. Skips undefined fields. */
|
|
122
|
+
export function applySpecToStore(store: ReturnType<typeof useStore>, spec: DtourSpec): void {
|
|
108
123
|
for (const key of SPEC_KEYS) {
|
|
109
124
|
const value = spec[key];
|
|
110
125
|
if (value !== undefined) {
|
package/src/styles.css
CHANGED
|
@@ -3,27 +3,27 @@
|
|
|
3
3
|
@import "tw-animate-css";
|
|
4
4
|
|
|
5
5
|
@theme {
|
|
6
|
-
--color-dtour-bg:
|
|
7
|
-
--color-dtour-surface:
|
|
8
|
-
--color-dtour-border:
|
|
9
|
-
--color-dtour-text:
|
|
10
|
-
--color-dtour-text-muted:
|
|
11
|
-
--color-dtour-accent:
|
|
12
|
-
--color-dtour-accent-hover:
|
|
13
|
-
--color-dtour-highlight:
|
|
6
|
+
--color-dtour-bg: oklch(0% 0 0);
|
|
7
|
+
--color-dtour-surface: oklch(18.66% 0 0);
|
|
8
|
+
--color-dtour-border: oklch(27.06% 0 0);
|
|
9
|
+
--color-dtour-text: oklch(79.91% 0 0);
|
|
10
|
+
--color-dtour-text-muted: oklch(61.66% 0 0);
|
|
11
|
+
--color-dtour-accent: oklch(61.13% 0.1707 259.68);
|
|
12
|
+
--color-dtour-accent-hover: oklch(65.65% 0.1581 258.37);
|
|
13
|
+
--color-dtour-highlight: oklch(100% 0 0);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/* --- Light theme overrides --- */
|
|
17
17
|
|
|
18
18
|
:where(.dtour-light) {
|
|
19
|
-
--color-dtour-bg:
|
|
20
|
-
--color-dtour-surface:
|
|
21
|
-
--color-dtour-border:
|
|
22
|
-
--color-dtour-text:
|
|
23
|
-
--color-dtour-text-muted:
|
|
24
|
-
--color-dtour-accent:
|
|
25
|
-
--color-dtour-accent-hover:
|
|
26
|
-
--color-dtour-highlight:
|
|
19
|
+
--color-dtour-bg: oklch(100% 0 0);
|
|
20
|
+
--color-dtour-surface: oklch(95.51% 0 0);
|
|
21
|
+
--color-dtour-border: oklch(85.76% 0 0);
|
|
22
|
+
--color-dtour-text: oklch(32.11% 0 0);
|
|
23
|
+
--color-dtour-text-muted: oklch(56.93% 0 0);
|
|
24
|
+
--color-dtour-accent: oklch(50.48% 0.164 258.95);
|
|
25
|
+
--color-dtour-accent-hover: oklch(44.94% 0.1531 259.62);
|
|
26
|
+
--color-dtour-highlight: oklch(0% 0 0);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/* --- Animation easing utility for tw-animate-css --- */
|