@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,450 @@
|
|
|
1
|
+
import { DataLayer } from './data/index.js';
|
|
2
|
+
import { GpuLayer } from './renderer/index.js';
|
|
3
|
+
import { LabelLayer } from './ui/index.js';
|
|
4
|
+
import { EventEmitter } from './event-emitter.js';
|
|
5
|
+
import { createError } from './errors.js';
|
|
6
|
+
/**
|
|
7
|
+
* WebGPUを使用して散布図を描画するメインクラス
|
|
8
|
+
*
|
|
9
|
+
* このクラスは3つの異なるレイヤーのファサード/コーディネーターとして機能する:
|
|
10
|
+
* - DataLayer: データ取得とクエリ管理を担当
|
|
11
|
+
* - GpuLayer: WebGPUレンダリングと変換を管理(LODと境界計算もGPU側で実行)
|
|
12
|
+
* - LabelLayer: ラベル用の2Dキャンバスオーバーレイを担当
|
|
13
|
+
*/
|
|
14
|
+
export class ScatterPlot extends EventEmitter {
|
|
15
|
+
/**
|
|
16
|
+
* ScatterPlotインスタンスを作成する
|
|
17
|
+
* @param options 散布図の設定オプション
|
|
18
|
+
*/
|
|
19
|
+
constructor(options) {
|
|
20
|
+
super();
|
|
21
|
+
/** GPUフィルターカラム名→インデックスのマッピング */
|
|
22
|
+
this.gpuFilterColumnMapping = new Map();
|
|
23
|
+
this.dataLayer = new DataLayer({
|
|
24
|
+
sizeSql: options.data.sizeSql,
|
|
25
|
+
colorSql: options.data.colorSql,
|
|
26
|
+
whereConditions: options.data.whereConditions,
|
|
27
|
+
gpuFilterColumns: options.data.gpuFilterColumns,
|
|
28
|
+
idColumn: options.data.idColumn,
|
|
29
|
+
onError: (error) => this.emitError(error),
|
|
30
|
+
onDataChanged: () => this.handleDataChanged(),
|
|
31
|
+
});
|
|
32
|
+
this.gpuLayer = new GpuLayer({
|
|
33
|
+
canvas: options.canvas,
|
|
34
|
+
backgroundColor: options.gpu?.backgroundColor,
|
|
35
|
+
visiblePointLimit: options.data.visiblePointLimit,
|
|
36
|
+
});
|
|
37
|
+
this.labelLayer = new LabelLayer({
|
|
38
|
+
canvas: options.canvas,
|
|
39
|
+
labelFontSize: options.labels?.fontSize,
|
|
40
|
+
filterLambda: options.labels?.filterLambda,
|
|
41
|
+
onLabelClick: options.labels?.onClick,
|
|
42
|
+
onPointHover: (data) => this.handlePointHover(data, options.interaction?.onPointHover),
|
|
43
|
+
onLabelHover: options.interaction?.onLabelHover,
|
|
44
|
+
hoverOutlineOptions: options.labels?.hoverOutlineOptions,
|
|
45
|
+
dataLayer: this.dataLayer,
|
|
46
|
+
});
|
|
47
|
+
this.dataUrl = options.dataUrl;
|
|
48
|
+
this.labelUrl = options.labels?.url;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* WebGPUを初期化し、レンダリングリソースを作成する
|
|
52
|
+
*/
|
|
53
|
+
async initialize() {
|
|
54
|
+
try {
|
|
55
|
+
const allPointsData = await this.dataLayer.initialize(this.dataUrl);
|
|
56
|
+
await this.gpuLayer.initialize(allPointsData);
|
|
57
|
+
const gpuFilterData = await this.dataLayer.loadGpuFilterColumns();
|
|
58
|
+
if (gpuFilterData) {
|
|
59
|
+
this.gpuLayer.uploadFilterColumns(gpuFilterData.data, gpuFilterData.columnCount);
|
|
60
|
+
this.gpuFilterColumnMapping = gpuFilterData.columnMapping;
|
|
61
|
+
}
|
|
62
|
+
this.labelLayer.initialize();
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
const error = this.categorizeInitError(e);
|
|
66
|
+
this.emitError(error);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (this.labelUrl) {
|
|
70
|
+
await this.loadLabelsFromUrl(this.labelUrl);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* データ変更時のハンドラ(sizeSql, colorSql, whereConditions変更時)
|
|
75
|
+
*/
|
|
76
|
+
async handleDataChanged() {
|
|
77
|
+
try {
|
|
78
|
+
const allPointsData = await this.dataLayer.loadAllPoints();
|
|
79
|
+
this.gpuLayer.uploadAllPoints(allPointsData);
|
|
80
|
+
this.render();
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
this.emitError(createError('QUERY_FAILED', 'Failed to reload data after options change', {
|
|
84
|
+
cause: e instanceof Error ? e : undefined,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* URLからラベルデータを読み込み、エラーハンドリングを行う
|
|
90
|
+
* @param url ラベルデータのURL
|
|
91
|
+
*/
|
|
92
|
+
async loadLabelsFromUrl(url) {
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch(url);
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
this.emitError(createError('LABEL_FETCH_FAILED', `Failed to fetch labels: ${response.status} ${response.statusText}`, {
|
|
97
|
+
context: { url, status: response.status },
|
|
98
|
+
}));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const labelData = await response.json();
|
|
102
|
+
this.loadLabels(labelData);
|
|
103
|
+
await this.dataLayer.loadLabelData(labelData);
|
|
104
|
+
}
|
|
105
|
+
catch (e) {
|
|
106
|
+
this.emitError(createError('LABEL_FETCH_FAILED', 'Network error while fetching labels', {
|
|
107
|
+
cause: e instanceof Error ? e : undefined,
|
|
108
|
+
context: { url },
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 初期化エラーをScatterPlotError型に分類する
|
|
114
|
+
* @param e 発生した例外
|
|
115
|
+
* @returns 分類されたScatterPlotErrorオブジェクト
|
|
116
|
+
*/
|
|
117
|
+
categorizeInitError(e) {
|
|
118
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
119
|
+
const cause = e instanceof Error ? e : undefined;
|
|
120
|
+
if (message.includes('WebGPU is not supported')) {
|
|
121
|
+
return createError('WEBGPU_NOT_SUPPORTED', message, { cause });
|
|
122
|
+
}
|
|
123
|
+
if (message.includes('GPU adapter') || message.includes('Failed to get GPU adapter')) {
|
|
124
|
+
return createError('GPU_ADAPTER_NOT_AVAILABLE', message, { cause });
|
|
125
|
+
}
|
|
126
|
+
if (message.includes('GPU device') || message.includes('Failed to get GPU device')) {
|
|
127
|
+
return createError('GPU_DEVICE_FAILED', message, { cause });
|
|
128
|
+
}
|
|
129
|
+
if (message.includes('WebGPU context') || message.includes('Failed to get WebGPU context')) {
|
|
130
|
+
return createError('WEBGPU_CONTEXT_FAILED', message, { cause });
|
|
131
|
+
}
|
|
132
|
+
if (message.includes('Database not initialized') || message.includes('not initialized')) {
|
|
133
|
+
return createError('DATA_LAYER_NOT_INITIALIZED', message, { cause });
|
|
134
|
+
}
|
|
135
|
+
if (message.includes('Parquet') || message.includes('parquet') || message.includes('load')) {
|
|
136
|
+
return createError('PARQUET_LOAD_FAILED', message, { cause });
|
|
137
|
+
}
|
|
138
|
+
return createError('WEBGPU_NOT_SUPPORTED', message, { cause });
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* エラーイベントを発行する。リスナーが登録されていない場合はconsole.warnに出力する
|
|
142
|
+
* @param error 発行するエラーオブジェクト
|
|
143
|
+
*/
|
|
144
|
+
emitError(error) {
|
|
145
|
+
const hasListeners = this.emit('error', error);
|
|
146
|
+
if (!hasListeners) {
|
|
147
|
+
// eslint-disable-next-line no-console
|
|
148
|
+
console.warn('[duckscatter]', `${error.code}: ${error.message}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 散布図をレンダリングする(GPUレイヤーとラベルの両方)
|
|
153
|
+
*/
|
|
154
|
+
render() {
|
|
155
|
+
this.gpuLayer.render();
|
|
156
|
+
this.labelLayer.render();
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* GeoJSONデータからラベルを読み込む
|
|
160
|
+
* @param geojsonData ラベルポイントを含むGeoJSON FeatureCollection
|
|
161
|
+
*/
|
|
162
|
+
loadLabels(geojsonData) {
|
|
163
|
+
this.labelLayer.loadLabels(geojsonData);
|
|
164
|
+
this.render();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* プロットデータを更新して再レンダリングする
|
|
168
|
+
* @param options 更新する設定オプション
|
|
169
|
+
*/
|
|
170
|
+
async update(options) {
|
|
171
|
+
if (options.data !== undefined) {
|
|
172
|
+
const result = this.dataLayer.updateOptions({
|
|
173
|
+
sizeSql: options.data.sizeSql,
|
|
174
|
+
colorSql: options.data.colorSql,
|
|
175
|
+
whereConditions: options.data.whereConditions,
|
|
176
|
+
gpuFilterColumns: options.data.gpuFilterColumns,
|
|
177
|
+
});
|
|
178
|
+
if (result.gpuFilterColumnsChanged) {
|
|
179
|
+
const gpuFilterData = await this.dataLayer.loadGpuFilterColumns();
|
|
180
|
+
if (gpuFilterData) {
|
|
181
|
+
this.gpuLayer.uploadFilterColumns(gpuFilterData.data, gpuFilterData.columnCount);
|
|
182
|
+
this.gpuFilterColumnMapping = gpuFilterData.columnMapping;
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
this.gpuFilterColumnMapping.clear();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (options.data.gpuWhereConditions !== undefined) {
|
|
189
|
+
const conditions = this.convertGpuWhereConditions(options.data.gpuWhereConditions);
|
|
190
|
+
this.gpuLayer.setGpuFilterConditions(conditions);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (options.gpu !== undefined || options.data?.visiblePointLimit !== undefined) {
|
|
194
|
+
this.gpuLayer.updateOptions({
|
|
195
|
+
backgroundColor: options.gpu?.backgroundColor,
|
|
196
|
+
visiblePointLimit: options.data?.visiblePointLimit,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (options.labels !== undefined) {
|
|
200
|
+
this.labelLayer.updateOptions({
|
|
201
|
+
labelFontSize: options.labels.fontSize,
|
|
202
|
+
filterLambda: options.labels.filterLambda,
|
|
203
|
+
onLabelClick: options.labels.onClick,
|
|
204
|
+
hoverOutlineOptions: options.labels.hoverOutlineOptions,
|
|
205
|
+
});
|
|
206
|
+
if (options.labels.url !== undefined) {
|
|
207
|
+
await this.loadLabelsFromUrl(options.labels.url);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (options.interaction !== undefined) {
|
|
211
|
+
this.labelLayer.updateOptions({
|
|
212
|
+
onPointHover: (data) => this.handlePointHover(data, options.interaction?.onPointHover),
|
|
213
|
+
onLabelHover: options.interaction.onLabelHover,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
this.render();
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* キャンバスをリサイズして再レンダリングする
|
|
220
|
+
* @param width 新しい幅(ピクセル)
|
|
221
|
+
* @param height 新しい高さ(ピクセル)
|
|
222
|
+
*/
|
|
223
|
+
resize(width, height) {
|
|
224
|
+
this.gpuLayer.resize(width, height);
|
|
225
|
+
this.labelLayer.resize(width, height);
|
|
226
|
+
this.render();
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* ズームレベルを設定する
|
|
230
|
+
* @param zoom ズームレベル(1.0 = 通常、>1.0 = ズームイン、<1.0 = ズームアウト)
|
|
231
|
+
*/
|
|
232
|
+
setZoom(zoom) {
|
|
233
|
+
this.gpuLayer.setZoom(zoom);
|
|
234
|
+
this.syncLabelViewTransform();
|
|
235
|
+
this.render();
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* 現在のズームレベルを取得する
|
|
239
|
+
* @returns 現在のズームレベル
|
|
240
|
+
*/
|
|
241
|
+
getZoom() {
|
|
242
|
+
return this.gpuLayer.getZoom();
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 指定した倍率でズームインする
|
|
246
|
+
* @param factor ズーム倍率(デフォルト: 1.2)
|
|
247
|
+
*/
|
|
248
|
+
zoomIn(factor = 1.2) {
|
|
249
|
+
this.setZoom(this.gpuLayer.getZoom() * factor);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* 指定した倍率でズームアウトする
|
|
253
|
+
* @param factor ズーム倍率(デフォルト: 1.2)
|
|
254
|
+
*/
|
|
255
|
+
zoomOut(factor = 1.2) {
|
|
256
|
+
this.setZoom(this.gpuLayer.getZoom() / factor);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 指定した画面座標を中心にズームする
|
|
260
|
+
* @param newZoom 新しいズームレベル
|
|
261
|
+
* @param screenX 画面X座標(キャンバスピクセル単位)
|
|
262
|
+
* @param screenY 画面Y座標(キャンバスピクセル単位)
|
|
263
|
+
*/
|
|
264
|
+
zoomToPoint(newZoom, screenX, screenY) {
|
|
265
|
+
this.gpuLayer.zoomToPoint(newZoom, screenX, screenY);
|
|
266
|
+
this.syncLabelViewTransform();
|
|
267
|
+
this.render();
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* ズームとパンをデフォルト値にリセットする
|
|
271
|
+
*/
|
|
272
|
+
resetView() {
|
|
273
|
+
this.gpuLayer.setZoom(1.0);
|
|
274
|
+
this.gpuLayer.setPan(0.0, 0.0);
|
|
275
|
+
this.syncLabelViewTransform();
|
|
276
|
+
this.render();
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* パンオフセットを設定する
|
|
280
|
+
* @param x 正規化座標でのX方向パンオフセット(-1から1)
|
|
281
|
+
* @param y 正規化座標でのY方向パンオフセット(-1から1)
|
|
282
|
+
*/
|
|
283
|
+
setPan(x, y) {
|
|
284
|
+
this.gpuLayer.setPan(x, y);
|
|
285
|
+
this.syncLabelViewTransform();
|
|
286
|
+
this.render();
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* 現在のパンオフセットを取得する
|
|
290
|
+
* @returns x, y座標を含むオブジェクト
|
|
291
|
+
*/
|
|
292
|
+
getPan() {
|
|
293
|
+
return this.gpuLayer.getPan();
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* 指定した差分だけパンする
|
|
297
|
+
* @param dx 正規化座標でのX方向の差分
|
|
298
|
+
* @param dy 正規化座標でのY方向の差分
|
|
299
|
+
*/
|
|
300
|
+
pan(dx, dy) {
|
|
301
|
+
const currentPan = this.gpuLayer.getPan();
|
|
302
|
+
this.setPan(currentPan.x + dx, currentPan.y + dy);
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* ラベルレイヤーのビュー変換をGPUレイヤーと同期する
|
|
306
|
+
*/
|
|
307
|
+
syncLabelViewTransform() {
|
|
308
|
+
const pan = this.gpuLayer.getPan();
|
|
309
|
+
this.labelLayer.updateViewTransform(this.gpuLayer.getZoom(), pan.x, pan.y);
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* ラベルレイヤーからのポイントホバーイベントを処理する
|
|
313
|
+
* @param data ホバー中のポイントデータ(またはnull)
|
|
314
|
+
* @param userCallback ユーザー定義のコールバック
|
|
315
|
+
*/
|
|
316
|
+
handlePointHover(data, userCallback) {
|
|
317
|
+
if (userCallback) {
|
|
318
|
+
userCallback(data);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* GpuWhereConditionをGpuLayer用の形式に変換する
|
|
323
|
+
* @param conditions ユーザー指定のGPUフィルター条件
|
|
324
|
+
* @returns GpuLayer用のフィルター条件配列
|
|
325
|
+
*/
|
|
326
|
+
convertGpuWhereConditions(conditions) {
|
|
327
|
+
return conditions
|
|
328
|
+
.filter((c) => this.gpuFilterColumnMapping.has(c.column))
|
|
329
|
+
.map((c) => ({
|
|
330
|
+
columnIndex: this.gpuFilterColumnMapping.get(c.column),
|
|
331
|
+
min: c.min ?? -Infinity,
|
|
332
|
+
max: c.max ?? Infinity,
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* カスタムSQLクエリを実行する
|
|
337
|
+
* @param query 実行するSQLクエリ文字列またはtoStringメソッドを持つオブジェクト
|
|
338
|
+
* @returns クエリ結果のParquetData
|
|
339
|
+
*/
|
|
340
|
+
async runQuery(query) {
|
|
341
|
+
return await this.dataLayer.executeQuery(query);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* 読み込まれたすべてのラベルを取得する
|
|
345
|
+
* @returns ラベルの配列
|
|
346
|
+
*/
|
|
347
|
+
getLabels() {
|
|
348
|
+
return this.labelLayer.getLabels();
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* IDを指定してプログラム的にポイントをホバー状態にする
|
|
352
|
+
* @param pointId ホバーするポイントのidColumn値
|
|
353
|
+
* @returns ポイントが見つかりホバーされた場合はtrue、そうでない場合はfalse
|
|
354
|
+
*/
|
|
355
|
+
async setPointHover(pointId) {
|
|
356
|
+
if (!this.dataLayer.isInitialized()) {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
const pointData = await this.dataLayer.findPointById(pointId);
|
|
360
|
+
if (!pointData) {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
this.labelLayer.setHoveredPoint(pointData);
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* ポイントのホバー状態をクリアする
|
|
368
|
+
*/
|
|
369
|
+
clearPointHover() {
|
|
370
|
+
this.labelLayer.setHoveredPoint(null);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* 現在ホバー中のポイントデータを取得する
|
|
374
|
+
* @returns ホバー中の場合はポイントデータ、そうでない場合はnull
|
|
375
|
+
*/
|
|
376
|
+
getHoveredPoint() {
|
|
377
|
+
return this.labelLayer.getHoveredPoint();
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* プログラム的にラベルをホバー状態にする
|
|
381
|
+
* @param identifier ラベル識別子(テキストまたはクラスターで識別)
|
|
382
|
+
* @returns ラベルが見つかりホバーされた場合はtrue、そうでない場合はfalse
|
|
383
|
+
*/
|
|
384
|
+
setLabelHover(identifier) {
|
|
385
|
+
const label = this.labelLayer.findLabel(identifier);
|
|
386
|
+
if (!label) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
this.labelLayer.setHoveredLabel(label);
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* ラベルのホバー状態をクリアする
|
|
394
|
+
*/
|
|
395
|
+
clearLabelHover() {
|
|
396
|
+
this.labelLayer.setHoveredLabel(null);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* 現在ホバー中のラベルを取得する
|
|
400
|
+
* @returns ホバー中の場合はラベル、そうでない場合はnull
|
|
401
|
+
*/
|
|
402
|
+
getHoveredLabel() {
|
|
403
|
+
return this.labelLayer.getHoveredLabel();
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* すべてのホバー状態をクリアする(ポイントとラベルの両方)
|
|
407
|
+
*/
|
|
408
|
+
clearAllHover() {
|
|
409
|
+
this.labelLayer.setHoveredPoint(null);
|
|
410
|
+
this.labelLayer.setHoveredLabel(null);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* グローバル透明度を設定する
|
|
414
|
+
* @param alpha 透明度 (0.0-1.0)
|
|
415
|
+
*/
|
|
416
|
+
setPointAlpha(alpha) {
|
|
417
|
+
this.gpuLayer.setPointAlpha(alpha);
|
|
418
|
+
this.render();
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* 現在のグローバル透明度を取得する
|
|
422
|
+
* @returns 現在の透明度値 (0.0-1.0)
|
|
423
|
+
*/
|
|
424
|
+
getPointAlpha() {
|
|
425
|
+
return this.gpuLayer.getPointAlpha();
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* グローバルサイズスケールを設定する
|
|
429
|
+
* @param scale サイズスケール (0.01以上)
|
|
430
|
+
*/
|
|
431
|
+
setPointSizeScale(scale) {
|
|
432
|
+
this.gpuLayer.setPointSizeScale(scale);
|
|
433
|
+
this.render();
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* 現在のグローバルサイズスケールを取得する
|
|
437
|
+
* @returns 現在のサイズスケール値
|
|
438
|
+
*/
|
|
439
|
+
getPointSizeScale() {
|
|
440
|
+
return this.gpuLayer.getPointSizeScale();
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* リソースを破棄する
|
|
444
|
+
*/
|
|
445
|
+
async destroy() {
|
|
446
|
+
await this.dataLayer.destroy();
|
|
447
|
+
this.gpuLayer.destroy();
|
|
448
|
+
this.labelLayer.destroy();
|
|
449
|
+
}
|
|
450
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* duckscatterライブラリの型定義
|
|
3
|
+
*/
|
|
4
|
+
export type LabelFilterLambda = (properties: Record<string, any>) => boolean;
|
|
5
|
+
export type PointHoverCallback = (data: {
|
|
6
|
+
row: any[];
|
|
7
|
+
columns: string[];
|
|
8
|
+
} | null) => void;
|
|
9
|
+
/** ポイント識別子の型(idColumnの値) */
|
|
10
|
+
export type PointId = string | number;
|
|
11
|
+
/** プログラムによるホバー制御用のラベル識別子 */
|
|
12
|
+
export interface LabelIdentifier {
|
|
13
|
+
/** テキストでラベルを識別 */
|
|
14
|
+
text?: string;
|
|
15
|
+
/** クラスター番号でラベルを識別 */
|
|
16
|
+
cluster?: number;
|
|
17
|
+
}
|
|
18
|
+
/** ラベルがホバーされた時に発火するコールバック */
|
|
19
|
+
export type LabelHoverCallback = (label: Label | null) => void;
|
|
20
|
+
export interface ColorRGBA {
|
|
21
|
+
r: number;
|
|
22
|
+
g: number;
|
|
23
|
+
b: number;
|
|
24
|
+
a: number;
|
|
25
|
+
}
|
|
26
|
+
export interface HoverOutlineOptions {
|
|
27
|
+
/** ホバーアウトラインを有効化(デフォルト: true) */
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
/** アウトラインの線色(デフォルト: 白) */
|
|
30
|
+
color?: string;
|
|
31
|
+
/** アウトラインの線幅(ピクセル単位、デフォルト: 2) */
|
|
32
|
+
width?: number;
|
|
33
|
+
minimumHoverSize?: number;
|
|
34
|
+
outlinedPointAddition?: number;
|
|
35
|
+
}
|
|
36
|
+
export interface Label {
|
|
37
|
+
/** 表示するラベルテキスト */
|
|
38
|
+
text: string;
|
|
39
|
+
/** データ空間でのX座標 */
|
|
40
|
+
x: number;
|
|
41
|
+
/** データ空間でのY座標 */
|
|
42
|
+
y: number;
|
|
43
|
+
/** オプションのラベルプロパティ */
|
|
44
|
+
cluster?: number;
|
|
45
|
+
count?: number;
|
|
46
|
+
/** 元のGeoJSONフィーチャーのプロパティ */
|
|
47
|
+
properties?: Record<string, any>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* データクエリ用のWHERE条件フィルター
|
|
51
|
+
*/
|
|
52
|
+
/** 数値比較演算子 */
|
|
53
|
+
export type NumericOperator = '>=' | '>' | '<=' | '<';
|
|
54
|
+
/** 文字列比較演算子 */
|
|
55
|
+
export type StringOperator = 'contains' | 'equals' | 'startsWith' | 'endsWith';
|
|
56
|
+
/** 数値フィルター条件 */
|
|
57
|
+
export interface NumericFilter {
|
|
58
|
+
type: 'numeric';
|
|
59
|
+
column: string;
|
|
60
|
+
operator: NumericOperator;
|
|
61
|
+
value: number;
|
|
62
|
+
}
|
|
63
|
+
/** 文字列フィルター条件 */
|
|
64
|
+
export interface StringFilter {
|
|
65
|
+
type: 'string';
|
|
66
|
+
column: string;
|
|
67
|
+
operator: StringOperator;
|
|
68
|
+
value: string;
|
|
69
|
+
}
|
|
70
|
+
/** 生SQLフィルター条件 */
|
|
71
|
+
export interface RawSqlFilter {
|
|
72
|
+
type: 'raw';
|
|
73
|
+
sql: string;
|
|
74
|
+
}
|
|
75
|
+
/** すべてのWHERE条件の共用体型 */
|
|
76
|
+
export type WhereCondition = NumericFilter | StringFilter | RawSqlFilter;
|
|
77
|
+
/** GPUフィルター条件 (range only) */
|
|
78
|
+
export interface GpuWhereCondition {
|
|
79
|
+
/** フィルター対象のカラム名 (gpuFilterColumnsで指定した名前) */
|
|
80
|
+
column: string;
|
|
81
|
+
/** 最小値 (指定しない場合は -Infinity) */
|
|
82
|
+
min?: number;
|
|
83
|
+
/** 最大値 (指定しない場合は +Infinity) */
|
|
84
|
+
max?: number;
|
|
85
|
+
}
|
|
86
|
+
export interface DataOptions {
|
|
87
|
+
/** レンダリングする表示ポイントの最大数 */
|
|
88
|
+
visiblePointLimit?: number;
|
|
89
|
+
/** ポイントサイズ用のSQL式(例: "LOG(favorite_count + 1) * 2 + 2") */
|
|
90
|
+
sizeSql?: string;
|
|
91
|
+
/** ポイントカラー用のSQL式(ARGB 32bit整数、例: "0xFF0000FF") */
|
|
92
|
+
colorSql?: string;
|
|
93
|
+
/** データをフィルタリングするWHERE条件(ANDのみ) */
|
|
94
|
+
whereConditions?: WhereCondition[];
|
|
95
|
+
/** GPUでフィルタリングするカラム名 (最大4つ) */
|
|
96
|
+
gpuFilterColumns?: string[];
|
|
97
|
+
/** GPU側で実行するフィルター条件 */
|
|
98
|
+
gpuWhereConditions?: GpuWhereCondition[];
|
|
99
|
+
/** ポイントを識別するカラム名 */
|
|
100
|
+
idColumn: string;
|
|
101
|
+
}
|
|
102
|
+
export interface GpuOptions {
|
|
103
|
+
/** 背景色(デフォルト: 透明な黒) */
|
|
104
|
+
backgroundColor?: ColorRGBA;
|
|
105
|
+
/** グローバル透明度 (0.0-1.0, デフォルト: 1.0) */
|
|
106
|
+
pointAlpha?: number;
|
|
107
|
+
/** グローバルサイズスケール (デフォルト: 1.0) */
|
|
108
|
+
pointSizeScale?: number;
|
|
109
|
+
}
|
|
110
|
+
export interface LabelOptions {
|
|
111
|
+
/** ラベルGeoJSONデータを取得するURL(初期化時に自動ロード) */
|
|
112
|
+
url?: string;
|
|
113
|
+
/** ラベルのフォントサイズ(ピクセル単位、デフォルト: 12) */
|
|
114
|
+
fontSize?: number;
|
|
115
|
+
/** プロパティに基づいてラベルの表示を制御するフィルター関数 */
|
|
116
|
+
filterLambda?: LabelFilterLambda;
|
|
117
|
+
/** ラベルがクリックされた時に発火するコールバック */
|
|
118
|
+
onClick?: (label: Label) => void;
|
|
119
|
+
/** ポイントホバーアウトラインの外観オプション */
|
|
120
|
+
hoverOutlineOptions?: HoverOutlineOptions;
|
|
121
|
+
}
|
|
122
|
+
export interface InteractionOptions {
|
|
123
|
+
/** ポイントがホバーされた時に発火するコールバック */
|
|
124
|
+
onPointHover?: PointHoverCallback;
|
|
125
|
+
/** ラベルがホバーされた時に発火するコールバック */
|
|
126
|
+
onLabelHover?: LabelHoverCallback;
|
|
127
|
+
}
|
|
128
|
+
export interface ScatterPlotOptions {
|
|
129
|
+
/** レンダリング先のCanvas要素 */
|
|
130
|
+
canvas: HTMLCanvasElement;
|
|
131
|
+
/** Parquetデータを取得するURL */
|
|
132
|
+
dataUrl: string;
|
|
133
|
+
/** データレイヤーオプション */
|
|
134
|
+
data: DataOptions;
|
|
135
|
+
/** GPUレンダリングオプション */
|
|
136
|
+
gpu?: GpuOptions;
|
|
137
|
+
/** ラベルレイヤーオプション */
|
|
138
|
+
labels?: LabelOptions;
|
|
139
|
+
/** インタラクションコールバック */
|
|
140
|
+
interaction?: InteractionOptions;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* エラーハンドリング型
|
|
144
|
+
*/
|
|
145
|
+
/** エラー重大度レベル */
|
|
146
|
+
export type ErrorSeverity = 'fatal' | 'error' | 'warning';
|
|
147
|
+
/** エラーカテゴリ */
|
|
148
|
+
export type ErrorCategory = 'webgpu' | 'data' | 'label' | 'query' | 'network';
|
|
149
|
+
/** すべての可能なエラーのエラーコード */
|
|
150
|
+
export type ErrorCode = 'WEBGPU_NOT_SUPPORTED' | 'GPU_ADAPTER_NOT_AVAILABLE' | 'GPU_DEVICE_FAILED' | 'WEBGPU_CONTEXT_FAILED' | 'DATA_LAYER_NOT_INITIALIZED' | 'PARQUET_LOAD_FAILED' | 'QUERY_FAILED' | 'LABEL_FETCH_FAILED' | 'LABEL_PARSE_FAILED' | 'NETWORK_ERROR';
|
|
151
|
+
/** エラーイベントペイロード */
|
|
152
|
+
export interface ScatterPlotError {
|
|
153
|
+
/** プログラムによるハンドリング用のエラーコード */
|
|
154
|
+
code: ErrorCode;
|
|
155
|
+
/** エラーカテゴリ */
|
|
156
|
+
category: ErrorCategory;
|
|
157
|
+
/** エラー重大度 */
|
|
158
|
+
severity: ErrorSeverity;
|
|
159
|
+
/** 人が読めるエラーメッセージ */
|
|
160
|
+
message: string;
|
|
161
|
+
/** 利用可能な場合、元のエラーオブジェクト */
|
|
162
|
+
cause?: Error;
|
|
163
|
+
/** 追加のコンテキスト情報 */
|
|
164
|
+
context?: Record<string, unknown>;
|
|
165
|
+
/** エラーが発生したタイムスタンプ */
|
|
166
|
+
timestamp: number;
|
|
167
|
+
}
|
|
168
|
+
/** ScatterPlot EventEmitter用のイベントマップ */
|
|
169
|
+
export interface ScatterPlotEventMap {
|
|
170
|
+
error: ScatterPlotError;
|
|
171
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LabelLayer } from './label-layer.js';
|
package/dist/ui/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LabelLayer } from './label-layer.js';
|