Closed12

100日チャレンジ day12 (GUIベースのインタラクティブ・データ探索ツール)

riddle_tecriddle_tec

昨日
https://zenn.dev/gin_nazo/scraps/d09bcb20a672eb


https://blog.framinal.life/entry/2025/04/14/154104

100日チャレンジに感化されたので、アレンジして自分でもやってみます。

やりたいこと

  • 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
  • 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
  • 生成aiをつかったバイブコーティングになれる
  • 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)

できたもの

https://github.com/lirlia/100day_challenge_backend/tree/main/day12_interactive_data_explorer

riddle_tecriddle_tec

DuckDBがきになってたので GUIベースのインタラクティブ・データ探索ツール を作ってみる。

※データは csv からインポートして、GUIでの選択に応じてクエリを組み立て叩く感じ

riddle_tecriddle_tec

はい、承知いたしました。「GUIベースのインタラクティブ・データ探索ツール」の仕様書を作成します。


Day 12: GUIベースのインタラクティブ・データ探索ツール 仕様書

1. アプリ概要

本アプリケーションは、ユーザーが事前に用意されたデータセット(CSV/Parquet形式)を選択し、Webブラウザ上のグラフィカルユーザーインターフェース(GUI)を通じてデータのフィルタリングや集計条件を指定し、その結果をテーブルおよび簡単なグラフ形式でインタラクティブに探索できるツールです。バックエンドでDuckDBを利用し、SQLを直接書かずにデータ分析の初歩を体験できることを目的とします。

2. 機能要件

  • FR-01: データセット選択機能
    • ユーザーは、アプリケーションに事前に登録されているデータセット(例: 世界の国々.csv, 映画データ.parquet)をドロップダウンリストから選択できる。
  • FR-02: カラム情報取得・表示機能
    • データセットを選択すると、そのデータセットに含まれるカラム名とそのデータ型(数値、文字列など簡易的な分類)をバックエンドから取得し、フィルターや集計の設定UIで利用できるようにする。
  • FR-03: GUIによるフィルタリング機能
    • ユーザーは、GUIコンポーネント(ドロップダウン、入力フィールド、スライダー等)を使って、データに対するフィルタリング条件を複数指定できる。
    • 数値カラムに対しては、比較演算子(>, <, =, >=, <=, !=)と値を指定できる。
    • 文字列(カテゴリ)カラムに対しては、一致する値(=, !=)や複数選択(IN)を指定できる。選択肢はデータのユニーク値から動的に取得して表示する。
  • FR-04: GUIによる集計機能
    • ユーザーは、GUIコンポーネントを使って、データをグループ化するカラム(複数選択可)と、集計を行うカラムおよび集計関数(COUNT, SUM, AVG, MIN, MAX)を指定できる。
    • 集計を行わない(生のデータをフィルタリングして表示する)選択も可能とする。
  • FR-05: 動的なSQL生成(バックエンド)
    • ユーザーがGUIで指定したフィルター条件および集計条件に基づき、バックエンドでDuckDBで実行可能なSQLクエリ(主にSELECT文)を動的に生成する。
  • FR-06: クエリ実行と結果表示(テーブル)
    • ユーザーが「更新」ボタンなどをクリックすると、生成されたSQLクエリがバックエンドのDuckDBで実行される。
    • 実行結果はテーブル形式で画面に表示される。
  • FR-07: 結果表示(グラフ)
    • テーブル表示に加え、集計結果などを簡単なグラフ(例: 棒グラフ、折れ線グラフ)で表示する。
    • 表示するグラフの種類や対象データは、集計内容に応じて自動選択されるか、ユーザーが選択できるようにする(今回は自動選択を基本とする)。
  • FR-08: エラーハンドリング
    • 不正な操作やデータ、クエリ実行時エラーが発生した場合、ユーザーにわかりやすいエラーメッセージを表示する。

3. 画面構成 (UI)

単一ページで構成される。

  • データセット選択エリア:
    • 利用可能なデータセットを選択するドロップダウンリスト。
  • 設定エリア (左ペインなど):
    • フィルターセクション:
      • 「フィルター追加」ボタン。
      • 追加された各フィルター条件の設定(カラム選択ドロップダウン、演算子選択ドロップダウン、値入力/選択コンポーネント)。
      • フィルター削除ボタン。
    • 集計セクション:
      • 「グループ化するカラム」選択(マルチセレクト)。
      • 「集計する値」設定エリア:
        • 「集計追加」ボタン。
        • 追加された各集計の設定(カラム選択ドロップダウン、集計関数選択ドロップダウン)。
        • 集計削除ボタン。
  • コントロールエリア:
    • 設定を適用して結果を更新する「表示更新」ボタン。
  • 結果表示エリア (右ペインなど):
    • グラフ表示セクション: Chart.js などで描画されたグラフ領域。
    • テーブル表示セクション: 結果データを示すテーブル (<table>)。

4. APIエンドポイント

  • GET /api/datasets
    • 役割: 利用可能なデータセットの一覧(ファイルパスと表示名)を返す。
    • レスポンス: { datasets: [{ id: 'countries', name: '世界の国々', path: 'data/countries.csv' }, ...] }
  • GET /api/columns?datasetId={datasetId}
    • 役割: 指定されたデータセットのカラム名、データ型、カテゴリカルカラムの場合はユニーク値のリストを返す。
    • リクエストパラメータ: datasetId (利用するデータセットの識別子)。
    • レスポンス: { columns: [{ name: 'country', type: 'string', distinctValues: [...] }, { name: 'population', type: 'number' }, ...] }
  • POST /api/query
    • 役割: フロントエンドのGUIから設定された条件(フィルター、集計)を受け取り、DuckDBでクエリを実行して結果(テーブルデータ、グラフ用データ)を返す。
    • リクエストボディ:
      {
        "datasetId": "countries",
        "filters": [
          { "column": "continent", "operator": "=", "value": "Asia" },
          { "column": "population", "operator": ">=", "value": 100000000 }
        ],
        "groups": ["continent"],
        "aggregations": [
          { "column": "population", "function": "SUM", "alias": "total_population" },
          { "column": "*", "function": "COUNT", "alias": "country_count" }
        ]
      }
      
    • レスポンス (成功時): 200 OK
      • Content-Type: application/json
      • ボディ:
        {
          "tableData": {
            "columns": ["continent", "total_population", "country_count"],
            "rows": [["Asia", 5000000000, 50], ...]
          },
          "chartData": { // Chart.js などが受け取れる形式
            "type": "bar",
            "labels": ["Asia", ...],
            "datasets": [
              { "label": "総人口", "data": [5000000000, ...] },
              { "label": "国数", "data": [50, ...] }
            ]
          }
        }
        
    • レスポンス (エラー時): 400/500 エラーコードと { "error": "エラーメッセージ" }

5. データフロー

  1. ユーザーがデータセットを選択する。
  2. フロントエンドは /api/columns を呼び出し、カラム情報を取得してGUIに反映する。
  3. ユーザーがGUIでフィルターや集計条件を設定する。
  4. ユーザーが「表示更新」ボタンをクリックする。
  5. フロントエンドは現在のGUI設定をJSONオブジェクトにまとめ、/api/query にPOSTリクエストを送信する。
  6. バックエンド (Route Handler) はリクエストを受け取る。
  7. 受け取った設定オブジェクトから、安全にDuckDB用のSQLクエリ文字列を生成する。
  8. DuckDBを起動し、対象データセットに対して生成したSQLを実行する。
  9. 実行結果を取得し、テーブル表示用データとグラフ表示用データに整形する。
  10. 整形したデータをJSON形式でフロントエンドに返す。エラーの場合はエラー情報を返す。
  11. フロントエンドはレスポンスを受け取り、成功ならテーブルとグラフを更新、エラーならメッセージを表示する。

6. 技術スタック

  • フレームワーク: Next.js (App Router)
  • 言語: TypeScript
  • データ処理エンジン: DuckDB (Node.jsライブラリ)
  • グラフ描画: Chart.js (react-chartjs-2)
  • スタイリング: Tailwind CSS
  • パッケージ管理: npm

7. 非機能要件・考慮事項

  • NFR-01: 事前データ: サーバー側にいくつかのCSV/Parquet形式のサンプルデータを用意しておく (/data ディレクトリなど)。
  • NFR-02: パフォーマンス: データセットのサイズは数MB〜数十MB程度を想定。巨大なデータに対する最適化は考慮しない。カラムのユニーク値取得やクエリ実行が低速にならない範囲で利用する。
  • NFR-03: セキュリティ: バックエンドでのSQL動的生成は、ユーザー入力を直接埋め込まず、パラメータ化クエリに類似した安全な方法で行う。
  • NFR-04: グラフの種類: まずは基本的な棒グラフや折れ線グラフから対応する。
  • NFR-05: UIの洗練度: 高度なインタラクションや見た目の作り込みよりも、基本機能の実装を優先する。

この仕様で進めてよろしいでしょうか? データセットはどのようなものが良さそうか、具体的な候補もいくつか考えてみましょうか?

riddle_tecriddle_tec

承知いたしました。以下に「GUIベースのインタラクティブ・データ探索ツール」の実装に向けた作業計画を提案します。

Day 12: GUIベースのインタラクティブ・データ探索ツール - 作業計画

  1. [済] プロジェクト初期化・仕様決定:

    • day12_duckdb_explorer プロジェクト作成 (template コピー)。
    • package.jsonname 変更。
    • README.md 更新 (アプリ概要)。
    • 仕様書の策定と合意 (改訂版v2)。
  2. 環境設定 & 依存ライブラリ導入:

    • 必要な npm パッケージをインストール:
      • duckdb: DuckDB Node.js ライブラリ。
      • chart.js: グラフ描画ライブラリ本体。
      • react-chartjs-2: React 用 Chart.js ラッパー。
      • @types/duckdb (必要であれば)。
    • コマンド: cd day12_duckdb_explorer && npm install duckdb chart.js react-chartjs-2
  3. データセット準備:

    • 公開されているデータセットを探し、以下の3種類を準備する。
      • countries.csv: 世界の国々の統計データ。
      • games.csv: ビデオゲームの売上データ。
      • universities.csv: 世界の大学ランキングデータ。
    • データを選定し、CSV形式で取得する。
    • 文字コードがUTF-8であること、ヘッダー行が存在することを確認・調整する。
    • day12_duckdb_explorer/data/ ディレクトリを作成し、上記のCSVファイルを配置する。
  4. バックエンド API 実装 (app/api/.../route.ts):

    • (a) /api/datasets: 利用可能なデータセットの一覧(ID, 名前, パス)を返す GET エンドポイントを実装する。
    • (b) /api/columns: datasetId をクエリパラメータで受け取り、指定されたデータセットのカラム情報(名前, 型, ユニーク値リスト)を返す GET エンドポイントを実装する (DuckDB使用)。
    • (c) /api/query: リクエストボディで datasetId, filters, groups, aggregations を受け取る POST エンドポイントを実装する。
      • 受け取った情報から DuckDB 用の SQL クエリを動的に生成するロジックを作成する (SQLインジェクション対策考慮)。
      • 生成した SQL を DuckDB で実行する。
      • 結果を generatedSql, tableData, chartData の形式に整形して返す。
      • エラーハンドリングを実装する。
  5. フロントエンド UI 実装 (app/page.tsx, components/):

    • (a) 状態管理: メインページ (app/page.tsx) で、選択中のデータセットID、カラム情報、フィルター/集計設定、APIレスポンス(結果データ、ローディング状態、エラー)などを管理する useState, useEffect を実装する。
    • (b) データセット選択コンポーネント (components/DatasetSelector.tsx): /api/datasets から取得した情報でドロップダウンを生成し、選択を状態に反映する。
    • (c) 設定UIコンポーネント (components/FilterBuilder.tsx, components/AggregationBuilder.tsx):
      • /api/columns から取得したカラム情報に基づいて、フィルター条件や集計条件を設定するUI(ドロップダウン、入力、ボタンなど)を作成する。
      • ユーザーの操作に応じて状態を更新する。
    • (d) 結果表示コンポーネント (components/ResultDisplay.tsx):
      • APIから受け取った generatedSql を表示する。
      • tableData を使って結果テーブルを描画する。
      • chartDatareact-chartjs-2 に渡してグラフを描画する。
      • ローディング中やエラー発生時の表示を行う。
  6. 連携と動作確認:

    • フロントエンドの各コンポーネントとAPI呼び出しを連携させる。
    • データセット選択 → カラム情報取得 → 設定UI更新 の流れを確認する。
    • 設定入力 → 「表示更新」ボタン → /api/query 呼び出し → 結果表示更新 の流れを確認する。
    • いくつかのパターンでフィルタリング、グループ化、集計を行い、意図したSQLが生成され、正しい結果がテーブルとグラフに表示されることを確認する。
    • エラーケース(不正な設定など)でエラーメッセージが表示されることを確認する。
  7. スタイリングと仕上げ:

    • Tailwind CSS を使って、UI要素の配置や見た目を整える。
    • 不要なコードやコンソールログを削除する。
    • README.md に使い方などを追記する(任意)。

この計画に沿って、まずはステップ2の「環境設定 & 依存ライブラリ導入」から進めていきます。

riddle_tecriddle_tec

なんか今日は最初の api の疎通すらできないところであたふたしてる

riddle_tecriddle_tec

duckdb を動かすところで結構つまってる
next と turbopack、duckdb の組み合わせがよくないっぽい

riddle_tecriddle_tec

duckdb を webpack でコンパイルしようとするとめちゃくちゃエラーでるのでそれをしない方式となった

riddle_tecriddle_tec

ネイティブ実装が遅々としてすすまないので wasm 版を利用することにする

riddle_tecriddle_tec

今回 next で無理やりバックエンドで wasm-duck を使うという筋の悪いことをしたけど、こんなことしなくてもフロントエンドから直接みる方がいいので注意

このスクラップは4ヶ月前にクローズされました