Observable Plot

Observable HQが開発したモダンなデータビジュアライゼーションライブラリ。Grammar of Graphics概念に基づき、宣言的なチャート作成が可能。D3.jsの思想を受け継ぎつつ、より簡潔な記述を実現。

JavaScriptデータ可視化静的サイトジェネレータダッシュボードインタラクティブD3.jsリアクティブ

フレームワーク

Observable Framework

概要

Observable Frameworkは、データアプリケーション、ダッシュボード、レポートを作成するための静的サイトジェネレーターです。

詳細

Observable Framework(オブザーバブル フレームワーク)は、Observable社が開発したデータ駆動型アプリケーションに特化した静的サイトジェネレーターです。フロントエンドのJavaScriptと任意のバックエンド言語を組み合わせて、高性能なデータ可視化とインタラクティブなダッシュボードを構築できます。リアクティブプログラミングモデルを採用し、データの変更に応じて自動的にビジュアライゼーションが更新される仕組みを提供します。データローダーによってビルド時にデータの静的スナップショットを生成するため、瞬時に読み込まれるダッシュボードを実現します。Observable Plot、D3.js、Mosaic、Vega-Lite、DuckDBなどの強力なライブラリと統合されており、大規模データセットの処理と可視化に対応しています。MarkdownとJavaScriptを組み合わせた直感的な記述方法で、技術的なレポートから複雑なビジネスインテリジェンスアプリケーションまで幅広く活用できます。

メリット・デメリット

メリット

  • 高性能: データローダーによる事前計算で瞬時に読み込まれるダッシュボード
  • リアクティブ: スプレッドシートライクな自動更新で状態管理が簡単
  • ポリグロット対応: JavaScript、Python、R、SQLなど複数言語をサポート
  • 豊富な可視化ライブラリ: Plot、D3、Vega-Lite、Mermaidなど多数の統合ライブラリ
  • 大規模データ対応: DuckDBによる効率的なクライアントサイドデータ処理
  • 直感的な記述: MarkdownとJavaScriptの組み合わせで簡単開発
  • ライブプレビュー: リアルタイムでの変更確認とホットリロード

デメリット

  • 学習コスト: Observable特有のリアクティブモデルの習得が必要
  • 静的サイト制約: ビルド時にデータが固定されるためリアルタイムデータに制限
  • エコシステム: React/Vueなどと比較して相対的に小さなコミュニティ
  • カスタマイズ制約: 特定のアーキテクチャに依存するため柔軟性に限界
  • デバッグの複雑さ: リアクティブな依存関係のデバッグが困難な場合がある

主要リンク

書き方の例

Hello World

// 基本的なMarkdown + JavaScriptの記述
display("Hello, Observable Framework!");

// HTMLエレメントの作成
display(html`<h1>Welcome to Observable!</h1>`);

// シンプルなデータ表示
const data = [1, 2, 3, 4, 5];
display(data);

基本的なデータ可視化

// データの準備
const salesData = [
  {month: "Jan", sales: 120},
  {month: "Feb", sales: 180},
  {month: "Mar", sales: 150},
  {month: "Apr", sales: 220},
  {month: "May", sales: 190}
];

// Observable Plotを使った基本的なバーチャート
Plot.plot({
  marks: [
    Plot.barY(salesData, {x: "month", y: "sales", fill: "steelblue"}),
    Plot.ruleY([0])
  ],
  y: {grid: true, label: "売上 (万円)"},
  title: "月別売上"
})

インタラクティブなチャート

// データの読み込み
const data = FileAttachment("sales.csv").csv({typed: true});

// インタラクティブな選択機能
const selectedCategory = view(
  Inputs.select(
    Array.from(new Set(data.map(d => d.category))), 
    {label: "カテゴリを選択"}
  )
);

// 選択に応じた動的チャート
Plot.plot({
  marks: [
    Plot.dot(
      data.filter(d => d.category === selectedCategory),
      {x: "date", y: "value", fill: "red", r: 4}
    ),
    Plot.lineY(
      data.filter(d => d.category === selectedCategory),
      {x: "date", y: "value", stroke: "blue"}
    )
  ],
  x: {type: "utc", label: "日付"},
  y: {grid: true, label: "値"},
  title: `${selectedCategory} のトレンド`
})

データの読み込みと処理

// CSVファイルの読み込み
const customers = FileAttachment("customers.csv").csv({typed: true});

// JSONデータの読み込み
const config = FileAttachment("config.json").json();

// データローダーからのデータ取得
const processedData = FileAttachment("data/processed.json").json();

// データの加工と表示
const summary = customers.then(data => 
  d3.rollup(
    data,
    v => v.length,
    d => d.region
  )
);

// テーブル形式での表示
Inputs.table(customers, {
  columns: ["name", "region", "revenue"],
  sort: "revenue",
  reverse: true
})

アニメーション

// 時間ベースのアニメーション
const currentTime = Generators.now(1000); // 1秒ごとに更新

// アニメーションするデータ
const animatedData = Array.from({length: 10}, (_, i) => ({
  x: i,
  y: Math.sin((currentTime / 1000 + i) * 0.5) * 50 + 100
}));

// アニメーションチャート
Plot.plot({
  marks: [
    Plot.lineY(animatedData, {x: "x", y: "y", stroke: "blue", strokeWidth: 3}),
    Plot.dot(animatedData, {x: "x", y: "y", fill: "red", r: 4})
  ],
  y: {domain: [50, 150]},
  title: "リアルタイムアニメーション"
})

複数のビューの連携

// 共有されるデータ
const salesData = FileAttachment("monthly-sales.csv").csv({typed: true});

// 地域選択コンポーネント
const selectedRegion = view(
  Inputs.select(
    ["全体", ...Array.from(new Set(salesData.map(d => d.region)))],
    {label: "地域選択", value: "全体"}
  )
);

// フィルタリングされたデータ
const filteredData = selectedRegion === "全体" 
  ? salesData 
  : salesData.filter(d => d.region === selectedRegion);

// トレンドチャート
const trendChart = Plot.plot({
  marks: [
    Plot.lineY(filteredData, {x: "month", y: "sales", stroke: "blue"}),
    Plot.dot(filteredData, {x: "month", y: "sales", fill: "red"})
  ],
  title: `${selectedRegion}の売上トレンド`
});

// サマリー表
const summaryTable = Inputs.table(
  d3.rollup(
    filteredData,
    v => ({
      totalSales: d3.sum(v, d => d.sales),
      avgSales: d3.mean(v, d => d.sales),
      count: v.length
    }),
    d => d.product
  ),
  {columns: ["product", "totalSales", "avgSales", "count"]}
);

// レイアウト
html`
  <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
    <div>${trendChart}</div>
    <div>${summaryTable}</div>
  </div>
`