@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,42 @@
|
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
|
3
|
+
# Please see the documentation for all configuration options:
|
|
4
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: "github-actions"
|
|
9
|
+
directory: "/"
|
|
10
|
+
schedule:
|
|
11
|
+
interval: "monthly"
|
|
12
|
+
open-pull-requests-limit: 50
|
|
13
|
+
labels:
|
|
14
|
+
- "dependencies"
|
|
15
|
+
groups:
|
|
16
|
+
dependencies:
|
|
17
|
+
patterns:
|
|
18
|
+
- "*"
|
|
19
|
+
|
|
20
|
+
- package-ecosystem: "npm"
|
|
21
|
+
directory: "/"
|
|
22
|
+
schedule:
|
|
23
|
+
interval: "monthly"
|
|
24
|
+
open-pull-requests-limit: 50
|
|
25
|
+
labels:
|
|
26
|
+
- "dependencies"
|
|
27
|
+
groups:
|
|
28
|
+
dependencies:
|
|
29
|
+
patterns:
|
|
30
|
+
- "*"
|
|
31
|
+
|
|
32
|
+
- package-ecosystem: "npm"
|
|
33
|
+
directory: "/examples/next"
|
|
34
|
+
schedule:
|
|
35
|
+
interval: "monthly"
|
|
36
|
+
open-pull-requests-limit: 50
|
|
37
|
+
labels:
|
|
38
|
+
- "dependencies"
|
|
39
|
+
groups:
|
|
40
|
+
dependencies:
|
|
41
|
+
patterns:
|
|
42
|
+
- "*"
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
on:
|
|
2
|
+
push:
|
|
3
|
+
branches:
|
|
4
|
+
- development
|
|
5
|
+
tags:
|
|
6
|
+
- v*
|
|
7
|
+
pull_request:
|
|
8
|
+
paths-ignore:
|
|
9
|
+
- '**.md'
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v6
|
|
16
|
+
- uses: actions/setup-node@v6
|
|
17
|
+
with:
|
|
18
|
+
node-version: 20
|
|
19
|
+
- uses: actions/cache@v5
|
|
20
|
+
with:
|
|
21
|
+
path: node_modules
|
|
22
|
+
key: ${{ hashFiles('package-lock.json') }}
|
|
23
|
+
- run: npm install
|
|
24
|
+
- name: build
|
|
25
|
+
run: npm run build
|
|
26
|
+
- uses: actions/cache/save@v5
|
|
27
|
+
with:
|
|
28
|
+
path: dist
|
|
29
|
+
key: dist-${{ github.sha }}
|
|
30
|
+
|
|
31
|
+
lint:
|
|
32
|
+
needs:
|
|
33
|
+
- build
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v6
|
|
37
|
+
- uses: actions/setup-node@v6
|
|
38
|
+
with:
|
|
39
|
+
node-version: 20
|
|
40
|
+
- uses: actions/cache@v5
|
|
41
|
+
with:
|
|
42
|
+
path: "**/node_modules"
|
|
43
|
+
key: ${{ hashFiles('package-lock.json', 'examples/next/package-lock.json') }}
|
|
44
|
+
- run: npm install
|
|
45
|
+
- run: npm install --prefix examples/next
|
|
46
|
+
- uses: actions/cache/restore@v5
|
|
47
|
+
with:
|
|
48
|
+
path: dist
|
|
49
|
+
key: dist-${{ github.sha }}
|
|
50
|
+
- name: lint
|
|
51
|
+
run: npm run lint:all
|
|
52
|
+
|
|
53
|
+
test:
|
|
54
|
+
needs:
|
|
55
|
+
- build
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@v6
|
|
59
|
+
- uses: actions/setup-node@v6
|
|
60
|
+
with:
|
|
61
|
+
node-version: 20
|
|
62
|
+
- uses: actions/cache@v5
|
|
63
|
+
with:
|
|
64
|
+
path: "**/node_modules"
|
|
65
|
+
key: ${{ hashFiles('package-lock.json') }}
|
|
66
|
+
- run: npm install
|
|
67
|
+
- uses: actions/cache/restore@v5
|
|
68
|
+
with:
|
|
69
|
+
path: dist
|
|
70
|
+
key: dist-${{ github.sha }}
|
|
71
|
+
- name: test
|
|
72
|
+
run: npm run test
|
|
73
|
+
|
|
74
|
+
publish:
|
|
75
|
+
runs-on: ubuntu-latest
|
|
76
|
+
needs:
|
|
77
|
+
- build
|
|
78
|
+
- lint
|
|
79
|
+
- test
|
|
80
|
+
if: startsWith(github.ref, 'refs/tags/')
|
|
81
|
+
permissions:
|
|
82
|
+
packages: write
|
|
83
|
+
contents: read
|
|
84
|
+
strategy:
|
|
85
|
+
matrix:
|
|
86
|
+
registry-url:
|
|
87
|
+
- https://npm.pkg.github.com
|
|
88
|
+
- https://registry.npmjs.org
|
|
89
|
+
steps:
|
|
90
|
+
- uses: actions/checkout@v6
|
|
91
|
+
- uses: actions/setup-node@v6
|
|
92
|
+
if: ${{ matrix.registry-url == 'https://npm.pkg.github.com' }}
|
|
93
|
+
with:
|
|
94
|
+
node-version: 20
|
|
95
|
+
registry-url: ${{ matrix.registry-url }}
|
|
96
|
+
env:
|
|
97
|
+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
98
|
+
- uses: actions/setup-node@v6
|
|
99
|
+
if: ${{ matrix.registry-url == 'https://registry.npmjs.org' }}
|
|
100
|
+
with:
|
|
101
|
+
node-version: 20
|
|
102
|
+
registry-url: ${{ matrix.registry-url }}
|
|
103
|
+
env:
|
|
104
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPMJS_TOKEN }}
|
|
105
|
+
- uses: actions/cache/restore@v5
|
|
106
|
+
with:
|
|
107
|
+
path: dist
|
|
108
|
+
key: dist-${{ github.sha }}
|
|
109
|
+
- run: npm publish --registry ${{ matrix.registry-url }} --access public
|
|
110
|
+
env:
|
|
111
|
+
NODE_AUTH_TOKEN: ${{ matrix.registry-url == 'https://npm.pkg.github.com' && secrets.GITHUB_TOKEN || secrets.NPMJS_TOKEN }}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
on:
|
|
2
|
+
workflow_dispatch:
|
|
3
|
+
inputs:
|
|
4
|
+
version:
|
|
5
|
+
description: version
|
|
6
|
+
required: true
|
|
7
|
+
type: choice
|
|
8
|
+
options:
|
|
9
|
+
- patch
|
|
10
|
+
- minor
|
|
11
|
+
- major
|
|
12
|
+
- premajor
|
|
13
|
+
- prerelease
|
|
14
|
+
preid:
|
|
15
|
+
description: preid
|
|
16
|
+
type: choice
|
|
17
|
+
options:
|
|
18
|
+
- ""
|
|
19
|
+
- rc
|
|
20
|
+
- alpha
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
create-tag:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
permissions:
|
|
26
|
+
contents: write
|
|
27
|
+
actions: write
|
|
28
|
+
outputs:
|
|
29
|
+
version: ${{ steps.version.outputs.version }}
|
|
30
|
+
steps:
|
|
31
|
+
- name: Generate a token
|
|
32
|
+
id: generate-token
|
|
33
|
+
uses: actions/create-github-app-token@v2
|
|
34
|
+
with:
|
|
35
|
+
app-id: ${{ secrets.RELEASE_APP_ID }}
|
|
36
|
+
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
|
|
37
|
+
- uses: actions/checkout@v6
|
|
38
|
+
with:
|
|
39
|
+
token: ${{ steps.generate-token.outputs.token }}
|
|
40
|
+
- uses: actions/setup-node@v6
|
|
41
|
+
with:
|
|
42
|
+
node-version: 20
|
|
43
|
+
|
|
44
|
+
- run: |
|
|
45
|
+
git config user.name "github-actions[bot]"
|
|
46
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
47
|
+
|
|
48
|
+
- if: ${{ github.event.inputs.preid == '' }}
|
|
49
|
+
run: npm version ${{ github.event.inputs.version }}
|
|
50
|
+
|
|
51
|
+
- if: ${{ github.event.inputs.preid != '' }}
|
|
52
|
+
run: npm version ${{ github.event.inputs.version }} --preid ${{ github.event.inputs.preid }}
|
|
53
|
+
|
|
54
|
+
- name: git push
|
|
55
|
+
run: git push origin --tags ${{ github.ref }}:${{ github.ref }}
|
package/.prettierrc
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# duckscatter
|
|
2
|
+
|
|
3
|
+
[](https://github.com/UoA-CSS-Lab/duckscatter/blob/main/LICENSE)
|
|
4
|
+
[](https://www.npmjs.com/package/@uoa-css-lab/duckscatter)
|
|
5
|
+
[](https://github.com/UoA-CSS-Lab/duckscatter/actions/workflows/ci.yaml)
|
|
6
|
+
[](https://deepwiki.com/UoA-CSS-Lab/duckscatter)
|
|
7
|
+
|
|
8
|
+
> duckscatterは大規模な散布図を描画するための、TypeScriptライブラリです。
|
|
9
|
+
|
|
10
|
+
従来の散布図描画ライブラリでは、全文検索などの複雑なフィルタリングに時間がかかりました。duckscatterは、DuckDB-WASMによるSQL内での高速なデータ処理と、WebGPUによる大規模並列レンダリングを組み合わせることで、数十万点規模のデータでもスムーズな操作を実現します。
|
|
11
|
+
|
|
12
|
+
* **WebGPUによるレンダリング:** GPUの能力を活用し、ブラウザ上で大規模な散布図を高速に描画します。CanvasやSVGでは扱えきれないような大量のデータポイントであっても、スムーズな操作を実現します。
|
|
13
|
+
|
|
14
|
+
* **DuckDB-WASMによる高速なデータ分析:** DuckDB-WASMを内蔵しており、標準的なSQLを実行できます。SQLを使って動的に描画データをフィルタリングしたり、点の色やサイズを計算したりすることが可能です。
|
|
15
|
+
|
|
16
|
+
* **ラベル表示:** データポイントにテキストラベルを表示できます。クラスタリングの結果を可視化する際に、各クラスタの中心や代表点にラベルを表示したり、特定のデータポイントに注釈を付けたりするのに最適です。
|
|
17
|
+
|
|
18
|
+
## 画面イメージ
|
|
19
|
+
|
|
20
|
+
<img src="./docs/image.png" alt="screen image" width="600">
|
|
21
|
+
|
|
22
|
+
## データについて
|
|
23
|
+
|
|
24
|
+
### Parquetファイル
|
|
25
|
+
|
|
26
|
+
duckscatterは、Parquet形式のデータファイルを読み込みます。以下のカラムが必要です:
|
|
27
|
+
|
|
28
|
+
| カラム | 型 | 説明 |
|
|
29
|
+
|--------|------|------|
|
|
30
|
+
| `x` | double | X座標 |
|
|
31
|
+
| `y` | double | Y座標 |
|
|
32
|
+
| IDカラム | 任意 | ポイントを識別するための一意キー(カラム名は`idColumn`オプションで指定) |
|
|
33
|
+
|
|
34
|
+
その他のカラムはSQLで参照でき、色やサイズの計算に利用できます。
|
|
35
|
+
|
|
36
|
+
### GeoJSONファイル(ラベル用)
|
|
37
|
+
|
|
38
|
+
ラベルを表示するには、GeoJSON形式のファイルを指定します:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"type": "FeatureCollection",
|
|
43
|
+
"features": [
|
|
44
|
+
{
|
|
45
|
+
"type": "Feature",
|
|
46
|
+
"geometry": {
|
|
47
|
+
"type": "Point",
|
|
48
|
+
"coordinates": [x, y]
|
|
49
|
+
},
|
|
50
|
+
"properties": {
|
|
51
|
+
"cluster_label": "ラベルテキスト"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
const plot = new ScatterPlot({
|
|
62
|
+
canvas: HTMLCanvasElement, // 描画先のcanvas要素
|
|
63
|
+
dataUrl: string, // ParquetファイルのURL
|
|
64
|
+
data: {
|
|
65
|
+
idColumn: string, // IDカラム名(必須)
|
|
66
|
+
visiblePointLimit?: number, // 描画最大ポイント数(デフォルト: 100,000)
|
|
67
|
+
sizeSql?: string, // サイズ計算SQL式(デフォルト: "3")
|
|
68
|
+
colorSql?: string, // 色計算SQL式(ARGB 32bit整数、デフォルト: "0x4D4D4DCC")
|
|
69
|
+
whereConditions?: WhereCondition[], // フィルタ条件(CPU側)
|
|
70
|
+
gpuFilterColumns?: string[], // GPUフィルタリング用カラム名(最大4つ)
|
|
71
|
+
gpuWhereConditions?: GpuWhereCondition[], // GPUフィルター条件
|
|
72
|
+
},
|
|
73
|
+
gpu?: {
|
|
74
|
+
backgroundColor?: ColorRGBA, // 背景色
|
|
75
|
+
pointAlpha?: number, // グローバル透明度(0.0-1.0、デフォルト: 1.0)
|
|
76
|
+
pointSizeScale?: number, // グローバルサイズスケール(デフォルト: 1.0)
|
|
77
|
+
},
|
|
78
|
+
labels?: {
|
|
79
|
+
url?: string, // GeoJSONファイルのURL
|
|
80
|
+
fontSize?: number, // フォントサイズ(デフォルト: 12)
|
|
81
|
+
filterLambda?: LabelFilterLambda, // ラベル表示フィルタ
|
|
82
|
+
onClick?: (label: Label) => void, // クリックコールバック
|
|
83
|
+
hoverOutlineOptions?: HoverOutlineOptions, // ホバーアウトライン設定
|
|
84
|
+
},
|
|
85
|
+
interaction?: {
|
|
86
|
+
onPointHover?: PointHoverCallback, // ポイントホバーコールバック
|
|
87
|
+
onLabelHover?: LabelHoverCallback, // ラベルホバーコールバック
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await plot.initialize();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
主要メソッド:
|
|
95
|
+
|
|
96
|
+
* `render()`: 描画
|
|
97
|
+
* `resize(width, height)`: キャンバスリサイズ
|
|
98
|
+
* `setZoom(zoom)` / `getZoom()` / `zoomIn()` / `zoomOut()`: ズーム操作
|
|
99
|
+
* `zoomToPoint(newZoom, screenX, screenY)`: 指定座標を中心にズーム
|
|
100
|
+
* `setPan(x, y)` / `getPan()` / `pan(dx, dy)`: パン操作
|
|
101
|
+
* `resetView()`: ビューリセット
|
|
102
|
+
* `update(options)`: オプション更新
|
|
103
|
+
* `runQuery(sql)`: カスタムSQLクエリ実行
|
|
104
|
+
* `getLabels()`: ラベル全件取得
|
|
105
|
+
* `destroy()`: リソース解放
|
|
106
|
+
|
|
107
|
+
**ポイント表示属性制御:**
|
|
108
|
+
|
|
109
|
+
* `setPointAlpha(alpha)`: グローバル透明度を設定(0.0-1.0)
|
|
110
|
+
* `getPointAlpha()`: 現在のグローバル透明度を取得
|
|
111
|
+
* `setPointSizeScale(scale)`: グローバルサイズスケールを設定
|
|
112
|
+
* `getPointSizeScale()`: 現在のグローバルサイズスケールを取得
|
|
113
|
+
|
|
114
|
+
**ホバー制御API:**
|
|
115
|
+
|
|
116
|
+
外部コンポーネントからプログラム的にホバー状態を制御できます。
|
|
117
|
+
|
|
118
|
+
* `setPointHover(pointId)`: ポイントをホバー状態に(`Promise<boolean>`)
|
|
119
|
+
* `clearPointHover()`: ポイントホバー解除
|
|
120
|
+
* `getHoveredPoint()`: ホバー中のポイント取得
|
|
121
|
+
* `setLabelHover(identifier)`: ラベルをホバー状態に(`boolean`を返す)
|
|
122
|
+
* `clearLabelHover()`: ラベルホバー解除
|
|
123
|
+
* `getHoveredLabel()`: ホバー中のラベル取得
|
|
124
|
+
* `clearAllHover()`: 全ホバー解除
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// 使用例
|
|
128
|
+
await plot.setPointHover(12345); // IDでポイントをホバー
|
|
129
|
+
plot.setLabelHover({ text: 'Cluster A' }); // テキストでラベルをホバー
|
|
130
|
+
plot.setLabelHover({ cluster: 5 }); // クラスタ番号でラベルをホバー
|
|
131
|
+
plot.clearAllHover(); // 全ホバー解除
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## GPUフィルタリング
|
|
135
|
+
|
|
136
|
+
GPUフィルタリングを使用すると、数値カラムの範囲フィルタをGPU側で高速に実行できます。データの再フェッチなしにリアルタイムでフィルタリングが可能です。
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const plot = new ScatterPlot({
|
|
140
|
+
// ...
|
|
141
|
+
data: {
|
|
142
|
+
idColumn: 'word',
|
|
143
|
+
// GPUフィルタリング用のカラムを指定(最大4つ)
|
|
144
|
+
gpuFilterColumns: ['frequency', 'length'],
|
|
145
|
+
// フィルター条件を指定
|
|
146
|
+
gpuWhereConditions: [
|
|
147
|
+
{ column: 'frequency', min: 100, max: 10000 },
|
|
148
|
+
{ column: 'length', min: 3 },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// 実行時にフィルター条件を更新
|
|
154
|
+
await plot.update({
|
|
155
|
+
data: {
|
|
156
|
+
gpuWhereConditions: [
|
|
157
|
+
{ column: 'frequency', min: 500 },
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**CPUフィルタ(`whereConditions`)との違い:**
|
|
164
|
+
|
|
165
|
+
| | CPUフィルタ | GPUフィルタ |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| **対応演算子** | 数値比較、文字列検索、生SQL | 範囲のみ(min/max) |
|
|
168
|
+
| **更新速度** | SQLクエリ再実行が必要 | 即座に反映 |
|
|
169
|
+
| **用途** | 複雑な条件、全文検索 | スライダーなどリアルタイム操作 |
|
|
170
|
+
|
|
171
|
+
## 型定義
|
|
172
|
+
|
|
173
|
+
主な型定義:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
// GPUフィルター条件
|
|
177
|
+
interface GpuWhereCondition {
|
|
178
|
+
column: string; // gpuFilterColumnsで指定したカラム名
|
|
179
|
+
min?: number; // 最小値(省略時: -Infinity)
|
|
180
|
+
max?: number; // 最大値(省略時: +Infinity)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ホバーアウトラインオプション
|
|
184
|
+
interface HoverOutlineOptions {
|
|
185
|
+
enabled?: boolean; // 有効化(デフォルト: true)
|
|
186
|
+
color?: string; // 線色(デフォルト: 白)
|
|
187
|
+
width?: number; // 線幅(ピクセル、デフォルト: 2)
|
|
188
|
+
minimumHoverSize?: number; // 最小ホバーサイズ
|
|
189
|
+
outlinedPointAddition?: number;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// WHERE条件フィルター
|
|
193
|
+
type WhereCondition = NumericFilter | StringFilter | RawSqlFilter;
|
|
194
|
+
|
|
195
|
+
interface NumericFilter {
|
|
196
|
+
type: 'numeric';
|
|
197
|
+
column: string;
|
|
198
|
+
operator: '>=' | '>' | '<=' | '<';
|
|
199
|
+
value: number;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
interface StringFilter {
|
|
203
|
+
type: 'string';
|
|
204
|
+
column: string;
|
|
205
|
+
operator: 'contains' | 'equals' | 'startsWith' | 'endsWith';
|
|
206
|
+
value: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
interface RawSqlFilter {
|
|
210
|
+
type: 'raw';
|
|
211
|
+
sql: string;
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Examples
|
|
216
|
+
|
|
217
|
+
### 実行方法
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# ライブラリのビルド(ルートディレクトリで)
|
|
221
|
+
npm install
|
|
222
|
+
npm run build
|
|
223
|
+
|
|
224
|
+
# サンプルアプリの実行
|
|
225
|
+
cd examples/next
|
|
226
|
+
npm install
|
|
227
|
+
npm run dev
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
ブラウザで http://localhost:3000 を開きます。
|
|
231
|
+
|
|
232
|
+
### サンプルデータ
|
|
233
|
+
|
|
234
|
+
サンプルでは、GloVe 6B単語ベクトルをUMAPで2次元に投影したデータを使用しています(約40万単語)。
|
|
235
|
+
|
|
236
|
+
* データセット: https://huggingface.co/datasets/mt0rm0/glove.6B.50d.umap.2d
|
|
237
|
+
|
|
238
|
+
### ラベル生成
|
|
239
|
+
|
|
240
|
+
ラベルはDBSCANクラスタリングとOpenAI APIを使って生成できます:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
cd examples/next
|
|
244
|
+
export OPENAI_API_KEY="your-api-key"
|
|
245
|
+
python scripts/generate_labels.py
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## ライセンス
|
|
249
|
+
|
|
250
|
+
このライブラリは [MIT License](./LICENSE) の下でライセンスされています。
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { ParquetData } from './repository.js';
|
|
2
|
+
import type { WhereCondition, ScatterPlotError, PointId } from '../types.js';
|
|
3
|
+
import type { AllPointsData } from '../renderer/gpu-layer.js';
|
|
4
|
+
/**
|
|
5
|
+
* DataLayerの設定オプション
|
|
6
|
+
*/
|
|
7
|
+
export interface DataLayerOptions {
|
|
8
|
+
/** ポイントサイズを計算するSQL式 */
|
|
9
|
+
sizeSql?: string;
|
|
10
|
+
/** ポイント色を計算するSQL式(ARGB形式) */
|
|
11
|
+
colorSql?: string;
|
|
12
|
+
/** データフィルタリング用のWHERE条件 */
|
|
13
|
+
whereConditions?: WhereCondition[];
|
|
14
|
+
/** GPUでフィルタリングするカラム名(最大4つ) */
|
|
15
|
+
gpuFilterColumns?: string[];
|
|
16
|
+
/** ポイントを識別するためのカラム名 */
|
|
17
|
+
idColumn: string;
|
|
18
|
+
/** エラーをScatterPlotに通知するためのコールバック */
|
|
19
|
+
onError?: (error: ScatterPlotError) => void;
|
|
20
|
+
/** データ変更時に呼び出されるコールバック */
|
|
21
|
+
onDataChanged?: () => void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* データ取得とクエリ管理を担当するレイヤー
|
|
25
|
+
* 責務:
|
|
26
|
+
* - ParquetReaderを介したParquetデータの読み込みと管理
|
|
27
|
+
* - 全データをGPU用フォーマットに変換
|
|
28
|
+
* - データ変更の検知と通知
|
|
29
|
+
*/
|
|
30
|
+
export declare class DataLayer {
|
|
31
|
+
/** Parquetデータへのアクセスを提供するリポジトリ */
|
|
32
|
+
private repository;
|
|
33
|
+
/** ポイントサイズのSQL式 */
|
|
34
|
+
private sizeSql;
|
|
35
|
+
/** ポイント色のSQL式(ARGB形式: a=0.3, r=0.3, g=0.3, b=0.8) */
|
|
36
|
+
private colorSql;
|
|
37
|
+
/** フィルタリング用のWHERE条件 */
|
|
38
|
+
private whereConditions;
|
|
39
|
+
/** GPUでフィルタリングするカラム名 */
|
|
40
|
+
private gpuFilterColumns;
|
|
41
|
+
/** エラー通知用コールバック */
|
|
42
|
+
private onError?;
|
|
43
|
+
/** データ変更通知用コールバック */
|
|
44
|
+
private onDataChanged?;
|
|
45
|
+
/** 全ポイントデータのキャッシュ(ポイント検索用) */
|
|
46
|
+
private allPointsCache;
|
|
47
|
+
/** ポイント識別用のカラム名 */
|
|
48
|
+
private idColumn;
|
|
49
|
+
/**
|
|
50
|
+
* DataLayerインスタンスを作成する
|
|
51
|
+
* @param options 設定オプション
|
|
52
|
+
*/
|
|
53
|
+
constructor(options: DataLayerOptions);
|
|
54
|
+
/**
|
|
55
|
+
* データレイヤーを初期化し、データを読み込む
|
|
56
|
+
* @param dataUrl Parquetファイルのurl
|
|
57
|
+
* @returns 処理済みの全データ
|
|
58
|
+
*/
|
|
59
|
+
initialize(dataUrl: string): Promise<AllPointsData>;
|
|
60
|
+
/**
|
|
61
|
+
* GeoJSONラベルデータをDuckDBテーブルに読み込む
|
|
62
|
+
* @param geojson GeoJSON FeatureCollectionオブジェクト
|
|
63
|
+
*/
|
|
64
|
+
loadLabelData(geojson: any): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* 単一の条件からWHERE句文字列を構築する
|
|
67
|
+
* @param condition WHERE条件
|
|
68
|
+
* @returns SQL WHERE句の文字列
|
|
69
|
+
*/
|
|
70
|
+
private buildWhereClauseString;
|
|
71
|
+
/**
|
|
72
|
+
* 全データを読み込んでGPU用フォーマットに変換する
|
|
73
|
+
* @returns 処理済みの全データ
|
|
74
|
+
*/
|
|
75
|
+
loadAllPoints(): Promise<AllPointsData>;
|
|
76
|
+
/**
|
|
77
|
+
* GPUフィルターカラムデータを読み込む
|
|
78
|
+
* @returns フィルターカラムデータとカラム数、カラムが指定されていない場合はnull
|
|
79
|
+
*/
|
|
80
|
+
loadGpuFilterColumns(): Promise<{
|
|
81
|
+
data: Float32Array;
|
|
82
|
+
columnCount: number;
|
|
83
|
+
columnMapping: Map<string, number>;
|
|
84
|
+
} | null>;
|
|
85
|
+
/**
|
|
86
|
+
* カスタムSQLクエリを実行する
|
|
87
|
+
* @param query SQLクエリ
|
|
88
|
+
* @returns クエリ結果のParquetData
|
|
89
|
+
*/
|
|
90
|
+
executeQuery(query: string | {
|
|
91
|
+
toString: () => string;
|
|
92
|
+
}): Promise<ParquetData | undefined>;
|
|
93
|
+
/**
|
|
94
|
+
* カラム形式のデータをGPU用インスタンスデータフォーマットに変換する
|
|
95
|
+
* フォーマット: ポイントごとに [x (f32), y (f32), color (u32), size (f32)]
|
|
96
|
+
* @param data ParquetData形式のデータ
|
|
97
|
+
* @returns 処理済みデータ
|
|
98
|
+
*/
|
|
99
|
+
private processDataToGpuFormat;
|
|
100
|
+
/**
|
|
101
|
+
* 設定オプションを更新する
|
|
102
|
+
* @param options 更新する設定オプション
|
|
103
|
+
* @returns GPUフィルターカラムが変更された場合はtrue
|
|
104
|
+
*/
|
|
105
|
+
updateOptions(options: Partial<DataLayerOptions>): {
|
|
106
|
+
gpuFilterColumnsChanged: boolean;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* 行データからポイントの色を取得する
|
|
110
|
+
* @param row 行データ
|
|
111
|
+
* @param columns カラム名の配列
|
|
112
|
+
* @returns RGBAカラーオブジェクト
|
|
113
|
+
*/
|
|
114
|
+
getPointColor(row: any[], columns: string[]): {
|
|
115
|
+
r: number;
|
|
116
|
+
g: number;
|
|
117
|
+
b: number;
|
|
118
|
+
a: number;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* 行データからポイントのサイズを取得する
|
|
122
|
+
* @param row 行データ
|
|
123
|
+
* @param columns カラム名の配列
|
|
124
|
+
* @returns ポイントサイズ
|
|
125
|
+
*/
|
|
126
|
+
getPointSize(row: any[], columns: string[]): number;
|
|
127
|
+
/**
|
|
128
|
+
* 画面座標に最も近いポイントを検索する
|
|
129
|
+
* @param screenX マウスのスクリーンX座標
|
|
130
|
+
* @param screenY マウスのスクリーンY座標
|
|
131
|
+
* @param canvasWidth キャンバスの幅(ピクセル)
|
|
132
|
+
* @param canvasHeight キャンバスの高さ(ピクセル)
|
|
133
|
+
* @param zoom 現在のズームレベル
|
|
134
|
+
* @param panX 現在のパンX
|
|
135
|
+
* @param panY 現在のパンY
|
|
136
|
+
* @param aspectRatio キャンバスのアスペクト比
|
|
137
|
+
* @param thresholdPixels ヒットと見なす最大距離(ピクセル、デフォルト: 10)
|
|
138
|
+
* @returns 見つかった場合はポイントデータ、そうでない場合はnull
|
|
139
|
+
*/
|
|
140
|
+
findNearestPoint(screenX: number, screenY: number, canvasWidth: number, canvasHeight: number, zoom: number, panX: number, panY: number, aspectRatio: number, thresholdPixels?: number): Promise<{
|
|
141
|
+
row: any[];
|
|
142
|
+
columns: string[];
|
|
143
|
+
} | null>;
|
|
144
|
+
/**
|
|
145
|
+
* データレイヤーが初期化されているかチェックする
|
|
146
|
+
* @returns 初期化されていればtrue
|
|
147
|
+
*/
|
|
148
|
+
isInitialized(): boolean;
|
|
149
|
+
/**
|
|
150
|
+
* IDでポイントを検索する
|
|
151
|
+
* @param pointId 検索するポイントのidColumn値
|
|
152
|
+
* @returns 見つかった場合はポイントデータ、そうでない場合はnull
|
|
153
|
+
*/
|
|
154
|
+
findPointById(pointId: PointId): Promise<{
|
|
155
|
+
row: any[];
|
|
156
|
+
columns: string[];
|
|
157
|
+
} | null>;
|
|
158
|
+
/**
|
|
159
|
+
* ParquetDataから指定行のデータを配列として構築する
|
|
160
|
+
* @param data ParquetData
|
|
161
|
+
* @param rowIndex 行インデックス
|
|
162
|
+
* @returns 行データの配列
|
|
163
|
+
*/
|
|
164
|
+
private buildRowFromData;
|
|
165
|
+
/**
|
|
166
|
+
* リソースをクリーンアップする
|
|
167
|
+
*/
|
|
168
|
+
destroy(): Promise<void>;
|
|
169
|
+
}
|