@dtour/viewer 0.1.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 (114) hide show
  1. package/dist/Dtour.d.ts +46 -0
  2. package/dist/Dtour.d.ts.map +1 -0
  3. package/dist/DtourViewer.d.ts +24 -0
  4. package/dist/DtourViewer.d.ts.map +1 -0
  5. package/dist/components/AxisOverlay.d.ts +9 -0
  6. package/dist/components/AxisOverlay.d.ts.map +1 -0
  7. package/dist/components/CircularSlider.d.ts +16 -0
  8. package/dist/components/CircularSlider.d.ts.map +1 -0
  9. package/dist/components/ColorLegend.d.ts +2 -0
  10. package/dist/components/ColorLegend.d.ts.map +1 -0
  11. package/dist/components/DtourToolbar.d.ts +5 -0
  12. package/dist/components/DtourToolbar.d.ts.map +1 -0
  13. package/dist/components/Gallery.d.ts +12 -0
  14. package/dist/components/Gallery.d.ts.map +1 -0
  15. package/dist/components/LassoOverlay.d.ts +9 -0
  16. package/dist/components/LassoOverlay.d.ts.map +1 -0
  17. package/dist/components/Logo.d.ts +2 -0
  18. package/dist/components/Logo.d.ts.map +1 -0
  19. package/dist/components/ui/button.d.ts +12 -0
  20. package/dist/components/ui/button.d.ts.map +1 -0
  21. package/dist/components/ui/dropdown-menu.d.ts +10 -0
  22. package/dist/components/ui/dropdown-menu.d.ts.map +1 -0
  23. package/dist/components/ui/slider.d.ts +6 -0
  24. package/dist/components/ui/slider.d.ts.map +1 -0
  25. package/dist/components/ui/tooltip.d.ts +8 -0
  26. package/dist/components/ui/tooltip.d.ts.map +1 -0
  27. package/dist/hooks/useAnimatePosition.d.ts +13 -0
  28. package/dist/hooks/useAnimatePosition.d.ts.map +1 -0
  29. package/dist/hooks/useGrandTour.d.ts +14 -0
  30. package/dist/hooks/useGrandTour.d.ts.map +1 -0
  31. package/dist/hooks/useLongPressIndicator.d.ts +5 -0
  32. package/dist/hooks/useLongPressIndicator.d.ts.map +1 -0
  33. package/dist/hooks/useModeCycling.d.ts +12 -0
  34. package/dist/hooks/useModeCycling.d.ts.map +1 -0
  35. package/dist/hooks/usePlayback.d.ts +9 -0
  36. package/dist/hooks/usePlayback.d.ts.map +1 -0
  37. package/dist/hooks/useScatter.d.ts +10 -0
  38. package/dist/hooks/useScatter.d.ts.map +1 -0
  39. package/dist/hooks/useSystemTheme.d.ts +6 -0
  40. package/dist/hooks/useSystemTheme.d.ts.map +1 -0
  41. package/dist/index.d.ts +16 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/layout/gallery-positions.d.ts +38 -0
  44. package/dist/layout/gallery-positions.d.ts.map +1 -0
  45. package/dist/layout/selector-size.d.ts +15 -0
  46. package/dist/layout/selector-size.d.ts.map +1 -0
  47. package/dist/lib/color-utils.d.ts +7 -0
  48. package/dist/lib/color-utils.d.ts.map +1 -0
  49. package/dist/lib/gram-schmidt.d.ts +9 -0
  50. package/dist/lib/gram-schmidt.d.ts.map +1 -0
  51. package/dist/lib/utils.d.ts +3 -0
  52. package/dist/lib/utils.d.ts.map +1 -0
  53. package/dist/portal-container.d.ts +10 -0
  54. package/dist/portal-container.d.ts.map +1 -0
  55. package/dist/radial-chart/RadialChart.d.ts +13 -0
  56. package/dist/radial-chart/RadialChart.d.ts.map +1 -0
  57. package/dist/radial-chart/arc-path.d.ts +23 -0
  58. package/dist/radial-chart/arc-path.d.ts.map +1 -0
  59. package/dist/radial-chart/index.d.ts +5 -0
  60. package/dist/radial-chart/index.d.ts.map +1 -0
  61. package/dist/radial-chart/parse-metrics.d.ts +10 -0
  62. package/dist/radial-chart/parse-metrics.d.ts.map +1 -0
  63. package/dist/radial-chart/types.d.ts +23 -0
  64. package/dist/radial-chart/types.d.ts.map +1 -0
  65. package/dist/spec.d.ts +42 -0
  66. package/dist/spec.d.ts.map +1 -0
  67. package/dist/state/atoms.d.ts +150 -0
  68. package/dist/state/atoms.d.ts.map +1 -0
  69. package/dist/state/spec-sync.d.ts +5 -0
  70. package/dist/state/spec-sync.d.ts.map +1 -0
  71. package/dist/viewer.css +3 -0
  72. package/dist/viewer.js +14501 -0
  73. package/dist/views.d.ts +30 -0
  74. package/dist/views.d.ts.map +1 -0
  75. package/package.json +48 -0
  76. package/src/Dtour.tsx +300 -0
  77. package/src/DtourViewer.tsx +541 -0
  78. package/src/components/AxisOverlay.tsx +224 -0
  79. package/src/components/CircularSlider.tsx +202 -0
  80. package/src/components/ColorLegend.tsx +178 -0
  81. package/src/components/DtourToolbar.tsx +642 -0
  82. package/src/components/Gallery.tsx +166 -0
  83. package/src/components/LassoOverlay.tsx +240 -0
  84. package/src/components/Logo.tsx +37 -0
  85. package/src/components/ui/button.tsx +36 -0
  86. package/src/components/ui/dropdown-menu.tsx +92 -0
  87. package/src/components/ui/slider.tsx +89 -0
  88. package/src/components/ui/tooltip.tsx +45 -0
  89. package/src/hooks/useAnimatePosition.ts +102 -0
  90. package/src/hooks/useGrandTour.ts +176 -0
  91. package/src/hooks/useLongPressIndicator.ts +342 -0
  92. package/src/hooks/useModeCycling.ts +64 -0
  93. package/src/hooks/usePlayback.ts +54 -0
  94. package/src/hooks/useScatter.ts +162 -0
  95. package/src/hooks/useSystemTheme.ts +19 -0
  96. package/src/index.ts +55 -0
  97. package/src/layout/gallery-positions.ts +105 -0
  98. package/src/layout/selector-size.ts +135 -0
  99. package/src/lib/color-utils.ts +22 -0
  100. package/src/lib/gram-schmidt.ts +41 -0
  101. package/src/lib/utils.ts +4 -0
  102. package/src/portal-container.tsx +14 -0
  103. package/src/radial-chart/RadialChart.tsx +184 -0
  104. package/src/radial-chart/arc-path.ts +80 -0
  105. package/src/radial-chart/index.ts +4 -0
  106. package/src/radial-chart/parse-metrics.ts +99 -0
  107. package/src/radial-chart/types.ts +23 -0
  108. package/src/spec.ts +48 -0
  109. package/src/state/atoms.ts +169 -0
  110. package/src/state/spec-sync.ts +190 -0
  111. package/src/styles.css +44 -0
  112. package/src/views.ts +76 -0
  113. package/tsconfig.json +12 -0
  114. package/vite.config.ts +21 -0
@@ -0,0 +1,190 @@
1
+ import type { WritableAtom } from 'jotai';
2
+ import { useStore } from 'jotai';
3
+ import { useEffect, useRef } from 'react';
4
+ import type { DtourSpec } from '../spec.ts';
5
+ import {
6
+ cameraPanXAtom,
7
+ cameraPanYAtom,
8
+ cameraZoomAtom,
9
+ pointColorAtom,
10
+ pointOpacityAtom,
11
+ pointSizeAtom,
12
+ previewCountAtom,
13
+ previewPaddingAtom,
14
+ previewScaleAtom,
15
+ showLegendAtom,
16
+ themeModeAtom,
17
+ tourByAtom,
18
+ tourDirectionAtom,
19
+ tourPlayingAtom,
20
+ tourPositionAtom,
21
+ tourSpeedAtom,
22
+ viewModeAtom,
23
+ } from './atoms.ts';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Spec ↔ Atom mapping
27
+ // ---------------------------------------------------------------------------
28
+
29
+ type SpecEntry<S, A> = {
30
+ atom: WritableAtom<A, [A], void>;
31
+ toAtom: (v: S) => A;
32
+ fromAtom: (v: A) => S;
33
+ };
34
+
35
+ function identity<T>(v: T): T {
36
+ return v;
37
+ }
38
+
39
+ function entry<T>(atom: WritableAtom<T, [T], void>): SpecEntry<T, T> {
40
+ return { atom, toAtom: identity, fromAtom: identity };
41
+ }
42
+
43
+ const SPEC_ATOM_MAP = {
44
+ tourBy: entry(tourByAtom),
45
+ tourPosition: entry(tourPositionAtom),
46
+ tourPlaying: entry(tourPlayingAtom),
47
+ tourSpeed: entry(tourSpeedAtom),
48
+ tourDirection: {
49
+ atom: tourDirectionAtom,
50
+ toAtom: (v: 'forward' | 'backward') => (v === 'forward' ? 1 : -1) as 1 | -1,
51
+ fromAtom: (v: 1 | -1): 'forward' | 'backward' => (v === 1 ? 'forward' : 'backward'),
52
+ },
53
+ previewCount: entry(previewCountAtom),
54
+ previewScale: entry(previewScaleAtom),
55
+ previewPadding: entry(previewPaddingAtom),
56
+ pointSize: entry(pointSizeAtom),
57
+ pointOpacity: entry(pointOpacityAtom),
58
+ pointColor: entry(pointColorAtom),
59
+ cameraPanX: entry(cameraPanXAtom),
60
+ cameraPanY: entry(cameraPanYAtom),
61
+ cameraZoom: entry(cameraZoomAtom),
62
+ viewMode: entry(viewModeAtom),
63
+ showLegend: entry(showLegendAtom),
64
+ themeMode: entry(themeModeAtom),
65
+ } as const;
66
+
67
+ type SpecKey = keyof typeof SPEC_ATOM_MAP;
68
+ const SPEC_KEYS = Object.keys(SPEC_ATOM_MAP) as SpecKey[];
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // Helpers
72
+ // ---------------------------------------------------------------------------
73
+
74
+ function shallowEqual(a: unknown, b: unknown): boolean {
75
+ if (a === b) return true;
76
+ if (Array.isArray(a) && Array.isArray(b)) {
77
+ if (a.length !== b.length) return false;
78
+ for (let i = 0; i < a.length; i++) {
79
+ if (a[i] !== b[i]) return false;
80
+ }
81
+ return true;
82
+ }
83
+ return false;
84
+ }
85
+
86
+ /** Read all atoms from the store and build a full resolved spec. */
87
+ function readFullSpec(store: ReturnType<typeof useStore>): Required<DtourSpec> {
88
+ const spec = {} as Record<string, unknown>;
89
+ for (const key of SPEC_KEYS) {
90
+ const mapping = SPEC_ATOM_MAP[key];
91
+ // biome-ignore lint/suspicious/noExplicitAny: generic atom mapping
92
+ const raw = store.get(mapping.atom as any);
93
+ // biome-ignore lint/suspicious/noExplicitAny: generic transform
94
+ spec[key] = (mapping as any).fromAtom(raw);
95
+ }
96
+ return spec as Required<DtourSpec>;
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Hook: initialize store from spec (called once before first render)
101
+ // ---------------------------------------------------------------------------
102
+
103
+ export function initStoreFromSpec(
104
+ store: ReturnType<typeof useStore>,
105
+ spec: DtourSpec | undefined,
106
+ ): void {
107
+ if (!spec) return;
108
+ for (const key of SPEC_KEYS) {
109
+ const value = spec[key];
110
+ if (value !== undefined) {
111
+ const mapping = SPEC_ATOM_MAP[key];
112
+ // biome-ignore lint/suspicious/noExplicitAny: generic atom mapping
113
+ store.set(mapping.atom as any, (mapping as any).toAtom(value));
114
+ }
115
+ }
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Hook: bidirectional spec ↔ atoms sync
120
+ // ---------------------------------------------------------------------------
121
+
122
+ const DEBOUNCE_MS = 250;
123
+
124
+ export function useSpecSync(
125
+ spec: DtourSpec | undefined,
126
+ onSpecChange: ((spec: Required<DtourSpec>) => void) | undefined,
127
+ ): void {
128
+ const store = useStore();
129
+ const suppressRef = useRef(false);
130
+ const onSpecChangeRef = useRef(onSpecChange);
131
+ onSpecChangeRef.current = onSpecChange;
132
+
133
+ // Inbound: spec prop → atoms
134
+ useEffect(() => {
135
+ if (!spec) return;
136
+ suppressRef.current = true;
137
+
138
+ for (const key of SPEC_KEYS) {
139
+ const value = spec[key];
140
+ if (value === undefined) continue;
141
+ const mapping = SPEC_ATOM_MAP[key];
142
+ // biome-ignore lint/suspicious/noExplicitAny: generic atom mapping
143
+ const atomValue = (mapping as any).toAtom(value);
144
+ // biome-ignore lint/suspicious/noExplicitAny: generic atom mapping
145
+ const current = store.get(mapping.atom as any);
146
+ if (!shallowEqual(atomValue, current)) {
147
+ // biome-ignore lint/suspicious/noExplicitAny: generic atom mapping
148
+ store.set(mapping.atom as any, atomValue);
149
+ }
150
+ }
151
+
152
+ queueMicrotask(() => {
153
+ suppressRef.current = false;
154
+ });
155
+ }, [spec, store]);
156
+
157
+ // Outbound: atoms → onSpecChange (debounced)
158
+ useEffect(() => {
159
+ if (!onSpecChangeRef.current) return;
160
+
161
+ let dirty = false;
162
+ let timerId: ReturnType<typeof setTimeout> | null = null;
163
+
164
+ const flush = () => {
165
+ timerId = null;
166
+ if (suppressRef.current || !dirty) return;
167
+ dirty = false;
168
+ onSpecChangeRef.current?.(readFullSpec(store));
169
+ };
170
+
171
+ const scheduleFlush = () => {
172
+ dirty = true;
173
+ if (timerId === null) {
174
+ timerId = setTimeout(flush, DEBOUNCE_MS);
175
+ }
176
+ };
177
+
178
+ const unsubs: (() => void)[] = [];
179
+ for (const key of SPEC_KEYS) {
180
+ const mapping = SPEC_ATOM_MAP[key];
181
+ // biome-ignore lint/suspicious/noExplicitAny: generic atom mapping
182
+ unsubs.push(store.sub(mapping.atom as any, scheduleFlush));
183
+ }
184
+
185
+ return () => {
186
+ for (const unsub of unsubs) unsub();
187
+ if (timerId !== null) clearTimeout(timerId);
188
+ };
189
+ }, [store]);
190
+ }
package/src/styles.css ADDED
@@ -0,0 +1,44 @@
1
+ @import "tailwindcss/theme";
2
+ @import "tailwindcss/utilities";
3
+ @import "tw-animate-css";
4
+
5
+ @theme {
6
+ --color-dtour-bg: #000000;
7
+ --color-dtour-surface: #222222;
8
+ --color-dtour-border: #333333;
9
+ --color-dtour-text: #bbbbbb;
10
+ --color-dtour-text-muted: #888888;
11
+ --color-dtour-accent: #4080e8;
12
+ --color-dtour-accent-hover: #5090f0;
13
+ --color-dtour-highlight: #ffffff;
14
+ }
15
+
16
+ /* --- Light theme overrides --- */
17
+
18
+ :where(.dtour-light) {
19
+ --color-dtour-bg: #ffffff;
20
+ --color-dtour-surface: #f0f0f0;
21
+ --color-dtour-border: #d0d0d0;
22
+ --color-dtour-text: #333333;
23
+ --color-dtour-text-muted: #777777;
24
+ --color-dtour-accent: #2060c0;
25
+ --color-dtour-accent-hover: #1850a8;
26
+ --color-dtour-highlight: #000000;
27
+ }
28
+
29
+ /* --- Animation easing utility for tw-animate-css --- */
30
+
31
+ @utility animate-ease-out {
32
+ animation-timing-function: ease-out;
33
+ }
34
+
35
+ @media (prefers-reduced-motion: reduce) {
36
+ *,
37
+ *::before,
38
+ *::after {
39
+ animation-duration: 0.01ms;
40
+ animation-delay: 0ms;
41
+ animation-iteration-count: 1;
42
+ transition-duration: 0.01ms;
43
+ }
44
+ }
package/src/views.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Create default "little tour" view matrices that cycle through consecutive
3
+ * dimension pairs: (d0,d1), (d1,d2), ..., wrapping back to the start.
4
+ *
5
+ * Each view is a p×2 column-major Float32Array:
6
+ * [x0, x1, ..., xp-1, y0, y1, ..., yp-1]
7
+ *
8
+ * When `activeIndices` is provided, only those dimensions get non-zero
9
+ * basis weights — inactive dimensions contribute zero to the projection.
10
+ *
11
+ * @param dims - total number of dimensions (p)
12
+ * @param count - number of views to generate (defaults to active dim count)
13
+ * @param activeIndices - sorted array of active dimension indices (defaults to all)
14
+ * @returns array of view matrices
15
+ */
16
+ export const createDefaultViews = (
17
+ dims: number,
18
+ count?: number,
19
+ activeIndices?: number[],
20
+ ): Float32Array[] => {
21
+ const indices = activeIndices ?? Array.from({ length: dims }, (_, i) => i);
22
+ const activeDims = indices.length;
23
+ const n = count ?? activeDims;
24
+ const views: Float32Array[] = [];
25
+ for (let i = 0; i < n; i++) {
26
+ const basis = new Float32Array(dims * 2);
27
+ const idx = Math.floor((i / n) * activeDims);
28
+ basis[indices[idx]!] = 1; // active dim → x
29
+ basis[dims + indices[(idx + 1) % activeDims]!] = 1; // next active dim → y
30
+ views.push(basis);
31
+ }
32
+ return views;
33
+ };
34
+
35
+ /**
36
+ * Create tour views from PCA eigenvectors.
37
+ * Cycles through consecutive PC pairs: [PC1,PC2], [PC2,PC3], ..., wrapping.
38
+ *
39
+ * Each eigenvector becomes a column of the p×2 basis matrix. Eigenvectors
40
+ * are assumed to be in the normalized space matching the projection shader.
41
+ *
42
+ * @param eigenvectors - sorted by descending eigenvalue, each of length pcaDims
43
+ * @param totalDims - total number of dimensions in the dataset (p)
44
+ * @param pcaDims - number of PCA dimensions (may be < totalDims if capped)
45
+ * @param count - number of views to generate (defaults to number of PCs)
46
+ */
47
+ export const createPCAViews = (
48
+ eigenvectors: Float32Array[],
49
+ totalDims: number,
50
+ pcaDims: number,
51
+ count?: number,
52
+ ): Float32Array[] => {
53
+ const numPCs = eigenvectors.length;
54
+ const n = count ?? numPCs;
55
+ const views: Float32Array[] = [];
56
+
57
+ for (let i = 0; i < n; i++) {
58
+ const basis = new Float32Array(totalDims * 2);
59
+ const pcX = i % numPCs;
60
+ const pcY = (i + 1) % numPCs;
61
+
62
+ const evX = eigenvectors[pcX]!;
63
+ for (let d = 0; d < pcaDims; d++) {
64
+ basis[d] = evX[d]!;
65
+ }
66
+
67
+ const evY = eigenvectors[pcY]!;
68
+ for (let d = 0; d < pcaDims; d++) {
69
+ basis[totalDims + d] = evY[d]!;
70
+ }
71
+
72
+ views.push(basis);
73
+ }
74
+
75
+ return views;
76
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "rootDir": "src",
6
+ "outDir": "dist",
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "types": ["vite/client"]
10
+ },
11
+ "include": ["src"]
12
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { resolve } from 'node:path';
2
+ import tailwindcss from '@tailwindcss/vite';
3
+ import react from '@vitejs/plugin-react';
4
+ import { defineConfig } from 'vite';
5
+
6
+ export default defineConfig({
7
+ plugins: [tailwindcss(), react()],
8
+ build: {
9
+ lib: {
10
+ entry: resolve(__dirname, 'src/index.ts'),
11
+ name: 'DtourViewer',
12
+ fileName: 'viewer',
13
+ formats: ['es'],
14
+ cssFileName: 'viewer',
15
+ },
16
+ rolldownOptions: {
17
+ // Peer dependencies — consumers provide these
18
+ external: ['react', 'react-dom', 'react/jsx-runtime', 'jotai'],
19
+ },
20
+ },
21
+ });