@uoa-css-lab/duckscatter 1.3.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/.github/dependabot.yml +42 -0
- package/.github/workflows/ci.yaml +111 -0
- package/.github/workflows/release.yml +55 -0
- package/.prettierrc +11 -0
- package/LICENSE +22 -0
- package/README.md +250 -0
- package/dist/data/data-layer.d.ts +169 -0
- package/dist/data/data-layer.js +402 -0
- package/dist/data/index.d.ts +2 -0
- package/dist/data/index.js +2 -0
- package/dist/data/repository.d.ts +48 -0
- package/dist/data/repository.js +109 -0
- package/dist/diagnostics.d.ts +27 -0
- package/dist/diagnostics.js +71 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +58 -0
- package/dist/event-emitter.d.ts +62 -0
- package/dist/event-emitter.js +82 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +13 -0
- package/dist/renderer/gpu-layer.d.ts +204 -0
- package/dist/renderer/gpu-layer.js +611 -0
- package/dist/renderer/index.d.ts +3 -0
- package/dist/renderer/index.js +3 -0
- package/dist/renderer/shaders.d.ts +13 -0
- package/dist/renderer/shaders.js +216 -0
- package/dist/renderer/webgpu-context.d.ts +20 -0
- package/dist/renderer/webgpu-context.js +88 -0
- package/dist/scatter-plot.d.ts +210 -0
- package/dist/scatter-plot.js +450 -0
- package/dist/types.d.ts +171 -0
- package/dist/types.js +1 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/label-layer.d.ts +176 -0
- package/dist/ui/label-layer.js +488 -0
- package/docs/image.png +0 -0
- package/eslint.config.js +72 -0
- package/examples/next/README.md +36 -0
- package/examples/next/app/components/ColorExpressionInput.tsx +41 -0
- package/examples/next/app/components/ControlPanel.tsx +30 -0
- package/examples/next/app/components/HoverControlPanel.tsx +69 -0
- package/examples/next/app/components/HoverInfoDisplay.tsx +40 -0
- package/examples/next/app/components/LabelFilterInput.tsx +46 -0
- package/examples/next/app/components/LabelList.tsx +106 -0
- package/examples/next/app/components/PointAlphaSlider.tsx +21 -0
- package/examples/next/app/components/PointLimitSlider.tsx +23 -0
- package/examples/next/app/components/PointList.tsx +105 -0
- package/examples/next/app/components/PointSizeScaleSlider.tsx +22 -0
- package/examples/next/app/components/ScatterPlotCanvas.tsx +150 -0
- package/examples/next/app/components/SearchBox.tsx +46 -0
- package/examples/next/app/components/Slider.tsx +76 -0
- package/examples/next/app/components/StatsDisplay.tsx +15 -0
- package/examples/next/app/components/TimeFilterSlider.tsx +169 -0
- package/examples/next/app/context/ScatterPlotContext.tsx +402 -0
- package/examples/next/app/favicon.ico +0 -0
- package/examples/next/app/globals.css +23 -0
- package/examples/next/app/layout.tsx +35 -0
- package/examples/next/app/page.tsx +15 -0
- package/examples/next/eslint.config.mjs +18 -0
- package/examples/next/next.config.ts +7 -0
- package/examples/next/package-lock.json +6572 -0
- package/examples/next/package.json +27 -0
- package/examples/next/postcss.config.mjs +7 -0
- package/examples/next/scripts/generate_labels.py +167 -0
- package/examples/next/tsconfig.json +34 -0
- package/package.json +43 -0
- package/src/data/data-layer.ts +515 -0
- package/src/data/index.ts +2 -0
- package/src/data/repository.ts +146 -0
- package/src/diagnostics.ts +108 -0
- package/src/errors.ts +69 -0
- package/src/event-emitter.ts +88 -0
- package/src/index.ts +40 -0
- package/src/renderer/gpu-layer.ts +757 -0
- package/src/renderer/index.ts +3 -0
- package/src/renderer/shaders.ts +219 -0
- package/src/renderer/webgpu-context.ts +98 -0
- package/src/scatter-plot.ts +533 -0
- package/src/types.ts +218 -0
- package/src/ui/index.ts +1 -0
- package/src/ui/label-layer.ts +648 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useState,
|
|
7
|
+
useCallback,
|
|
8
|
+
useRef,
|
|
9
|
+
useEffect,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import type { ScatterPlot, WhereCondition, Label, PointId, LabelIdentifier, GpuWhereCondition } from '@uoa-css-lab/duckscatter';
|
|
13
|
+
|
|
14
|
+
interface ScatterPlotState {
|
|
15
|
+
isInitialized: boolean;
|
|
16
|
+
isLoading: boolean;
|
|
17
|
+
error: string | null;
|
|
18
|
+
hoveredPoint: { row: unknown[]; columns: string[] } | null;
|
|
19
|
+
hoveredLabel: Label | null;
|
|
20
|
+
pointCount: number | null;
|
|
21
|
+
/** created_atカラムの範囲(min/max) */
|
|
22
|
+
timeRange: { min: number; max: number } | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PointListItem {
|
|
26
|
+
id: string | number;
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ScatterPlotContextValue {
|
|
32
|
+
plot: ScatterPlot | null;
|
|
33
|
+
state: ScatterPlotState;
|
|
34
|
+
initializePlot: (canvas: HTMLCanvasElement) => Promise<void>;
|
|
35
|
+
updateSize: (sizeSql: string) => Promise<void>;
|
|
36
|
+
updateColor: (colorSql: string) => Promise<void>;
|
|
37
|
+
updateSearch: (searchText: string) => Promise<void>;
|
|
38
|
+
updatePointLimit: (limit: number) => Promise<void>;
|
|
39
|
+
updateLabelFilter: (searchText: string) => void;
|
|
40
|
+
/** GPU側で時間範囲フィルターを適用 */
|
|
41
|
+
updateTimeFilter: (min: number | null, max: number | null) => Promise<void>;
|
|
42
|
+
/** グローバル透明度を設定 */
|
|
43
|
+
updatePointAlpha: (alpha: number) => void;
|
|
44
|
+
/** グローバルサイズスケールを設定 */
|
|
45
|
+
updatePointSizeScale: (scale: number) => void;
|
|
46
|
+
// Hover control
|
|
47
|
+
setPointHover: (pointId: PointId) => Promise<boolean>;
|
|
48
|
+
setLabelHover: (identifier: LabelIdentifier) => boolean;
|
|
49
|
+
clearAllHover: () => void;
|
|
50
|
+
// List data
|
|
51
|
+
fetchPoints: (page: number, pageSize: number) => Promise<PointListItem[]>;
|
|
52
|
+
getLabels: () => Label[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const ScatterPlotContext = createContext<ScatterPlotContextValue | null>(null);
|
|
56
|
+
|
|
57
|
+
export function ScatterPlotProvider({ children }: { children: ReactNode }) {
|
|
58
|
+
const plotRef = useRef<ScatterPlot | null>(null);
|
|
59
|
+
const [state, setState] = useState<ScatterPlotState>({
|
|
60
|
+
isInitialized: false,
|
|
61
|
+
isLoading: false,
|
|
62
|
+
error: null,
|
|
63
|
+
hoveredPoint: null,
|
|
64
|
+
hoveredLabel: null,
|
|
65
|
+
pointCount: null,
|
|
66
|
+
timeRange: null,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const filtersRef = useRef<{
|
|
70
|
+
searchText: string;
|
|
71
|
+
sizeSql: string;
|
|
72
|
+
colorSql: string;
|
|
73
|
+
visiblePointLimit: number;
|
|
74
|
+
timeRangeFilter: { min: number | null; max: number | null };
|
|
75
|
+
}>({
|
|
76
|
+
searchText: '',
|
|
77
|
+
sizeSql: '5',
|
|
78
|
+
colorSql: '(CAST(255 AS BIGINT) * 16777216 + 59 * 65536 + 130 * 256 + 246 - CASE WHEN (CAST(255 AS BIGINT) * 16777216 + 59 * 65536 + 130 * 256 + 246) > 2147483647 THEN 4294967296 ELSE 0 END)::INTEGER',
|
|
79
|
+
visiblePointLimit: 100000,
|
|
80
|
+
timeRangeFilter: { min: null, max: null },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const buildWhereConditions = useCallback((): WhereCondition[] => {
|
|
84
|
+
const conditions: WhereCondition[] = [];
|
|
85
|
+
if (filtersRef.current.searchText) {
|
|
86
|
+
conditions.push({
|
|
87
|
+
type: 'string',
|
|
88
|
+
column: 'token',
|
|
89
|
+
operator: 'contains',
|
|
90
|
+
value: filtersRef.current.searchText,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return conditions;
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
const initializePlot = useCallback(
|
|
97
|
+
async (canvas: HTMLCanvasElement) => {
|
|
98
|
+
const { ScatterPlot } = await import('@uoa-css-lab/duckscatter');
|
|
99
|
+
|
|
100
|
+
setState((s) => ({ ...s, isLoading: true, error: null }));
|
|
101
|
+
|
|
102
|
+
const plot = new ScatterPlot({
|
|
103
|
+
canvas,
|
|
104
|
+
dataUrl: '/output.parquet',
|
|
105
|
+
data: {
|
|
106
|
+
idColumn: '__index_level_0__',
|
|
107
|
+
sizeSql: filtersRef.current.sizeSql,
|
|
108
|
+
colorSql: filtersRef.current.colorSql,
|
|
109
|
+
visiblePointLimit: filtersRef.current.visiblePointLimit,
|
|
110
|
+
gpuFilterColumns: ['created_at'],
|
|
111
|
+
},
|
|
112
|
+
gpu: {
|
|
113
|
+
backgroundColor: { r: 0.85, g: 0.85, b: 0.85, a: 1.0 },
|
|
114
|
+
},
|
|
115
|
+
labels: {
|
|
116
|
+
url: 'label.geojson',
|
|
117
|
+
onClick: (label) => {
|
|
118
|
+
console.log('Label clicked:', label);
|
|
119
|
+
},
|
|
120
|
+
hoverOutlineOptions: {
|
|
121
|
+
enabled: true,
|
|
122
|
+
color: '#ffffff',
|
|
123
|
+
width: 2,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
interaction: {
|
|
127
|
+
onPointHover: (data) => {
|
|
128
|
+
setState((s) => ({ ...s, hoveredPoint: data }));
|
|
129
|
+
},
|
|
130
|
+
onLabelHover: (label) => {
|
|
131
|
+
setState((s) => ({ ...s, hoveredLabel: label }));
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
plot.on('error', (error) => {
|
|
137
|
+
setState((s) => ({ ...s, error: error.message }));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await plot.initialize();
|
|
142
|
+
plotRef.current = plot;
|
|
143
|
+
plot.render();
|
|
144
|
+
|
|
145
|
+
// Get point count
|
|
146
|
+
const result = await plot.runQuery('SELECT COUNT(*) as count FROM parquet_data');
|
|
147
|
+
let pointCount = 0;
|
|
148
|
+
if (result && result.rowCount > 0) {
|
|
149
|
+
const countCol = result.columnData.get('count');
|
|
150
|
+
if (countCol) {
|
|
151
|
+
pointCount = Number(countCol[0]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Get time range (created_at min/max)
|
|
156
|
+
let timeRange: { min: number; max: number } | null = null;
|
|
157
|
+
const timeResult = await plot.runQuery(
|
|
158
|
+
'SELECT MIN(created_at) as min_time, MAX(created_at) as max_time FROM parquet_data'
|
|
159
|
+
);
|
|
160
|
+
if (timeResult && timeResult.rowCount > 0) {
|
|
161
|
+
const minCol = timeResult.columnData.get('min_time');
|
|
162
|
+
const maxCol = timeResult.columnData.get('max_time');
|
|
163
|
+
if (minCol && maxCol) {
|
|
164
|
+
const minVal = Number(minCol.get(0));
|
|
165
|
+
const maxVal = Number(maxCol.get(0));
|
|
166
|
+
if (!isNaN(minVal) && !isNaN(maxVal)) {
|
|
167
|
+
timeRange = { min: minVal, max: maxVal };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
setState((s) => ({ ...s, isInitialized: true, isLoading: false, pointCount, timeRange }));
|
|
173
|
+
} catch (e) {
|
|
174
|
+
setState((s) => ({
|
|
175
|
+
...s,
|
|
176
|
+
isLoading: false,
|
|
177
|
+
error: e instanceof Error ? e.message : 'Failed to initialize',
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
[]
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const updateSize = useCallback(
|
|
185
|
+
async (sizeSql: string) => {
|
|
186
|
+
if (!plotRef.current) return;
|
|
187
|
+
filtersRef.current.sizeSql = sizeSql;
|
|
188
|
+
await plotRef.current.update({
|
|
189
|
+
data: {
|
|
190
|
+
idColumn: '__index_level_0__',
|
|
191
|
+
sizeSql,
|
|
192
|
+
colorSql: filtersRef.current.colorSql,
|
|
193
|
+
whereConditions: buildWhereConditions(),
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
plotRef.current.render();
|
|
197
|
+
},
|
|
198
|
+
[buildWhereConditions]
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const updateColor = useCallback(
|
|
202
|
+
async (colorSql: string) => {
|
|
203
|
+
if (!plotRef.current) return;
|
|
204
|
+
filtersRef.current.colorSql = colorSql;
|
|
205
|
+
await plotRef.current.update({
|
|
206
|
+
data: {
|
|
207
|
+
idColumn: '__index_level_0__',
|
|
208
|
+
sizeSql: filtersRef.current.sizeSql,
|
|
209
|
+
colorSql,
|
|
210
|
+
whereConditions: buildWhereConditions(),
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
plotRef.current.render();
|
|
214
|
+
},
|
|
215
|
+
[buildWhereConditions]
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const updateSearch = useCallback(
|
|
219
|
+
async (searchText: string) => {
|
|
220
|
+
if (!plotRef.current) return;
|
|
221
|
+
filtersRef.current.searchText = searchText;
|
|
222
|
+
await plotRef.current.update({
|
|
223
|
+
data: {
|
|
224
|
+
idColumn: '__index_level_0__',
|
|
225
|
+
sizeSql: filtersRef.current.sizeSql,
|
|
226
|
+
colorSql: filtersRef.current.colorSql,
|
|
227
|
+
whereConditions: buildWhereConditions(),
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
plotRef.current.render();
|
|
231
|
+
},
|
|
232
|
+
[buildWhereConditions]
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const updatePointLimit = useCallback(
|
|
236
|
+
async (limit: number) => {
|
|
237
|
+
if (!plotRef.current) return;
|
|
238
|
+
filtersRef.current.visiblePointLimit = limit;
|
|
239
|
+
await plotRef.current.update({
|
|
240
|
+
data: {
|
|
241
|
+
idColumn: '__index_level_0__',
|
|
242
|
+
sizeSql: filtersRef.current.sizeSql,
|
|
243
|
+
colorSql: filtersRef.current.colorSql,
|
|
244
|
+
visiblePointLimit: limit,
|
|
245
|
+
whereConditions: buildWhereConditions(),
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
plotRef.current.render();
|
|
249
|
+
},
|
|
250
|
+
[buildWhereConditions]
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Hover control methods
|
|
254
|
+
const setPointHover = useCallback(async (pointId: PointId): Promise<boolean> => {
|
|
255
|
+
if (!plotRef.current) return false;
|
|
256
|
+
return await plotRef.current.setPointHover(pointId);
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
const setLabelHover = useCallback((identifier: LabelIdentifier): boolean => {
|
|
260
|
+
if (!plotRef.current) return false;
|
|
261
|
+
return plotRef.current.setLabelHover(identifier);
|
|
262
|
+
}, []);
|
|
263
|
+
|
|
264
|
+
const clearAllHover = useCallback(() => {
|
|
265
|
+
if (!plotRef.current) return;
|
|
266
|
+
plotRef.current.clearAllHover();
|
|
267
|
+
}, []);
|
|
268
|
+
|
|
269
|
+
const updateLabelFilter = useCallback((searchText: string) => {
|
|
270
|
+
if (!plotRef.current) return;
|
|
271
|
+
if (searchText.trim() === '') {
|
|
272
|
+
plotRef.current.update({
|
|
273
|
+
labels: {
|
|
274
|
+
filterLambda: undefined,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
} else {
|
|
278
|
+
const lowerSearch = searchText.toLowerCase();
|
|
279
|
+
plotRef.current.update({
|
|
280
|
+
labels: {
|
|
281
|
+
filterLambda: (properties) => {
|
|
282
|
+
const label = (properties.cluster_label as string) || '';
|
|
283
|
+
return label.toLowerCase().includes(lowerSearch);
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
plotRef.current.render();
|
|
289
|
+
}, []);
|
|
290
|
+
|
|
291
|
+
const updateTimeFilter = useCallback(
|
|
292
|
+
async (min: number | null, max: number | null) => {
|
|
293
|
+
if (!plotRef.current) return;
|
|
294
|
+
filtersRef.current.timeRangeFilter = { min, max };
|
|
295
|
+
|
|
296
|
+
// GPU側のフィルター条件を構築
|
|
297
|
+
const gpuConditions: GpuWhereCondition[] = [];
|
|
298
|
+
if (min !== null || max !== null) {
|
|
299
|
+
gpuConditions.push({
|
|
300
|
+
column: 'created_at',
|
|
301
|
+
min: min ?? undefined,
|
|
302
|
+
max: max ?? undefined,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await plotRef.current.update({
|
|
307
|
+
data: {
|
|
308
|
+
idColumn: '__index_level_0__',
|
|
309
|
+
sizeSql: filtersRef.current.sizeSql,
|
|
310
|
+
colorSql: filtersRef.current.colorSql,
|
|
311
|
+
gpuWhereConditions: gpuConditions,
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
plotRef.current.render();
|
|
315
|
+
},
|
|
316
|
+
[]
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const updatePointAlpha = useCallback((alpha: number) => {
|
|
320
|
+
if (!plotRef.current) return;
|
|
321
|
+
plotRef.current.setPointAlpha(alpha);
|
|
322
|
+
}, []);
|
|
323
|
+
|
|
324
|
+
const updatePointSizeScale = useCallback((scale: number) => {
|
|
325
|
+
if (!plotRef.current) return;
|
|
326
|
+
plotRef.current.setPointSizeScale(scale);
|
|
327
|
+
}, []);
|
|
328
|
+
|
|
329
|
+
// List data methods
|
|
330
|
+
const fetchPoints = useCallback(
|
|
331
|
+
async (page: number, pageSize: number): Promise<PointListItem[]> => {
|
|
332
|
+
if (!plotRef.current) return [];
|
|
333
|
+
const offset = page * pageSize;
|
|
334
|
+
const result = await plotRef.current.runQuery(
|
|
335
|
+
`SELECT __index_level_0__ as id, x, y FROM parquet_data ORDER BY __index_level_0__ LIMIT ${pageSize} OFFSET ${offset}`
|
|
336
|
+
);
|
|
337
|
+
if (!result || result.rowCount === 0) return [];
|
|
338
|
+
|
|
339
|
+
const idCol = result.columnData.get('id');
|
|
340
|
+
const xCol = result.columnData.get('x');
|
|
341
|
+
const yCol = result.columnData.get('y');
|
|
342
|
+
|
|
343
|
+
const points: PointListItem[] = [];
|
|
344
|
+
for (let i = 0; i < result.rowCount; i++) {
|
|
345
|
+
points.push({
|
|
346
|
+
id: idCol?.get(i),
|
|
347
|
+
x: xCol?.get(i),
|
|
348
|
+
y: yCol?.get(i),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return points;
|
|
352
|
+
},
|
|
353
|
+
[]
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const getLabels = useCallback((): Label[] => {
|
|
357
|
+
if (!plotRef.current) return [];
|
|
358
|
+
return plotRef.current.getLabels();
|
|
359
|
+
}, []);
|
|
360
|
+
|
|
361
|
+
useEffect(() => {
|
|
362
|
+
return () => {
|
|
363
|
+
if (plotRef.current) {
|
|
364
|
+
plotRef.current.destroy();
|
|
365
|
+
plotRef.current = null;
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}, []);
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<ScatterPlotContext.Provider
|
|
372
|
+
value={{
|
|
373
|
+
plot: plotRef.current,
|
|
374
|
+
state,
|
|
375
|
+
initializePlot,
|
|
376
|
+
updateSize,
|
|
377
|
+
updateColor,
|
|
378
|
+
updateSearch,
|
|
379
|
+
updatePointLimit,
|
|
380
|
+
updateLabelFilter,
|
|
381
|
+
updateTimeFilter,
|
|
382
|
+
updatePointAlpha,
|
|
383
|
+
updatePointSizeScale,
|
|
384
|
+
setPointHover,
|
|
385
|
+
setLabelHover,
|
|
386
|
+
clearAllHover,
|
|
387
|
+
fetchPoints,
|
|
388
|
+
getLabels,
|
|
389
|
+
}}
|
|
390
|
+
>
|
|
391
|
+
{children}
|
|
392
|
+
</ScatterPlotContext.Provider>
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function useScatterPlot() {
|
|
397
|
+
const context = useContext(ScatterPlotContext);
|
|
398
|
+
if (!context) {
|
|
399
|
+
throw new Error('useScatterPlot must be used within ScatterPlotProvider');
|
|
400
|
+
}
|
|
401
|
+
return context;
|
|
402
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--background: #ffffff;
|
|
5
|
+
--foreground: #171717;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@theme inline {
|
|
9
|
+
--color-background: var(--background);
|
|
10
|
+
--color-foreground: var(--foreground);
|
|
11
|
+
--font-sans: var(--font-geist-sans);
|
|
12
|
+
--font-mono: var(--font-geist-mono);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
html,
|
|
17
|
+
body {
|
|
18
|
+
height: 100%;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
background: var(--background);
|
|
21
|
+
color: var(--foreground);
|
|
22
|
+
font-family: Arial, Helvetica, sans-serif;
|
|
23
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
import { ScatterPlotProvider } from "./context/ScatterPlotContext";
|
|
5
|
+
|
|
6
|
+
const geistSans = Geist({
|
|
7
|
+
variable: "--font-geist-sans",
|
|
8
|
+
subsets: ["latin"],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const geistMono = Geist_Mono({
|
|
12
|
+
variable: "--font-geist-mono",
|
|
13
|
+
subsets: ["latin"],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const metadata: Metadata = {
|
|
17
|
+
title: "DuckScatter Example",
|
|
18
|
+
description: "Next.js example for duckscatter library",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default function RootLayout({
|
|
22
|
+
children,
|
|
23
|
+
}: Readonly<{
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
}>) {
|
|
26
|
+
return (
|
|
27
|
+
<html lang="en">
|
|
28
|
+
<body
|
|
29
|
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
30
|
+
>
|
|
31
|
+
<ScatterPlotProvider>{children}</ScatterPlotProvider>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ScatterPlotCanvas } from './components/ScatterPlotCanvas';
|
|
4
|
+
import { ControlPanel } from './components/ControlPanel';
|
|
5
|
+
|
|
6
|
+
export default function Home() {
|
|
7
|
+
return (
|
|
8
|
+
<div className="h-screen w-screen flex overflow-hidden">
|
|
9
|
+
<ControlPanel />
|
|
10
|
+
<main className="flex-1 relative">
|
|
11
|
+
<ScatterPlotCanvas />
|
|
12
|
+
</main>
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig, globalIgnores } from "eslint/config";
|
|
2
|
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
3
|
+
import nextTs from "eslint-config-next/typescript";
|
|
4
|
+
|
|
5
|
+
const eslintConfig = defineConfig([
|
|
6
|
+
...nextVitals,
|
|
7
|
+
...nextTs,
|
|
8
|
+
// Override default ignores of eslint-config-next.
|
|
9
|
+
globalIgnores([
|
|
10
|
+
// Default ignores of eslint-config-next:
|
|
11
|
+
".next/**",
|
|
12
|
+
"out/**",
|
|
13
|
+
"build/**",
|
|
14
|
+
"next-env.d.ts",
|
|
15
|
+
]),
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
export default eslintConfig;
|