@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,30 @@
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 declare const createDefaultViews: (dims: number, count?: number, activeIndices?: number[]) => Float32Array[];
17
+ /**
18
+ * Create tour views from PCA eigenvectors.
19
+ * Cycles through consecutive PC pairs: [PC1,PC2], [PC2,PC3], ..., wrapping.
20
+ *
21
+ * Each eigenvector becomes a column of the p×2 basis matrix. Eigenvectors
22
+ * are assumed to be in the normalized space matching the projection shader.
23
+ *
24
+ * @param eigenvectors - sorted by descending eigenvalue, each of length pcaDims
25
+ * @param totalDims - total number of dimensions in the dataset (p)
26
+ * @param pcaDims - number of PCA dimensions (may be < totalDims if capped)
27
+ * @param count - number of views to generate (defaults to number of PCs)
28
+ */
29
+ export declare const createPCAViews: (eigenvectors: Float32Array[], totalDims: number, pcaDims: number, count?: number) => Float32Array[];
30
+ //# sourceMappingURL=views.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"views.d.ts","sourceRoot":"","sources":["../src/views.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,kBAAkB,GAC7B,MAAM,MAAM,EACZ,QAAQ,MAAM,EACd,gBAAgB,MAAM,EAAE,KACvB,YAAY,EAad,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,cAAc,GACzB,cAAc,YAAY,EAAE,EAC5B,WAAW,MAAM,EACjB,SAAS,MAAM,EACf,QAAQ,MAAM,KACb,YAAY,EAwBd,CAAC"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@dtour/viewer",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/viewer.js",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./dist/viewer.js",
9
+ "types": "./dist/index.d.ts"
10
+ },
11
+ "./dist/viewer.css": "./dist/viewer.css"
12
+ },
13
+ "scripts": {
14
+ "build": "vite build && tsc --emitDeclarationOnly --declaration --outDir dist",
15
+ "dev": "vite build --watch"
16
+ },
17
+ "dependencies": {
18
+ "@dtour/scatter": "workspace:*",
19
+ "@phosphor-icons/react": "^2.1.10",
20
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
21
+ "@radix-ui/react-popover": "^1.1.15",
22
+ "@radix-ui/react-select": "^2.2.6",
23
+ "@radix-ui/react-slider": "^1.3.6",
24
+ "@radix-ui/react-slot": "catalog:",
25
+ "@tailwindcss/vite": "catalog:",
26
+ "@uwdata/flechette": "catalog:",
27
+ "class-variance-authority": "catalog:",
28
+ "clsx": "catalog:",
29
+ "jotai": "catalog:",
30
+ "radix-ui": "^1.4.3",
31
+ "react": "catalog:",
32
+ "react-dom": "catalog:",
33
+ "tailwind-merge": "catalog:",
34
+ "tailwindcss": "catalog:",
35
+ "tw-animate-css": "^1.4.0",
36
+ "zod": "catalog:"
37
+ },
38
+ "devDependencies": {
39
+ "@types/react": "catalog:",
40
+ "@types/react-dom": "catalog:",
41
+ "@vitejs/plugin-react": "catalog:",
42
+ "vite": "catalog:"
43
+ },
44
+ "peerDependencies": {
45
+ "react": ">=18",
46
+ "react-dom": ">=18"
47
+ }
48
+ }
package/src/Dtour.tsx ADDED
@@ -0,0 +1,300 @@
1
+ import type { ScatterInstance, ScatterStatus } from '@dtour/scatter';
2
+ import { bitPackIndices } from '@dtour/scatter';
3
+ import { Provider, createStore, useAtomValue, useSetAtom } from 'jotai';
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
+ import { DtourViewer } from './DtourViewer.tsx';
6
+ import { ColorLegend } from './components/ColorLegend.tsx';
7
+ import { DtourToolbar } from './components/DtourToolbar.tsx';
8
+ import { useModeCycling } from './hooks/useModeCycling.ts';
9
+ import { useSystemTheme } from './hooks/useSystemTheme.ts';
10
+ import { PortalContainerContext } from './portal-container.tsx';
11
+ import type { RadialTrackConfig } from './radial-chart/types.ts';
12
+ import type { DtourSpec } from './spec.ts';
13
+ import {
14
+ backgroundColorAtom,
15
+ colorMapAtom,
16
+ legendSelectionAtom,
17
+ legendVisibleAtom,
18
+ metadataAtom,
19
+ pointColorAtom,
20
+ resolvedThemeAtom,
21
+ viewModeAtom,
22
+ } from './state/atoms.ts';
23
+ import { initStoreFromSpec, useSpecSync } from './state/spec-sync.ts';
24
+
25
+ export type DtourHandle = {
26
+ /** Select points by index array or bit-packed mask. */
27
+ select: (
28
+ indicesOrMask: number[] | Int32Array | Uint32Array,
29
+ opts?: { isBitPacked?: boolean },
30
+ ) => void;
31
+ /** Clear the current selection. */
32
+ clearSelection: () => void;
33
+ };
34
+
35
+ export type DtourProps = {
36
+ /** Arrow IPC or Parquet ArrayBuffer. Ownership is transferred on load. */
37
+ data?: ArrayBuffer;
38
+ /** Tour keyframe views (p×2 column-major). Auto-generated if omitted. */
39
+ views?: Float32Array[];
40
+ /** Arrow IPC ArrayBuffer with per-view quality metrics (columns = metrics, rows = views). */
41
+ metrics?: ArrayBuffer;
42
+ /** Track configuration for radial bar charts. When omitted, all metrics are shown with defaults. */
43
+ metricTracks?: RadialTrackConfig[];
44
+ /** Global bar width override for radial charts ('full' or px). */
45
+ metricBarWidth?: 'full' | number;
46
+ /** Partial spec controlling component state. Omitted fields use defaults. */
47
+ spec?: DtourSpec;
48
+ /** Fires when internal state changes (debounced ~250ms). Full resolved spec. */
49
+ onSpecChange?: (spec: Required<DtourSpec>) => void;
50
+ /** Called on every status event from the renderer. */
51
+ onStatus?: (status: ScatterStatus) => void;
52
+ /** Hide the toolbar. Default false. */
53
+ hideToolbar?: boolean;
54
+ /** Called when the user requests loading new data via the toolbar file picker. */
55
+ onLoadData?: (data: ArrayBuffer, fileName: string) => void;
56
+ /** Fires when legend selection changes for a categorical color column. Reports selected label names or empty array when cleared. */
57
+ onSelectionChange?: (labels: string[]) => void;
58
+ /** Per-label color map. Values are hex strings or theme-aware {light, dark} objects. */
59
+ colorMap?: Record<string, string | { light: string; dark: string }>;
60
+ /** Element to portal Radix popups into (for Shadow DOM isolation). When omitted, portals render into document.body as usual. */
61
+ portalContainer?: HTMLElement;
62
+ /** Called when the viewer is ready with an API handle for programmatic control. */
63
+ onReady?: (api: DtourHandle) => void;
64
+ };
65
+
66
+ export const Dtour = ({
67
+ data,
68
+ views,
69
+ metrics,
70
+ metricTracks,
71
+ metricBarWidth,
72
+ spec,
73
+ onSpecChange,
74
+ onStatus,
75
+ hideToolbar = false,
76
+ onLoadData,
77
+ onSelectionChange,
78
+ colorMap,
79
+ portalContainer,
80
+ onReady,
81
+ }: DtourProps) => {
82
+ // Each Dtour instance gets its own isolated jotai store.
83
+ // Eagerly apply initial spec values so there's no flash of defaults.
84
+ // biome-ignore lint/correctness/useExhaustiveDependencies: store created once on mount
85
+ const store = useMemo(() => {
86
+ const s = createStore();
87
+ initStoreFromSpec(s, spec);
88
+ return s;
89
+ }, []);
90
+
91
+ return (
92
+ <PortalContainerContext.Provider value={portalContainer}>
93
+ <Provider store={store}>
94
+ <DtourInner
95
+ data={data}
96
+ views={views}
97
+ metrics={metrics}
98
+ metricTracks={metricTracks}
99
+ metricBarWidth={metricBarWidth}
100
+ spec={spec}
101
+ onSpecChange={onSpecChange}
102
+ onStatus={onStatus}
103
+ hideToolbar={hideToolbar}
104
+ onLoadData={onLoadData}
105
+ onSelectionChange={onSelectionChange}
106
+ colorMap={colorMap}
107
+ onReady={onReady}
108
+ />
109
+ </Provider>
110
+ </PortalContainerContext.Provider>
111
+ );
112
+ };
113
+
114
+ /** Inner component that lives inside the Provider so hooks bind to the store. */
115
+ const DtourInner = ({
116
+ data,
117
+ views,
118
+ metrics,
119
+ metricTracks,
120
+ metricBarWidth,
121
+ spec,
122
+ onSpecChange,
123
+ onStatus,
124
+ hideToolbar,
125
+ onLoadData,
126
+ onSelectionChange,
127
+ colorMap,
128
+ onReady,
129
+ }: {
130
+ data: ArrayBuffer | undefined;
131
+ views: Float32Array[] | undefined;
132
+ metrics: ArrayBuffer | undefined;
133
+ metricTracks: RadialTrackConfig[] | undefined;
134
+ metricBarWidth: 'full' | number | undefined;
135
+ spec: DtourSpec | undefined;
136
+ onSpecChange: ((spec: Required<DtourSpec>) => void) | undefined;
137
+ onStatus: ((status: ScatterStatus) => void) | undefined;
138
+ hideToolbar: boolean;
139
+ onLoadData: ((data: ArrayBuffer, fileName: string) => void) | undefined;
140
+ onSelectionChange: ((labels: string[]) => void) | undefined;
141
+ colorMap: Record<string, string | { light: string; dark: string }> | undefined;
142
+ onReady: ((api: DtourHandle) => void) | undefined;
143
+ }) => {
144
+ useSpecSync(spec, onSpecChange);
145
+ useModeCycling();
146
+ useSystemTheme();
147
+
148
+ // Sync colorMap prop → atom
149
+ const setColorMap = useSetAtom(colorMapAtom);
150
+ useEffect(() => {
151
+ setColorMap(colorMap ?? null);
152
+ }, [colorMap, setColorMap]);
153
+
154
+ // Sync resolved theme → background color + CSS class
155
+ const resolvedTheme = useAtomValue(resolvedThemeAtom);
156
+ const setBackgroundColor = useSetAtom(backgroundColorAtom);
157
+ useEffect(() => {
158
+ setBackgroundColor(resolvedTheme === 'light' ? [1, 1, 1] : [0, 0, 0]);
159
+ }, [resolvedTheme, setBackgroundColor]);
160
+
161
+ // Forward legend selection changes to the parent as label name strings
162
+ const legendSelection = useAtomValue(legendSelectionAtom);
163
+ const pointColor = useAtomValue(pointColorAtom);
164
+ const metadata = useAtomValue(metadataAtom);
165
+
166
+ useEffect(() => {
167
+ if (!onSelectionChange) return;
168
+
169
+ if (typeof pointColor !== 'string' || !metadata) return;
170
+ if (!metadata.categoricalColumnNames.includes(pointColor)) return;
171
+
172
+ if (!legendSelection || legendSelection.size === 0) {
173
+ onSelectionChange([]);
174
+ return;
175
+ }
176
+
177
+ const allLabels = metadata.categoricalLabels[pointColor] ?? [];
178
+ const selectedLabels = Array.from(legendSelection)
179
+ .map((i) => allLabels[i])
180
+ .filter((l): l is string => l !== undefined);
181
+
182
+ onSelectionChange(selectedLabels.length > 0 ? selectedLabels : []);
183
+ }, [legendSelection, pointColor, metadata, onSelectionChange]);
184
+
185
+ // Track scatter instance for programmatic select API
186
+ const [scatterInstance, setScatterInstance] = useState<ScatterInstance | null>(null);
187
+ const onReadyRef = useRef(onReady);
188
+ onReadyRef.current = onReady;
189
+
190
+ useEffect(() => {
191
+ if (!scatterInstance || !metadata) return;
192
+
193
+ const handle: DtourHandle = {
194
+ select: (indicesOrMask, opts) => {
195
+ if (opts?.isBitPacked && indicesOrMask instanceof Uint32Array) {
196
+ scatterInstance.setSelectionMask(new Uint32Array(indicesOrMask));
197
+ } else {
198
+ const packed = bitPackIndices(indicesOrMask, metadata.rowCount);
199
+ scatterInstance.setSelectionMask(packed);
200
+ }
201
+ },
202
+ clearSelection: () => {
203
+ scatterInstance.clearSelection();
204
+ },
205
+ };
206
+
207
+ onReadyRef.current?.(handle);
208
+ }, [scatterInstance, metadata]);
209
+
210
+ const viewMode = useAtomValue(viewModeAtom);
211
+ const isGrand = viewMode === 'grand';
212
+ const legendVisible = useAtomValue(legendVisibleAtom);
213
+
214
+ // Sidebar width state — remembered across open/close cycles
215
+ const [sidebarWidth, setSidebarWidth] = useState(200);
216
+ const [dragging, setDragging] = useState(false);
217
+ const containerRef = useRef<HTMLDivElement>(null);
218
+
219
+ // Drag-to-resize handler
220
+ const onHandleMouseDown = useCallback(
221
+ (e: React.MouseEvent) => {
222
+ if (!legendVisible) return;
223
+ e.preventDefault();
224
+ setDragging(true);
225
+
226
+ const onMouseMove = (me: MouseEvent) => {
227
+ const container = containerRef.current;
228
+ if (!container) return;
229
+ const rect = container.getBoundingClientRect();
230
+ const maxWidth = rect.width * 0.4;
231
+ const newWidth = Math.min(maxWidth, Math.max(64, rect.right - me.clientX));
232
+ setSidebarWidth(newWidth);
233
+ };
234
+
235
+ const onMouseUp = () => {
236
+ setDragging(false);
237
+ window.removeEventListener('mousemove', onMouseMove);
238
+ window.removeEventListener('mouseup', onMouseUp);
239
+ };
240
+
241
+ window.addEventListener('mousemove', onMouseMove);
242
+ window.addEventListener('mouseup', onMouseUp);
243
+ },
244
+ [legendVisible],
245
+ );
246
+
247
+ const displayWidth = legendVisible ? sidebarWidth : 0;
248
+
249
+ return (
250
+ <div
251
+ ref={containerRef}
252
+ className={`relative w-full h-full overflow-hidden flex ${resolvedTheme === 'light' ? 'dtour-light' : ''}`}
253
+ >
254
+ {/* Canvas panel — grows to fill remaining space */}
255
+ <div className="relative flex-1 min-w-0">
256
+ {/* Toolbar — inside left panel so it shrinks with the legend */}
257
+ <div
258
+ className={`absolute inset-x-0 top-0 z-10 h-10 transition-[transform,opacity] duration-300 ease-out ${
259
+ isGrand ? '-translate-y-full' : 'translate-y-0'
260
+ } ${hideToolbar ? 'opacity-0 pointer-events-none' : 'opacity-100'}`}
261
+ >
262
+ <DtourToolbar onLoadData={onLoadData} />
263
+ </div>
264
+ <div className="absolute inset-0 overflow-hidden">
265
+ <DtourViewer
266
+ data={data}
267
+ views={views}
268
+ metrics={metrics}
269
+ metricTracks={metricTracks}
270
+ metricBarWidth={metricBarWidth}
271
+ onStatus={onStatus}
272
+ toolbarHeight={hideToolbar ? 0 : 40}
273
+ onScatterReady={setScatterInstance}
274
+ />
275
+ </div>
276
+ </div>
277
+ {/* Drag handle */}
278
+ <div
279
+ className={`w-px shrink-0 transition-colors ${
280
+ legendVisible
281
+ ? 'cursor-col-resize bg-dtour-surface hover:bg-dtour-text-muted active:bg-dtour-highlight'
282
+ : 'pointer-events-none'
283
+ }`}
284
+ onMouseDown={onHandleMouseDown}
285
+ />
286
+ {/* Legend sidebar */}
287
+ <div
288
+ className="shrink-0 overflow-hidden"
289
+ style={{
290
+ width: displayWidth,
291
+ transition: dragging ? 'none' : 'width 300ms cubic-bezier(.1,.1,0,1)',
292
+ }}
293
+ >
294
+ <div className="h-full" style={{ width: sidebarWidth }}>
295
+ <ColorLegend />
296
+ </div>
297
+ </div>
298
+ </div>
299
+ );
300
+ };