@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.
Files changed (63) hide show
  1. package/dist/Dtour.d.ts +5 -1
  2. package/dist/Dtour.d.ts.map +1 -1
  3. package/dist/DtourViewer.d.ts +4 -1
  4. package/dist/DtourViewer.d.ts.map +1 -1
  5. package/dist/components/AxisOverlay.d.ts +11 -1
  6. package/dist/components/AxisOverlay.d.ts.map +1 -1
  7. package/dist/components/CircularSlider.d.ts +21 -2
  8. package/dist/components/CircularSlider.d.ts.map +1 -1
  9. package/dist/components/DtourToolbar.d.ts +2 -1
  10. package/dist/components/DtourToolbar.d.ts.map +1 -1
  11. package/dist/components/Gallery.d.ts +3 -3
  12. package/dist/components/Gallery.d.ts.map +1 -1
  13. package/dist/components/RevertCameraButton.d.ts +6 -0
  14. package/dist/components/RevertCameraButton.d.ts.map +1 -0
  15. package/dist/components/ui/checkbox.d.ts +6 -0
  16. package/dist/components/ui/checkbox.d.ts.map +1 -0
  17. package/dist/hooks/usePlayback.d.ts +7 -5
  18. package/dist/hooks/usePlayback.d.ts.map +1 -1
  19. package/dist/hooks/useScatter.d.ts.map +1 -1
  20. package/dist/index.d.ts +4 -4
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/layout/gallery-positions.d.ts +3 -1
  23. package/dist/layout/gallery-positions.d.ts.map +1 -1
  24. package/dist/layout/selector-size.d.ts +4 -2
  25. package/dist/layout/selector-size.d.ts.map +1 -1
  26. package/dist/lib/arcball.d.ts +21 -0
  27. package/dist/lib/arcball.d.ts.map +1 -0
  28. package/dist/lib/position-remap.d.ts +16 -0
  29. package/dist/lib/position-remap.d.ts.map +1 -0
  30. package/dist/lib/throttle-debounce.d.ts +28 -0
  31. package/dist/lib/throttle-debounce.d.ts.map +1 -0
  32. package/dist/radial-chart/RadialChart.d.ts +5 -1
  33. package/dist/radial-chart/RadialChart.d.ts.map +1 -1
  34. package/dist/spec.d.ts +32 -0
  35. package/dist/spec.d.ts.map +1 -1
  36. package/dist/state/atoms.d.ts +67 -0
  37. package/dist/state/atoms.d.ts.map +1 -1
  38. package/dist/state/spec-sync.d.ts +2 -0
  39. package/dist/state/spec-sync.d.ts.map +1 -1
  40. package/dist/viewer.css +1 -1
  41. package/dist/viewer.js +11620 -10118
  42. package/package.json +6 -1
  43. package/src/Dtour.tsx +82 -9
  44. package/src/DtourViewer.tsx +480 -100
  45. package/src/components/AxisOverlay.tsx +332 -182
  46. package/src/components/CircularSlider.tsx +363 -174
  47. package/src/components/DtourToolbar.tsx +121 -10
  48. package/src/components/Gallery.tsx +197 -39
  49. package/src/components/RevertCameraButton.tsx +39 -0
  50. package/src/components/ui/checkbox.tsx +32 -0
  51. package/src/hooks/usePlayback.ts +18 -44
  52. package/src/hooks/useScatter.ts +21 -5
  53. package/src/index.ts +16 -3
  54. package/src/layout/gallery-positions.ts +15 -4
  55. package/src/layout/selector-size.ts +24 -10
  56. package/src/lib/arcball.ts +119 -0
  57. package/src/lib/position-remap.ts +51 -0
  58. package/src/lib/throttle-debounce.ts +79 -0
  59. package/src/radial-chart/RadialChart.tsx +45 -6
  60. package/src/spec.ts +143 -0
  61. package/src/state/atoms.ts +65 -0
  62. package/src/state/spec-sync.ts +15 -0
  63. package/src/styles.css +16 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtour/viewer",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/viewer.js",
6
6
  "exports": {
@@ -10,6 +10,11 @@
10
10
  },
11
11
  "./dist/viewer.css": "./dist/viewer.css"
12
12
  },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/flekschas/dtour",
16
+ "directory": "packages/viewer"
17
+ },
13
18
  "scripts": {
14
19
  "build": "vite build && tsc --emitDeclarationOnly --declaration --outDir dist",
15
20
  "dev": "vite build --watch"
package/src/Dtour.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ScatterInstance, ScatterStatus } from '@dtour/scatter';
2
2
  import { bitPackIndices } from '@dtour/scatter';
3
- import { Provider, createStore, useAtomValue, useSetAtom } from 'jotai';
3
+ import { Provider, createStore, useAtomValue, useSetAtom, useStore } from 'jotai';
4
4
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
5
  import { DtourViewer } from './DtourViewer.tsx';
6
6
  import { ColorLegend } from './components/ColorLegend.tsx';
@@ -13,14 +13,20 @@ import type { DtourSpec } from './spec.ts';
13
13
  import {
14
14
  backgroundColorAtom,
15
15
  colorMapAtom,
16
+ embeddedConfigAtom,
17
+ frameLoadingsAtom,
16
18
  legendSelectionAtom,
17
19
  legendVisibleAtom,
18
20
  metadataAtom,
19
21
  pointColorAtom,
20
22
  resolvedThemeAtom,
23
+ showTourDescriptionAtom,
24
+ tourDescriptionAtom,
25
+ tourFrameDescriptionAtom,
26
+ tourModeAtom,
21
27
  viewModeAtom,
22
28
  } from './state/atoms.ts';
23
- import { initStoreFromSpec, useSpecSync } from './state/spec-sync.ts';
29
+ import { applySpecToStore, initStoreFromSpec, useSpecSync } from './state/spec-sync.ts';
24
30
 
25
31
  export type DtourHandle = {
26
32
  /** Select points by index array or bit-packed mask. */
@@ -53,6 +59,8 @@ export type DtourProps = {
53
59
  hideToolbar?: boolean;
54
60
  /** Called when the user requests loading new data via the toolbar file picker. */
55
61
  onLoadData?: (data: ArrayBuffer, fileName: string) => void;
62
+ /** Called when the user clicks the toolbar logo. */
63
+ onLogoClick?: () => void;
56
64
  /** Fires when legend selection changes for a categorical color column. Reports selected label names or empty array when cleared. */
57
65
  onSelectionChange?: (labels: string[]) => void;
58
66
  /** Per-label color map. Values are hex strings or theme-aware {light, dark} objects. */
@@ -61,6 +69,8 @@ export type DtourProps = {
61
69
  portalContainer?: HTMLElement;
62
70
  /** Called when the viewer is ready with an API handle for programmatic control. */
63
71
  onReady?: (api: DtourHandle) => void;
72
+ /** Rendering backend. Default 'webgpu'. */
73
+ backend?: 'webgpu' | 'webgl';
64
74
  };
65
75
 
66
76
  export const Dtour = ({
@@ -74,10 +84,12 @@ export const Dtour = ({
74
84
  onStatus,
75
85
  hideToolbar = false,
76
86
  onLoadData,
87
+ onLogoClick,
77
88
  onSelectionChange,
78
89
  colorMap,
79
90
  portalContainer,
80
91
  onReady,
92
+ backend,
81
93
  }: DtourProps) => {
82
94
  // Each Dtour instance gets its own isolated jotai store.
83
95
  // Eagerly apply initial spec values so there's no flash of defaults.
@@ -102,9 +114,11 @@ export const Dtour = ({
102
114
  onStatus={onStatus}
103
115
  hideToolbar={hideToolbar}
104
116
  onLoadData={onLoadData}
117
+ onLogoClick={onLogoClick}
105
118
  onSelectionChange={onSelectionChange}
106
119
  colorMap={colorMap}
107
120
  onReady={onReady}
121
+ backend={backend}
108
122
  />
109
123
  </Provider>
110
124
  </PortalContainerContext.Provider>
@@ -123,9 +137,11 @@ const DtourInner = ({
123
137
  onStatus,
124
138
  hideToolbar,
125
139
  onLoadData,
140
+ onLogoClick,
126
141
  onSelectionChange,
127
142
  colorMap,
128
143
  onReady,
144
+ backend,
129
145
  }: {
130
146
  data: ArrayBuffer | undefined;
131
147
  views: Float32Array[] | undefined;
@@ -137,24 +153,64 @@ const DtourInner = ({
137
153
  onStatus: ((status: ScatterStatus) => void) | undefined;
138
154
  hideToolbar: boolean;
139
155
  onLoadData: ((data: ArrayBuffer, fileName: string) => void) | undefined;
156
+ onLogoClick: (() => void) | undefined;
140
157
  onSelectionChange: ((labels: string[]) => void) | undefined;
141
158
  colorMap: Record<string, string | { light: string; dark: string }> | undefined;
142
159
  onReady: ((api: DtourHandle) => void) | undefined;
160
+ backend: 'webgpu' | 'webgl' | undefined;
143
161
  }) => {
144
162
  useSpecSync(spec, onSpecChange);
145
163
  useModeCycling();
146
164
  useSystemTheme();
147
165
 
148
- // Sync colorMap prop atom
166
+ // ── Apply embedded config from Parquet metadata ──────────────────────
167
+ const embeddedConfig = useAtomValue(embeddedConfigAtom);
168
+ const store = useStore();
169
+ const embeddedAppliedRef = useRef(false);
170
+
171
+ // Reset when data changes so the next file's embedded config can apply
172
+ // biome-ignore lint/correctness/useExhaustiveDependencies: data triggers reset
173
+ useEffect(() => {
174
+ embeddedAppliedRef.current = false;
175
+ }, [data]);
176
+
177
+ // Apply embedded spec fields that are NOT overridden by the prop spec
178
+ useEffect(() => {
179
+ if (!embeddedConfig || embeddedAppliedRef.current) return;
180
+ embeddedAppliedRef.current = true;
181
+
182
+ const fieldsToApply: DtourSpec = {};
183
+ for (const [key, value] of Object.entries(embeddedConfig.spec)) {
184
+ if (spec?.[key as keyof DtourSpec] === undefined) {
185
+ (fieldsToApply as Record<string, unknown>)[key] = value;
186
+ }
187
+ }
188
+ applySpecToStore(store, fieldsToApply);
189
+ }, [embeddedConfig, spec, store]);
190
+
191
+ // Sync colorMap prop → atom (embedded colorMap used as fallback)
149
192
  const setColorMap = useSetAtom(colorMapAtom);
150
193
  useEffect(() => {
151
- setColorMap(colorMap ?? null);
152
- }, [colorMap, setColorMap]);
194
+ setColorMap(colorMap ?? embeddedConfig?.colorMap ?? null);
195
+ }, [colorMap, embeddedConfig, setColorMap]);
196
+
197
+ // Sync tour frame loadings, tour mode, and description strings from embedded config
198
+ const setFrameLoadings = useSetAtom(frameLoadingsAtom);
199
+ const setTourMode = useSetAtom(tourModeAtom);
200
+ const setTourDescription = useSetAtom(tourDescriptionAtom);
201
+ const setTourFrameDescription = useSetAtom(tourFrameDescriptionAtom);
202
+ useEffect(() => {
203
+ setFrameLoadings(embeddedConfig?.tour?.frameLoadings ?? null);
204
+ setTourMode(embeddedConfig?.tour?.tourMode ?? null);
205
+ setTourDescription(embeddedConfig?.tour?.tourDescription ?? null);
206
+ setTourFrameDescription(embeddedConfig?.tour?.tourFrameDescription ?? null);
207
+ }, [embeddedConfig, setFrameLoadings, setTourMode, setTourDescription, setTourFrameDescription]);
153
208
 
154
209
  // Sync resolved theme → background color + CSS class
155
210
  const resolvedTheme = useAtomValue(resolvedThemeAtom);
156
211
  const setBackgroundColor = useSetAtom(backgroundColorAtom);
157
212
  useEffect(() => {
213
+ // sRGB values matching --color-dtour-bg: dark=#000000, light=#ffffff
158
214
  setBackgroundColor(resolvedTheme === 'light' ? [1, 1, 1] : [0, 0, 0]);
159
215
  }, [resolvedTheme, setBackgroundColor]);
160
216
 
@@ -211,6 +267,13 @@ const DtourInner = ({
211
267
  const isGrand = viewMode === 'grand';
212
268
  const legendVisible = useAtomValue(legendVisibleAtom);
213
269
 
270
+ // Tour description sub-bar
271
+ const showTourDescription = useAtomValue(showTourDescriptionAtom);
272
+ const tourDescription = useAtomValue(tourDescriptionAtom);
273
+ const descriptionVisible =
274
+ showTourDescription && viewMode === 'guided' && tourDescription !== null;
275
+ const effectiveToolbarHeight = hideToolbar ? 0 : descriptionVisible ? 72 : 40;
276
+
214
277
  // Sidebar width state — remembered across open/close cycles
215
278
  const [sidebarWidth, setSidebarWidth] = useState(200);
216
279
  const [dragging, setDragging] = useState(false);
@@ -253,13 +316,22 @@ const DtourInner = ({
253
316
  >
254
317
  {/* Canvas panel — grows to fill remaining space */}
255
318
  <div className="relative flex-1 min-w-0">
256
- {/* Toolbar inside left panel so it shrinks with the legend */}
319
+ {/* Toolbar + optional description sub-bar */}
257
320
  <div
258
- className={`absolute inset-x-0 top-0 z-10 h-10 transition-[transform,opacity] duration-300 ease-out ${
321
+ className={`absolute inset-x-0 top-0 z-10 transition-[transform,opacity] duration-300 ease-out ${
259
322
  isGrand ? '-translate-y-full' : 'translate-y-0'
260
323
  } ${hideToolbar ? 'opacity-0 pointer-events-none' : 'opacity-100'}`}
261
324
  >
262
- <DtourToolbar onLoadData={onLoadData} />
325
+ <div className="h-10">
326
+ <DtourToolbar onLoadData={onLoadData} onLogoClick={onLogoClick} />
327
+ </div>
328
+ {descriptionVisible && (
329
+ <div className="h-8 flex items-center justify-center border-b border-dtour-surface bg-dtour-bg px-3">
330
+ <span className="text-[11px] text-dtour-text-muted italic">
331
+ <strong>Tour:</strong> {tourDescription}
332
+ </span>
333
+ </div>
334
+ )}
263
335
  </div>
264
336
  <div className="absolute inset-0 overflow-hidden">
265
337
  <DtourViewer
@@ -269,8 +341,9 @@ const DtourInner = ({
269
341
  metricTracks={metricTracks}
270
342
  metricBarWidth={metricBarWidth}
271
343
  onStatus={onStatus}
272
- toolbarHeight={hideToolbar ? 0 : 40}
344
+ toolbarHeight={effectiveToolbarHeight}
273
345
  onScatterReady={setScatterInstance}
346
+ backend={backend}
274
347
  />
275
348
  </div>
276
349
  </div>