@uoa-css-lab/duckscatter
v1.11.0
Published
A TypeScript library for plotting scatter charts using WebGPU
Maintainers
Readme
duckscatter
duckscatterは大規模な散布図を描画するための、TypeScriptライブラリです。
従来の散布図描画ライブラリでは、全文検索などの複雑なフィルタリングに時間がかかりました。duckscatterは、DuckDB-WASMによるSQL内での高速なデータ処理と、WebGPUによる大規模並列レンダリングを組み合わせることで、数十万点規模のデータでもスムーズな操作を実現します。
WebGPUによるレンダリング: GPUの能力を活用し、ブラウザ上で大規模な散布図を高速に描画します。CanvasやSVGでは扱えきれないような大量のデータポイントであっても、スムーズな操作を実現します。
DuckDB-WASMによる高速なデータ分析: DuckDB-WASMを内蔵しており、標準的なSQLを実行できます。SQLを使って動的に描画データをフィルタリングしたり、点の色やサイズを計算したりすることが可能です。
ラベル表示: データポイントにテキストラベルを表示できます。クラスタリングの結果を可視化する際に、各クラスタの中心や代表点にラベルを表示したり、特定のデータポイントに注釈を付けたりするのに最適です。
画面イメージ
データについて
Parquetファイル
duckscatterは、Parquet形式のデータファイルを読み込みます。以下のカラムが必要です:
| カラム | 型 | 説明 |
|--------|------|------|
| x | double | X座標 |
| y | double | Y座標 |
その他のカラムはSQLで参照でき、色やサイズの計算に利用できます。
GeoJSONファイル(ラベル用)
ラベルを表示するには、GeoJSON形式のファイルを指定します:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [x, y]
},
"properties": {
"cluster_label": "ラベルテキスト"
}
}
]
}API
const plot = new ScatterPlot({
canvas: HTMLCanvasElement, // 描画先のcanvas要素
dataUrl: string, // ParquetファイルのURL
data: {
visiblePointLimit?: number, // 描画最大ポイント数(デフォルト: 100,000)
sizeSql?: string, // サイズ計算SQL式(デフォルト: "3")
colorSql?: string, // 色計算SQL式(ARGB 32bit整数、デフォルト: "0x4D4D4DCC")
whereConditions?: WhereCondition[], // フィルタ条件(CPU側)
gpuFilterColumns?: string[], // GPUフィルタリング用カラム名(最大4つ)
gpuWhereConditions?: GpuWhereCondition[], // GPUフィルター条件
},
gpu?: {
backgroundColor?: Color4f, // 背景色
pointAlpha?: number, // グローバル透明度(0.0-1.0、デフォルト: 1.0)
pointSizeScale?: number, // グローバルサイズスケール(デフォルト: 1.0)
},
labels?: {
url?: string, // GeoJSONファイルのURL
fontSize?: number, // フォントサイズ(デフォルト: 12)
filterLambda?: LabelFilterLambda, // ラベル表示フィルタ(false=非マッチ)
unmatchedLabelOpacity?: number, // 非マッチラベルをグレー化せず元色のまま dim(0-1)
onClick?: (label: Label) => void, // クリックコールバック
hoverOutlineOptions?: HoverOutlineOptions, // ホバーアウトライン設定
},
interaction?: {
onPointHover?: PointHoverCallback, // ポイントホバーコールバック
onLabelHover?: LabelHoverCallback, // ラベルホバーコールバック
},
});
await plot.initialize();主要メソッド:
render(): 描画resize(width, height): キャンバスリサイズsetZoom(zoom)/getZoom()/zoomIn()/zoomOut(): ズーム操作zoomToPoint(newZoom, screenX, screenY): 指定座標を中心にズームsetPan(x, y)/getPan()/pan(dx, dy): パン操作resetView(): ビューリセットupdate(options): オプション更新getLastUpdatePlan(): 直近のupdate()が DuckDB / GPU / label のどの更新経路を通ったかを取得runQuery(sql): カスタムSQLクエリ実行getLabels(): ラベル全件取得destroy(): リソース解放
ポイント表示属性制御:
setPointAlpha(alpha): グローバル透明度を設定(0.0-1.0)getPointAlpha(): 現在のグローバル透明度を取得setPointSizeScale(scale): グローバルサイズスケールを設定getPointSizeScale(): 現在のグローバルサイズスケールを取得
選択(ブラシ)API:
GPU 常駐の選択マスク(1 bit/point)を矩形ブラシや ID 指定で操作します(詳細は「ブラシ選択(Selection)」節を参照)。
brushSelect(bounds, options?): データ空間矩形で選択を更新brushSelectScreenRect(rect, options?): キャンバス画面座標(物理px)矩形で選択を更新setSelectedPointIds(ids): ポイント ID(rowid)集合で選択を直接設定clearSelection(): 選択を全クリアsetSelectionStyle(style): 選択の描画スタイルを更新getSelectionCount(): 現在の選択数を取得(Promise<number>)setBrushActive(active): 選択点が0でも選択 dim を強制(ブラシ開始時に背景を即 dim、終了時にfalseへ戻す)
ホバーマスク(クラスタ強調)API(selection とは独立):
選択の dim 表示中に、ホバー中クラスタのノードだけを dim 解除して強調するための GPU 常駐マスク(1 bit/point)。selection とは別バッファで、getSelectionCount() / ブラシ / 選択状態を一切汚染しません。
setHoveredPointIds(ids): ポイント ID(rowid)集合を hover-mask に設定(該当点の selection dim を解除し元の明度へ戻す)clearHover(): hover-mask を全クリア(selection には影響しない)
ホバー制御API:
外部コンポーネントからプログラム的にホバー状態を制御できます。
setPointHover(pointId): ポイントをホバー状態に(Promise<boolean>)clearPointHover(): ポイントホバー解除getHoveredPoint(): ホバー中のポイント取得setLabelHover(identifier): ラベルをホバー状態に(booleanを返す)clearLabelHover(): ラベルホバー解除getHoveredLabel(): ホバー中のラベル取得clearAllHover(): 全ホバー解除
// 使用例
await plot.setPointHover(12345); // rowidでポイントをホバー
plot.setLabelHover({ text: 'Cluster A' }); // テキストでラベルをホバー
plot.setLabelHover({ cluster: 5 }); // クラスタ番号でラベルをホバー
plot.clearAllHover(); // 全ホバー解除GPUフィルタリング
GPUフィルタリングを使用すると、数値カラムの範囲フィルタをGPU側で高速に実行できます。データの再フェッチなしにリアルタイムでフィルタリングが可能です。
const plot = new ScatterPlot({
// ...
data: {
// GPUフィルタリング用のカラムを指定(最大4つ)
gpuFilterColumns: ['frequency', 'length'],
// フィルター条件を指定
gpuWhereConditions: [
{ column: 'frequency', min: 100, max: 10000 },
{ column: 'length', min: 3 },
],
},
});
// 実行時にフィルター条件を更新
await plot.update({
data: {
gpuWhereConditions: [
{ column: 'frequency', min: 500 },
],
},
});CPUフィルタ(whereConditions)との違い:
| | CPUフィルタ | GPUフィルタ | |---|---|---| | 対応演算子 | 数値比較、文字列検索、生SQL | 範囲のみ(min/max) | | 更新速度 | SQLクエリ再実行が必要 | 即座に反映 | | 用途 | 複雑な条件、全文検索 | スライダーなどリアルタイム操作 |
soft-edge フェード(fade)
gpuWhereConditions の各条件に fade を付けると、フィルタ範囲 [min,max] の端で
ポイントの不透明度(alpha)を連続的にランプさせられます。範囲外のハードカットは
そのままで、端から内側へ width 分だけフェードします。colorSql の再評価を伴わず
GPU の uniform 更新のみで反映されるため、フィルタ範囲をスライドさせながら毎フレーム
安価にフェードできます(例: 時間窓スライド時のノードのフェードイン/アウト)。
await plot.update({
data: {
gpuWhereConditions: [
{
column: 'created_at',
min: t0,
max: t1,
fade: { width: dt, edges: 'both' }, // 窓の両端から dt 分フェード
},
],
},
});width: 端のランプ幅(columnと同じ単位、> 0。0以下でフェード無効)edges: フェードする端('both'(既定) /'min'/'max')。min/maxを省略した 無限端は自動的にフェード無効。
gpuFilterColumnsは最大 4 列です。5 列目以降は無視され、CONFIG_WARNINGイベントが発火します。
更新経路の確認
update() の実行後、getLastUpdatePlan() でその更新がどの経路を通ったかを確認できます。
UI 操作が DuckDB の再クエリを伴うのか、GPU uniform 更新だけで済んだのかをデバッグできます。
await plot.update({ data: { gpuWhereConditions: [{ column: 'frequency', min: 500 }] } });
console.log(plot.getLastUpdatePlan()?.paths);
// ['gpu-filter-uniforms']| 経路 | 意味 |
|---|---|
| duckdb-all-points | sizeSql / colorSql 変更により DuckDB で GPU 用 point buffer を再生成 |
| duckdb-visibility-flags | whereConditions 変更により DuckDB で visibility bitmap を再生成 |
| gpu-filter-columns-buffer | gpuFilterColumns 変更により GPU filter column buffer を再アップロード |
| gpu-filter-uniforms | gpuWhereConditions / filteredPointDisplayMode 変更。高頻度操作向け |
| gpu-render-uniforms | 背景色・透明度・サイズスケール・visiblePointLimit などの更新 |
| label-layer | ラベル設定またはラベルデータの更新 |
| interaction-callbacks | hover などの callback 更新 |
ブラシ選択(Selection)
矩形ブラシで点集合を選択し、GPU 常駐の選択マスク(1 bit/point)として保持できます。選択状態は GPU 上のビットセットに直接書き込まれ、CPU へ読み戻さずレンダリングに反映されるため、数百万点規模でも高速です。選択が 1 点以上あるときだけ選択点を強調色で描画し、非選択点を減衰させます(選択が空のときは通常表示のまま)。
// データ空間の矩形で選択(既定: mode='replace' / target='filtered-data')
plot.brushSelect({ minX, maxX, minY, maxY });
// キャンバス画面座標(物理ピクセル)の矩形で選択
plot.brushSelectScreenRect({ x0, y0, x1, y1 }, { mode: 'add', target: 'all-data' });
// ポイント ID(rowid)集合で直接選択
plot.setSelectedPointIds([0, 12, 345]);
// 選択数の取得(GPU からの非同期読み戻し)
const n = await plot.getSelectionCount();
// 選択解除 / スタイル変更
plot.clearSelection();
plot.setSelectionStyle({ selectedColor: { r: 1, g: 0.2, b: 0.2, a: 1 }, unselectedAlpha: 0.15 });
// hover-mask: 選択の dim 中、ホバー中クラスタのノードだけ dim を解除して強調(selection とは独立)
plot.setHoveredPointIds([2, 5, 9]);
plot.clearHover();
// ブラシ操作の開始/終了で「選択 dim」を即時に出す/消す(0 選択でも背景を dim)
plot.setBrushActive(true); // 右ドラッグ開始時など
plot.setBrushActive(false); // ジェスチャ終了時BrushOptions:
mode: 既存選択との合成方法'replace'(既定): ブラシ内を選択し、ブラシ外を解除'add': ブラシ内を選択に追加'subtract': ブラシ内を選択から除外'toggle': ブラシ内の選択状態を反転
target: 選択対象の集合'filtered-data'(既定): 現在のwhereConditions/gpuWhereConditionsを通過した点のみ'all-data': フィルタ状態に関係なく全点
SelectionStyle(コンストラクタの gpu.selection または setSelectionStyle で指定):
selectedColor: 選択点の色(既定: 黄系の強調色)unselectedAlpha: 選択有効時の非選択点の alpha 係数(既定0.25)selectedSizeScale: 選択点のサイズ倍率(既定1.35、highlightSelected: trueのときのみ適用)highlightSelected: 選択点を強調するか(既定true)。trueで選択点をselectedColorに塗り替えselectedSizeScaleで拡大。falseにすると選択点は色もサイズも元のまま保持し、非選択点の減衰(unselectedAlpha)のみ行う(クラスタ配色などを保ったまま「非選択を暗くする」dim-only 表示)
ポイント ID は Parquet の行順(rowid = ポイントバッファの index)に対応します。
型定義
主な型定義:
// GPUフィルター条件
interface GpuWhereCondition {
column: string; // gpuFilterColumnsで指定したカラム名
min?: number; // 最小値(省略時: -Infinity)
max?: number; // 最大値(省略時: +Infinity)
fade?: { // オプション: 範囲端の soft-edge フェード
width: number; // 端のランプ幅(> 0)
edges?: 'both' | 'min' | 'max'; // フェードする端(既定 'both')
};
}
// ホバーアウトラインオプション
interface HoverOutlineOptions {
enabled?: boolean; // 有効化(デフォルト: true)
color?: string; // 線色(デフォルト: 白)
width?: number; // 線幅(ピクセル、デフォルト: 2)
minimumHoverSize?: number; // 最小ホバーサイズ
outlinedPointAddition?: number;
}
// WHERE条件フィルター
type WhereCondition = NumericFilter | StringFilter | RawSqlFilter;
interface NumericFilter {
type: 'numeric';
column: string;
operator: '>=' | '>' | '<=' | '<';
value: number;
}
interface StringFilter {
type: 'string';
column: string;
operator: 'contains' | 'equals' | 'startsWith' | 'endsWith';
value: string;
}
interface RawSqlFilter {
type: 'raw';
sql: string;
}Examples
実行方法
# ライブラリのビルド(ルートディレクトリで)
npm install
npm run build
# サンプルアプリの実行
cd examples/next
npm install
npm run devブラウザで http://localhost:3000 を開きます。
サンプルデータ
サンプルでは、GloVe 6B単語ベクトルをUMAPで2次元に投影したデータを使用しています(約40万単語)。
- データセット: https://huggingface.co/datasets/mt0rm0/glove.6B.50d.umap.2d
ラベル生成
ラベルはDBSCANクラスタリングとOpenAI APIを使って生成できます:
cd examples/next
export OPENAI_API_KEY="your-api-key"
python scripts/generate_labels.pyライセンス
このライブラリは MIT License の下でライセンスされています。
