mcpField
D
dashboard

dashboard

github.com/starchild-ai-agentデザイン・UI2988 回インストール1 日前に更新
リポジトリ

dashboard について

dashboardは、HTML、CSS、JavaScriptでデータダッシュボードを作るスキルです。データソースの調査、チャート選定、レスポンシブな画面構成、SSEやWebSocketによる更新を扱います。

主な機能

  1. 01データソースの調査と評価
  2. 02レスポンシブなHTML/CSS/JSダッシュボード生成
  3. 03チャートライブラリの統合
  4. 04SSEまたはWebSocketによる更新
  5. 05アクセシビリティとデザイン指針の適用

活用シーン

  1. 01公開APIを使ったダッシュボードの構築
  2. 02頻繁に更新される指標の表示
  3. 03ダッシュボード用チャートの選定

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/starchild-ai-agent/official-skills --skill dashboard

インストール後、Claude Code で/dashboardを入力して読み込まれたか確認します。

詳細

ダッシュボード作成スキル

このスキルでは、データ可視化、リアルタイム更新、無料データソースとの連携を備えたプロフェッショナルな HTML/CSS/JavaScript ダッシュボードを作成する方法を学びます。2025~2026 年のベストプラクティスとモダンなアーキテクチャパターンに基づいています。

主な機能

  1. データソースの探索 - ダッシュボード用データの無料 API を調査して推奨します
  2. ダッシュボードの生成 - レスポンシブな HTML/CSS/JS ダッシュボードを作成します
  3. 可視化の統合 - モダンなライブラリを使用してチャートを実装します
  4. リアルタイム更新 - SSE または WebSocket によるライブデータストリーミングを追加します
  5. ベストプラクティス - 2026 年の UX/UI およびアクセシビリティ標準に準拠します

1. データソースの探索

ユーザーがダッシュボード用のデータソースを必要としている場合は、適切な無料の公開 API を見つける手助けをします。

Star Child のデータソースを探索する ⭐ まずこちらを確認してください

外部 API を検索する前に、Star Child がすでに何を持っているかを探索してください。推測せず、確認してください。

利用可能なデータソースを探索する方法:

# 1. Check which APIs Star Child supports
read_file core/http_client.py | grep -A 15 "DEFAULT_PROXIED_APIS"

# 2. Check environment variables for configured keys
bash env | grep -i "api_key\|_key"

core/http_client.py で探すもの:

  • DEFAULT_PROXIED_APIS - どの API がプロキシ対応かを示します
  • DOMAIN_TO_API_TYPE - 実際のドメイン/エンドポイントを示します

一般的な Star Child のデータソース(本執筆時点のものですが、必ず確認してください):

  • coingecko - 暗号資産市場データ(Bitcoin、Ethereum など)
  • twelvedata - 株式、為替、コモディティ(AAPL、EUR/USD、XAU/USD)
  • taapi - テクニカル分析インジケーター(RSI、MACD など)
  • coinglass - デリバティブ/オプションデータ
  • lunarcrush - ソーシャルセンチメントデータ
  • twitterapi - Twitter/X データ
  • birdeye - スマートマネーおよびウォレット分析
  • oneinch - DEX アグリゲーター

前提をハードコードしないでください。 上記のリストは古くなっている可能性があります。実際に何が設定されているかを確認するには、常に core/http_client.py をチェックしてください。

API がリストに載っている場合:

  1. 対応する *_API_KEY 環境変数が存在するかを確認します
  2. web_fetch を使用して API ドキュメントを確認します(「{api_name} API documentation」で検索)
  3. これらの API は Star Child のプロキシ経由でルーティングされます - 最初はレート制限を気にせずに直接使用できます

例: 暗号資産データソースを見つける

# Step 1: Check what's available
read_file core/http_client.py | grep -i "coingecko\|crypto"

# Step 2: Check if key is configured
bash env | grep COINGECKO_API_KEY

# Step 3: If found, fetch docs to understand endpoints
web_fetch "https://docs.coingecko.com/reference/endpoint-overview"

プロキシの自動設定パターン:

// Add this at the top of your data fetching script
// (Only needed if running standalone outside the browser)
if (typeof process !== 'undefined' && process.env) {
  const proxyHost = process.env.PROXY_HOST;
  const proxyPort = process.env.PROXY_PORT || '8080';

  if (proxyHost && !process.env.HTTP_PROXY) {
    // Handle IPv6 addresses
    const host = proxyHost.includes(':') && !proxyHost.startsWith('[')
      ? `[${proxyHost}]`
      : proxyHost;
    const proxyUrl = `http://${host}:${proxyPort}`;

    process.env.HTTP_PROXY = proxyUrl;
    process.env.HTTPS_PROXY = proxyUrl;
  }
}

ブラウザでの使用(Fetch API):

// CoinGecko - Bitcoin price
async function fetchBitcoinPrice() {
  const response = await fetch(
    'https://pro-api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd',
    {
      headers: {
        'x-cg-pro-api-key': 'YOUR_API_KEY'  // Get from env in production
      }
    }
  );
  const data = await response.json();
  return data.bitcoin.usd;
}

// Twelve Data - Apple stock
async function fetchAppleStock() {
  const response = await fetch(
    'https://api.twelvedata.com/price?symbol=AAPL&apikey=YOUR_API_KEY'
  );
  const data = await response.json();
  return data.price;
}
API 選択の決定ツリー

重要: Star Child のプレミアム API(CoinGecko、Twelve Data)にはレート制限があります。プレミアムと無料の代替手段のどちらを使用するかを判断するには、この決定ツリーを使用してください:

Step 1: Identify data domain
├─ Crypto (BTC, ETH, etc.)
├─ Stocks (AAPL, MSFT, etc.)
├─ Forex (EUR/USD, etc.)
├─ Commodities (Gold, Oil)
└─ Other (Weather, News, etc.)

Step 2: Determine update frequency
├─ High-frequency (< 5 min intervals)    → Research FREE alternatives
├─ Medium-frequency (5-15 min)           → Star Child premium OK
└─ Low-frequency (> 15 min)              → Star Child premium preferred

Step 3: Assess data requirements
├─ Simple current prices                 → FREE alternatives often sufficient
├─ Historical OHLC data                  → Star Child premium if available
├─ Real-time streaming                   → Research specialized APIs
└─ Multiple data points per asset        → Consider rate limit impact

Step 4: Decision
├─ Use Star Child premium if:
│   - Update frequency is medium/low
│   - Need historical OHLC data
│   - Need reliable, clean data
│   - Total API calls < rate limit
│
└─ Research FREE alternative if:
    - High-frequency updates
    - Simple data needs (current price only)
    - Premium quota exhausted
    - Multiple assets × high frequency = too many calls

レート制限の影響計算ツール:

Updates per hour = (60 / update_interval_minutes)
Assets to track = N
Daily API calls = Updates per hour × 24 × N

Example:
- 5 cryptocurrencies
- Update every 2 minutes
- Calls per hour: 60/2 = 30
- Daily calls: 30 × 24 × 5 = 3,600 calls/day

If Star Child premium limit is 1,000 calls/day → NEED FREE ALTERNATIVE
If Star Child premium limit is 10,000 calls/day → Premium OK

無料の代替手段を調査すべきタイミング:

  1. 算出された 1 日あたりの呼び出し回数がプレミアム API のクォータを超える場合
  2. ユーザーが明示的に無料/認証不要のソリューションを希望している場合
  3. ダッシュボードが公開向けの場合(多くのユーザー = リクエストの増加)
  4. プロトタイピング/テスト段階の場合(プレミアムクォータを節約)
  5. ユーザーが API キーを設定していない場合
無料 API のためのリサーチ哲学

プレミアム API が使えない場合(レート制限、高頻度、ユーザーの希望など)、あなたの仕事は「使えるもの」を見つけることです。チェックリストに従うのではなく、実際に問題を解決することです。

次のように考えてください:

ユーザーはデータを必要としています。あなたは、そのデータがどこに無料で存在するかを見つけ、それが十分に信頼できるか、そしてユーザーのユースケースで実際に機能するかどうかを判断する必要があります。まず計算してください — 1 日 3,600 回の呼び出しが必要なのに、「無料」API が 100 回しか許可していないなら、何も解決していません。

重要なこと:

  • レート制限と実際の使用量 - 制限を列挙するだけでなく、それが十分かどうかを計算してください
  • メンテナンス - 2022 年の完璧な API が更新されていなければ、時限爆弾です
  • CORS - これがなければブラウザダッシュボードは動きません
  • データ品質 - データが古かったり間違っていたりすれば、「無料」には何の意味もありません
  • 摩擦 - 認証なし > API キー > OAuth(ダッシュボードのユースケースの場合)

リサーチの深さは重要性に合わせるべきです:

  • 簡単なプロトタイプ? 良い選択肢が 1 つあれば十分です。
  • 本番ダッシュボード? 2〜3 の選択肢を見つけ、トレードオフを比較してください。
  • ユーザーが「最高のものを見つけて」と言った? 実際に作業を行ってください。

どこを見るべきか:

  • github.com/public-apis/public-apis(900 以上の API、まずこれを確認)
  • 類似の統合を使用している最近の GitHub リポジトリ
  • 2025〜2026 年の開発者記事(2022 年ではない)
  • API マーケットプレイスサイト(rapidapi、publicapis.io)

危険信号:

  • 最終コミットが 2 年以上前
  • レート制限が埋もれている、または不明確
  • クレジットカードを要求する「無料枠」
  • 特定の地域でしか動作しないのに、そのことに言及していない
  • GitHub の issue で CORS の問題が言及されている

信じるのではなく、テストする: ドキュメントを取得したり、エンドポイントをテストしたりできるなら、そうしてください。2023 年のブログ記事に基づいて推薦しないでください。

まとめ: リサーチファーストのアプローチ

ダッシュボードスキルが優先すること:

  1. 選ぶ前に計算する: 更新頻度とアセット数に基づいて、必要な API 呼び出しの総量を常に計算します
  2. 適切な場合はプレミアムを: 中・低頻度および履歴データには Star Child プレミアム API を使用します
  3. 必要な場合はリサーチを: 高頻度、認証なし、または特殊なニーズには深いリサーチを行います
  4. 選択肢を提示する: ユーザーに、明確なトレードオフを伴う 2〜4 のリサーチ済み選択肢を提示します
  5. 鮮度を検証する: 2025〜2026 年の情報源と活発なメンテナンスを優先します

エージェントが決してしてはならないこと:

  • レート制限への影響を先に計算せずに API を推薦すること
  • クォータを確認せずに、高頻度ダッシュボード用のプレミアム API を提案すること
  • 現在の状況をリサーチせずに、特定の無料 API を指定すること
  • 無料の代替手段が必要な場合に、リサーチのステップを省略すること
  • 非推奨またはメンテナンスされていない API を推薦すること

2. ダッシュボード生成のアプローチ

アプローチ A: テンプレートベース(迅速な開発に推奨)

事前に構築された HTML ダッシュボードテンプレートを基盤として使用します:

Tabler (github.com/tabler/tabler) ⭐ 推奨

  • Bootstrap 5.3 ベース、GitHub スター 40k 以上
  • 100 以上のレスポンシブコンポーネント、200 以上のサンプルページ
  • 5,400 以上の SVG アイコンを同梱
  • ダークモード内蔵
  • MIT ライセンス

AdminKit (github.com/adminkit/adminkit)

  • Bootstrap 5 ベース、モダンなビルドパイプライン
  • jQuery 依存なし
  • Webpack、Sass、BrowserSync
  • すべてのデバイスでレスポンシブ

実装手順:

# Option 1: Download template
curl -L https://github.com/tabler/tabler/archive/refs/heads/main.zip -o tabler.zip
unzip tabler.zip
cd tabler-main/dist

# Option 2: Use via CDN (faster for prototyping)
# See code example below

Tabler クイックスタート:

<!DOCTYPE html>
<html>
<head>
  <title>Dashboard</title>
  <link href="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css" rel="stylesheet"/>
</head>
<body>
  <div class="page">
    <div class="page-wrapper">
      <div class="page-header">
        <div class="container-xl">
          <h1>My Dashboard</h1>
        </div>
      </div>
      <div class="page-body">
        <div class="container-xl">
          <div class="row row-cards">
            
          </div>
        </div>
      </div>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/js/tabler.min.js"></script>
</body>
</html>
アプローチ B: カスタム HTML/CSS/JS(worldmonitor にインスパイア)

より細かい制御が必要な場合は、モジュール式のダッシュボードをゼロから構築します:

アーキテクチャパターン(worldmonitor にインスパイア):

dashboard/
├── index.html          # Main page structure
├── css/
│   ├── base.css       # Reset, typography, variables
│   ├── layout.css     # Grid, containers, responsive
│   └── components.css # Cards, charts, widgets
├── js/
│   ├── config.js      # API endpoints, settings
│   ├── data.js        # Data fetching & caching
│   ├── charts.js      # Visualization logic
│   └── main.js        # Initialization
└── assets/
    └── icons/         # SVG icons

コア HTML 構造:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Dashboard</title>
  <link rel="stylesheet" href="css/base.css">
  <link rel="stylesheet" href="css/layout.css">
  <link rel="stylesheet" href="css/components.css">
</head>
<body>
  <div class="dashboard">
    <aside class="sidebar">
      <nav class="nav">
        
      </nav>
    </aside>

    <main class="main-content">
      <header class="header">
        <h1 class="title">Dashboard</h1>
      </header>

      <div class="grid">
        
        <div class="card">
          <h2 class="card-title">Metrics</h2>
          <div class="card-body">
            <canvas id="chart1"></canvas>
          </div>
        </div>
      </div>
    </main>
  </div>

  <script src="js/config.js"></script>
  <script src="js/data.js"></script>
  <script src="js/charts.js"></script>
  <script src="js/main.js"></script>
</body>
</html>

レスポンシブ CSS グリッドレイアウト:

/* Mobile-first grid */
.grid {
  display: grid;
  gap: 1.5rem;
  padding: 1.5rem;
}

/* Tablet: 2 columns */
@media (min-width: 768px) {
  .grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* Desktop: 3 columns */
@media (min-width: 1024px) {
  .grid {
    grid-template-columns: repeat(3, 1fr);
  }

  .card.wide {
    grid-column: span 2;
  }

  .card.full {
    grid-column: span 3;
  }
}

3. 可視化ライブラリ

要件に基づいて適切なライブラリを選択します:

Chart.js ⭐ 推奨(デフォルトの選択肢)

最適な用途: シンプルなチャート、迅速な実装、優れたデフォルト設定 長所: 使いやすい、レスポンシブ、canvas ベース(高速)、充実したドキュメント 短所: D3.js と比べてカスタマイズ性が限られる


<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

<canvas id="myChart"></canvas>

<script>
const ctx = document.getElementById('myChart').getContext('2d');
const chart = new Chart(ctx, {
  type: 'line',
  data: {
    labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'],
    datasets: [{
      label: 'Sales',
      data: [12, 19, 3, 5, 2],
      borderColor: 'rgb(75, 192, 192)',
      tension: 0.1
    }]
  },
  options: {
    responsive: true,
    maintainAspectRatio: false
  }
});

// Update chart with new data
function updateChart(newData) {
  chart.data.datasets[0].data = newData;
  chart.update();
}
</script>
ApexCharts(美しいリアルタイムダッシュボード向け)

最適な用途: マーケティングダッシュボード、リアルタイムデータ、美しさ 長所: 美しいデフォルト設定、滑らかなアニメーション、リアルタイム更新、注釈 短所: API がやや複雑

<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>

<div id="chart"></div>

<script>
const options = {
  chart: {
    type: 'area',
    height: 350,
    animations: {
      enabled: true,
      dynamicAnimation: {
        speed: 1000
      }
    }
  },
  series: [{
    name: 'Value',
    data: [31, 40, 28, 51, 42, 109, 100]
  }],
  xaxis: {
    categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul']
  }
};

const chart = new ApexCharts(document.querySelector("#chart"), options);
chart.render();

// Real-time updates
function addDataPoint(value) {
  chart.appendData([{
    data: [value]
  }]);
}
</script>
D3.js(カスタム可視化向け)

最適な用途: カスタム可視化、完全なクリエイティブコントロール、複雑なデータ メリット: 最大限の柔軟性、強力なデータバインディング デメリット: 学習曲線が急、より多くのコードが必要

D3.js は次の場合にのみ使用してください:

  • ユーザーがカスタム/独自の可視化を明示的に要望した場合
  • 標準的なチャート種別(折れ線、棒、円)では不十分な場合
  • 複雑なインタラクションやアニメーションが必要な場合
<script src="https://d3js.org/d3.v7.min.js"></script>

<svg id="chart" width="600" height="400"></svg>

<script>
const data = [30, 86, 168, 281, 303, 365];

const svg = d3.select("#chart");
const margin = {top: 20, right: 20, bottom: 30, left: 40};
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;

const x = d3.scaleBand()
  .domain(d3.range(data.length))
  .range([0, width])
  .padding(0.1);

const y = d3.scaleLinear()
  .domain([0, d3.max(data)])
  .range([height, 0]);

const g = svg.append("g")
  .attr("transform", `translate(${margin.left},${margin.top})`);

g.selectAll(".bar")
  .data(data)
  .join("rect")
  .attr("class", "bar")
  .attr("x", (d, i) => x(i))
  .attr("y", d => y(d))
  .attr("width", x.bandwidth())
  .attr("height", d => height - y(d))
  .attr("fill", "steelblue");
</script>

ライブラリ選択ガイド:

User needs simple charts? → Chart.js
User needs beautiful real-time dashboard? → ApexCharts
User needs custom/unique visualizations? → D3.js
User unsure? → Chart.js (safest default)

4. リアルタイムデータ統合

SSE(Server-Sent Events) ⭐ 推奨

SSE を使う場面: サーバーからクライアントへのストリーミング(ダッシュボードのユースケースの95%) メリット: シンプル、自動再接続、HTTP 上で動作、TTFB が良好 デメリット: 単方向のみ(サーバー → クライアント)

2026年に SSE が選ばれる理由:

  • HTTP/3(QUIC)がヘッドオブラインブロッキングを排除
  • AI ストリーミング、ライブメトリクス、通知に最適
  • 一方向のデータフローでは WebSocket よりシンプル
  • ダッシュボードの更新に適している(クライアント → サーバーのメッセージが不要)

クライアント側の実装:

// Connect to SSE endpoint
const eventSource = new EventSource('/api/dashboard-updates');

// Listen for updates
eventSource.addEventListener('metric-update', (event) => {
  const data = JSON.parse(event.data);
  updateChart(data);
});

eventSource.addEventListener('error', (error) => {
  console.error('SSE error:', error);
  // Auto-reconnects by default
});

// Close connection when done
function cleanup() {
  eventSource.close();
}

サーバー側(バックエンドを構築する場合):

# FastAPI example
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
import json

app = FastAPI()

async def event_stream():
    while True:
        # Fetch latest data
        data = get_dashboard_metrics()

        # SSE format: "event: name\ndata: {json}\n\n"
        yield f"event: metric-update\ndata: {json.dumps(data)}\n\n"

        await asyncio.sleep(5)  # Update every 5 seconds

@app.get("/api/dashboard-updates")
async def stream_updates():
    return StreamingResponse(
        event_stream(),
        media_type="text/event-stream"
    )
WebSocket(双方向通信向け)

WebSocket を使う場面:

  • ユーザーがサーバーへデータを送信する必要がある場合(チャット、共同編集)
  • サブ秒のレイテンシが必要な場合(トレーディングプラットフォーム、マルチプレイヤーゲーム)
  • 真の双方向通信が必要な場合

クライアント側:

const ws = new WebSocket('wss://api.example.com/ws');

ws.onopen = () => {
  console.log('Connected');
  ws.send(JSON.stringify({ type: 'subscribe', channel: 'metrics' }));
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  updateChart(data);
};

ws.onerror = (error) => {
  console.error('WebSocket error:', error);
};

ws.onclose = () => {
  console.log('Disconnected');
  // Implement reconnection logic
  setTimeout(connectWebSocket, 5000);
};
ポーリング(フォールバック)

ポーリングを使う場面:

  • SSE/WebSocket が利用できない場合
  • 更新頻度が低いシンプルなユースケース
  • 最大限の互換性が必要な場合
async function pollData() {
  try {
    const response = await fetch('/api/metrics');
    const data = await response.json();
    updateChart(data);
  } catch (error) {
    console.error('Polling error:', error);
  }
}

// Poll every 10 seconds
setInterval(pollData, 10000);
pollData(); // Initial load

5. ダッシュボード設計のベストプラクティス(2026)

視覚的階層

優先的な配置:

  • 最も重要なデータ: 左上(自然な視線の流れ)
  • 二次的なメトリクス: 右上
  • 詳細なチャート: 中央および下部
  • アクション/フィルター: 上部またはサイドバー

レイアウト例:

<div class="grid">
  
  <div class="card full">
    <div class="metrics-row">
      <div class="metric">
        <span class="metric-label">Total Revenue</span>
        <span class="metric-value">$142,592</span>
        <span class="metric-change positive">+12.5%</span>
      </div>
      
    </div>
  </div>

  
  <div class="card wide">
    <canvas id="revenue-chart"></canvas>
  </div>
  <div class="card">
    <canvas id="users-chart"></canvas>
  </div>

  
  <div class="card full">
    <table class="data-table">
      
    </table>
  </div>
</div>
モバイルファーストのレスポンシブデザイン

アプローチ:

  1. まずモバイル(320px)向けに設計
  2. タブレット(768px以上)向けにプログレッシブエンハンスメント
  3. デスクトップレイアウト(1024px以上)

主要なパターン:

/* Mobile: Stack vertically */
.metrics-row {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

/* Tablet: 2 columns */
@media (min-width: 768px) {
  .metrics-row {
    flex-direction: row;
    flex-wrap: wrap;
  }

  .metric {
    flex: 1 1 calc(50% - 0.5rem);
  }
}

/* Desktop: 4 columns */
@media (min-width: 1024px) {
  .metric {
    flex: 1 1 calc(25% - 0.75rem);
  }
}

/* Touch-friendly controls (min 44x44px) */
.button {
  min-height: 44px;
  min-width: 44px;
  padding: 0.75rem 1.5rem;
}
アクセシビリティ

必須の実践:

  • 色覚多様性に安全なパレット(パターン + 色を使用)
  • スクリーンリーダー向けの ARIA ラベル
  • キーボードナビゲーションのサポート
  • 十分なコントラスト比(最低限 WCAG AA)

<div class="chart-container" role="img" aria-label="Revenue trend showing 12% growth over last quarter">
  <canvas id="chart"></canvas>

  
  <table class="sr-only">
    <caption>Revenue Data</caption>
    <thead>
      <tr>
        <th>Month</th>
        <th>Revenue</th>
      </tr>
    </thead>
    <tbody>
      <tr><td>January</td><td>$12,000</td></tr>
      
    </tbody>
  </table>
</div>
/* Screen reader only class */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border-width: 0;
}

/* Color-blind safe palette */
:root {
  --color-primary: #0066CC;    /* Blue */
  --color-success: #28A745;    /* Green */
  --color-warning: #FFC107;    /* Amber */
  --color-danger: #DC3545;     /* Red */
  --color-info: #17A2B8;       /* Cyan */
}
パフォーマンス最適化

重要な実践:

// 1. Debounce resize events
let resizeTimeout;
window.addEventListener('resize', () => {
  clearTimeout(resizeTimeout);
  resizeTimeout = setTimeout(() => {
    chart.resize();
  }, 250);
});

// 2. Limit data points for large datasets
function decimateData(data, maxPoints = 100) {
  if (data.length <= maxPoints) return data;

  const step = Math.ceil(data.length / maxPoints);
  return data.filter((_, i) => i % step === 0);
}

// 3. Use RequestAnimationFrame for smooth updates
function smoothUpdate(newValue) {
  requestAnimationFrame(() => {
    updateMetric(newValue);
  });
}

// 4. Lazy load charts (Intersection Observer)
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      initializeChart(entry.target);
      observer.unobserve(entry.target);
    }
  });
});

document.querySelectorAll('.chart-container').forEach(el => {
  observer.observe(el);
});
ダークモードのサポート
/* CSS Variables for theming */
:root {
  --bg-primary: #ffffff;
  --bg-secondary: #f8f9fa;
  --text-primary: #212529;
  --text-secondary: #6c757d;
  --border-color: #dee2e6;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg-primary: #1a1a1a;
    --bg-secondary: #2d2d2d;
    --text-primary: #e9ecef;
    --text-secondary: #adb5bd;
    --border-color: #495057;
  }
}

body {
  background-color: var(--bg-primary);
  color: var(--text-primary);
}

.card {
  background-color: var(--bg-secondary);
  border: 1px solid var(--border-color);
}

6. 完全なダッシュボードの例

以下は、すべてのベストプラクティスを組み合わせた本番環境対応のテンプレートです:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Live Dashboard</title>
  <link href="https://cdn.jsdelivr.net/npm/@tabler/core@latest/dist/css/tabler.min.css" rel="stylesheet"/>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <style>
    .metric-card {
      text-align: center;
      padding: 1.5rem;
    }
    .metric-value {
      font-size: 2.5rem;
      font-weight: bold;
      margin: 0.5rem 0;
    }
    .metric-change {
      font-size: 0.9rem;
      font-weight: 500;
    }
    .metric-change.positive { color: #28a745; }
    .metric-change.negative { color: #dc3545; }
    .chart-container {
      position: relative;
      height: 300px;
    }
  </style>
</head>
<body>
  <div class="page">
    <div class="page-wrapper">
      <div class="page-header">
        <div class="container-xl">
          <h1 class="page-title">Analytics Dashboard</h1>
        </div>
      </div>

      <div class="page-body">
        <div class="container-xl">
          
          <div class="row row-cards mb-3">
            <div class="col-sm-6 col-lg-3">
              <div class="card metric-card">
                <div class="metric-label text-muted">Total Users</div>
                <div class="metric-value" id="total-users">0</div>
                <div class="metric-change positive" id="users-change">+0%</div>
              </div>
            </div>
            <div class="col-sm-6 col-lg-3">
              <div class="card metric-card">
                <div class="metric-label text-muted">Revenue</div>
                <div class="metric-value" id="revenue">$0</div>
                <div class="metric-change positive" id="revenue-change">+0%</div>
              </div>
            </div>
            <div class="col-sm-6 col-lg-3">
              <div class="card metric-card">
                <div class="metric-label text-muted">Conversion</div>
                <div class="metric-value" id="conversion">0%</div>
                <div class="metric-change" id="conversion-change">+0%</div>
              </div>
            </div>
            <div class="col-sm-6 col-lg-3">
              <div class="card metric-card">
                <div class="metric-label text-muted">Active Now</div>
                <div class="metric-value" id="active">0</div>
                <div class="metric-change" id="active-change">--</div>
              </div>
            </div>
          </div>

          
          <div class="row row-cards">
            <div class="col-lg-8">
              <div class="card">
                <div class="card-header">
                  <h3 class="card-title">Revenue Trend</h3>
                </div>
                <div class="card-body">
                  <div class="chart-container">
                    <canvas id="revenueChart"></canvas>
                  </div>
                </div>
              </div>
            </div>
            <div class="col-lg-4">
              <div class="card">
                <div class="card-header">
                  <h3 class="card-title">Traffic Sources</h3>
                </div>
                <div class="card-body">
                  <div class="chart-container">
                    <canvas id="trafficChart"></canvas>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <script>
    // Chart configurations
    const revenueChart = new Chart(document.getElementById('revenueChart'), {
      type: 'line',
      data: {
        labels: [],
        datasets: [{
          label: 'Revenue',
          data: [],
          borderColor: '#0066CC',
          backgroundColor: 'rgba(0, 102, 204, 0.1)',
          tension: 0.4,
          fill: true
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: { display: false }
        },
        scales: {
          y: { beginAtZero: true }
        }
      }
    });

    const trafficChart = new Chart(document.getElementById('trafficChart'), {
      type: 'doughnut',
      data: {
        labels: ['Direct', 'Social', 'Search', 'Referral'],
        datasets: [{
          data: [30, 25, 35, 10],
          backgroundColor: ['#0066CC', '#28A745', '#FFC107', '#DC3545']
        }]
      },
      options: {
        responsive: true,
        maintainAspectRatio: false,
        plugins: {
          legend: { position: 'bottom' }
        }
      }
    });

    // Real-time updates with SSE
    const eventSource = new EventSource('/api/dashboard-updates');

    eventSource.addEventListener('metrics', (event) => {
      const data = JSON.parse(event.data);

      // Update metric cards
      document.getElementById('total-users').textContent = data.users.toLocaleString();
      document.getElementById('revenue').textContent = '$' + data.revenue.toLocaleString();
      document.getElementById('conversion').textContent = data.conversion + '%';
      document.getElementById('active').textContent = data.active.toLocaleString();

      // Update changes
      updateChange('users-change', data.usersChange);
      updateChange('revenue-change', data.revenueChange);
      updateChange('conversion-change', data.conversionChange);

      // Update charts
      revenueChart.data.labels.push(data.timestamp);
      revenueChart.data.datasets[0].data.push(data.revenue);

      // Keep only last 20 points
      if (revenueChart.data.labels.length > 20) {
        revenueChart.data.labels.shift();
        revenueChart.data.datasets[0].data.shift();
      }

      revenueChart.update('none'); // No animation for real-time
    });

    function updateChange(elementId, value) {
      const element = document.getElementById(elementId);
      element.textContent = (value >= 0 ? '+' : '') + value + '%';
      element.className = 'metric-change ' + (value >= 0 ? 'positive' : 'negative');
    }

    // Fallback to polling if SSE fails
    eventSource.onerror = () => {
      console.log('SSE failed, falling back to polling');
      eventSource.close();
      setInterval(fetchData, 10000);
    };

    async function fetchData() {
      try {
        const response = await fetch('/api/metrics');
        const data = await response.json();
        // Update dashboard with data
      } catch (error) {
        console.error('Fetch error:', error);
      }
    }
  </script>
</body>
</html>

7. ダッシュボード作成のワークフロー

ユーザーからダッシュボードの作成を依頼された場合は、次のワークフローに従ってください:

ステップ1: 要件の収集
Ask the user:
1. What data do you want to display? (metrics, trends, comparisons)
2. Do you have data sources already? (If no, proceed to discovery)
3. Real-time updates needed? (yes/no, update frequency)
4. Target devices? (desktop-only, mobile-first, both)
5. Preferred style? (minimal, professional, colorful)
ステップ2: データソースの探索(必要な場合)
1. Identify domain(s): finance, weather, social, etc.
2. Use web_search to find appropriate APIs
3. Recommend 2-3 options with examples
4. Document API endpoints and authentication
ステップ3: アーキテクチャの決定
Choose approach:
- **Template-based (Tabler/AdminKit)**: Fast, professional, best for standard dashboards
- **Custom HTML/CSS/JS**: More control, best for unique requirements
- **Hybrid**: Template + custom visualizations

Choose visualization library:
- **Chart.js**: Default choice for most use cases
- **ApexCharts**: If real-time + beautiful aesthetics required
- **D3.js**: Only if custom visualizations explicitly needed
ステップ4: 実装
1. Create HTML structure (responsive grid layout)
2. Add CSS (mobile-first, accessibility, dark mode)
3. Integrate chart library
4. Implement data fetching (API calls)
5. Add real-time updates (SSE preferred, polling fallback)
6. Test responsiveness and accessibility
ステップ5: ドキュメント化
Provide user with:
1. Installation instructions
2. API setup guide (if external APIs used)
3. Customization options
4. How to deploy (static host, backend requirements)

8. 一般的なパターンとレシピ

パターン: マルチソースダッシュボード

Star Child の組み込み API と外部ソースのデータを組み合わせる:

// config.js
const API_CONFIG = {
  coingecko: {
    baseUrl: 'https://pro-api.coingecko.com/api/v3',
    apiKey: process.env.COINGECKO_API_KEY || 'YOUR_API_KEY'
  },
  twelvedata: {
    baseUrl: 'https://api.twelvedata.com',
    apiKey: process.env.TWELVEDATA_API_KEY || 'YOUR_API_KEY'
  },
  openMeteo: {
    baseUrl: 'https://api.open-meteo.com/v1'
  }
};

// data.js
async function fetchAllData() {
  const results = await Promise.all([
    fetchCryptoPrices(),
    fetchStockPrices(),
    fetchWeather(),
    fetchNews()
  ]);

  return {
    crypto: results[0],
    stocks: results[1],
    weather: results[2],
    news: results[3],
    timestamp: new Date().toISOString()
  };
}

// CoinGecko - Multiple crypto prices
async function fetchCryptoPrices() {
  const response = await fetch(
    `${API_CONFIG.coingecko.baseUrl}/simple/price?ids=bitcoin,ethereum,solana&vs_currencies=usd&include_24hr_change=true`,
    {
      headers: {
        'x-cg-pro-api-key': API_CONFIG.coingecko.apiKey
      }
    }
  );

  if (!response.ok) {
    throw new Error(`CoinGecko API error: ${response.status}`);
  }

  return response.json();
}

// Twelve Data - Stock prices
async function fetchStockPrices() {
  const symbols = ['AAPL', 'MSFT', 'GOOGL'];
  const promises = symbols.map(symbol =>
    fetch(`${API_CONFIG.twelvedata.baseUrl}/price?symbol=${symbol}&apikey=${API_CONFIG.twelvedata.apiKey}`)
      .then(r => r.json())
      .then(data => ({ symbol, price: parseFloat(data.price) }))
  );

  const results = await Promise.all(promises);
  return results.reduce((acc, { symbol, price }) => {
    acc[symbol] = price;
    return acc;
  }, {});
}

// Open-Meteo - Weather (no auth required)
async function fetchWeather() {
  const params = new URLSearchParams({
    latitude: 52.52,
    longitude: 13.41,
    current_weather: true
  });

  const response = await fetch(`${API_CONFIG.openMeteo.baseUrl}/forecast?${params}`);
  return response.json();
}

// Hacker News - Tech news (no auth required)
async function fetchNews() {
  const response = await fetch('https://hn.algolia.com/api/v1/search?tags=front_page&hitsPerPage=5');
  return response.json();
}

// main.js
async function initializeDashboard() {
  try {
    const data = await fetchAllData();

    // Update crypto cards
    updateCryptoCard('bitcoin', data.crypto.bitcoin.usd, data.crypto.bitcoin.usd_24h_change);
    updateCryptoCard('ethereum', data.crypto.ethereum.usd, data.crypto.ethereum.usd_24h_change);
    updateCryptoCard('solana', data.crypto.solana.usd, data.crypto.solana.usd_24h_change);

    // Update stock cards
    updateStockCard('AAPL', data.stocks.AAPL);
    updateStockCard('MSFT', data.stocks.MSFT);
    updateStockCard('GOOGL', data.stocks.GOOGL);

    // Update weather widget
    updateWeatherWidget(data.weather);

    // Update news feed
    updateNewsFeed(data.news.hits);

  } catch (error) {
    console.error('Dashboard initialization failed:', error);
    showError('Failed to load dashboard data. Please try again.');
  }
}

function updateCryptoCard(coinId, price, change24h) {
  const card = document.getElementById(`crypto-${coinId}`);
  if (!card) return;

  card.querySelector('.price').textContent = '$' + price.toLocaleString(undefined, {
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  });

  const changeEl = card.querySelector('.change');
  const changeValue = change24h.toFixed(2);
  changeEl.textContent = (change24h >= 0 ? '+' : '') + changeValue + '%';
  changeEl.className = 'change ' + (change24h >= 0 ? 'positive' : 'negative');
}

function updateStockCard(symbol, price) {
  const card = document.getElementById(`stock-${symbol}`);
  if (!card) return;

  card.querySelector('.price').textContent = '$' + price.toFixed(2);
}

function updateWeatherWidget(weatherData) {
  const widget = document.getElementById('weather-widget');
  if (!widget) return;

  const temp = weatherData.current_weather.temperature;
  const windSpeed = weatherData.current_weather.windspeed;

  widget.querySelector('.temperature').textContent = temp + '°C';
  widget.querySelector('.wind').textContent = windSpeed + ' km/h';
}

function updateNewsFeed(newsItems) {
  const feed = document.getElementById('news-feed');
  if (!feed) return;

  feed.innerHTML = newsItems.map(item => `
    <div class="news-item">
      <a href="${item.url}" target="_blank">${item.title}</a>
      <span class="news-meta">${item.points} points • ${item.author}</span>
    </div>
  `).join('');
}

function showError(message) {
  const errorEl = document.getElementById('error-message');
  if (errorEl) {
    errorEl.textContent = message;
    errorEl.style.display = 'block';
    setTimeout(() => errorEl.style.display = 'none', 5000);
  }
}

// Initialize on page load
document.addEventListener('DOMContentLoaded', initializeDashboard);

// Auto-refresh every 5 minutes
setInterval(initializeDashboard, 5 * 60 * 1000);

マルチソースダッシュボードの HTML 構造:

<div class="dashboard-grid">
  
  <div class="section">
    <h2>Cryptocurrency</h2>
    <div class="cards">
      <div class="card" id="crypto-bitcoin">
        <h3>Bitcoin</h3>
        <div class="price">$0</div>
        <div class="change">0%</div>
      </div>
      <div class="card" id="crypto-ethereum">
        <h3>Ethereum</h3>
        <div class="price">$0</div>
        <div class="change">0%</div>
      </div>
      <div class="card" id="crypto-solana">
        <h3>Solana</h3>
        <div class="price">$0</div>
        <div class="change">0%</div>
      </div>
    </div>
  </div>

  
  <div class="section">
    <h2>Stock Market</h2>
    <div class="cards">
      <div class="card" id="stock-AAPL">
        <h3>Apple</h3>
        <div class="price">$0</div>
      </div>
      <div class="card" id="stock-MSFT">
        <h3>Microsoft</h3>
        <div class="price">$0</div>
      </div>
      <div class="card" id="stock-GOOGL">
        <h3>Google</h3>
        <div class="price">$0</div>
      </div>
    </div>
  </div>

  
  <div class="card" id="weather-widget">
    <h3>Weather</h3>
    <div class="temperature">--°C</div>
    <div class="wind">-- km/h</div>
  </div>

  
  <div class="section">
    <h2>Tech News</h2>
    <div id="news-feed"></div>
  </div>
</div>

<div id="error-message" class="error" style="display: none;"></div>
パターン: エラーハンドリングとローディング状態
<div class="card">
  <div class="card-header">
    <h3>Bitcoin Price</h3>
  </div>
  <div class="card-body">
    
    <div class="loading" id="crypto-loading">
      <div class="spinner"></div>
      <p>Loading...</p>
    </div>

    
    <div class="error" id="crypto-error" style="display: none;">
      <p class="error-message"></p>
      <button onclick="retryFetch()">Retry</button>
    </div>

    
    <div class="content" id="crypto-content" style="display: none;">
      <div class="metric-value" id="btc-price">$0</div>
    </div>
  </div>
</div>

<script>
async function fetchCryptoWithStates() {
  const loadingEl = document.getElementById('crypto-loading');
  const errorEl = document.getElementById('crypto-error');
  const contentEl = document.getElementById('crypto-content');

  // Show loading
  loadingEl.style.display = 'block';
  errorEl.style.display = 'none';
  contentEl.style.display = 'none';

  try {
    const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd');

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    const data = await response.json();

    // Show content
    loadingEl.style.display = 'none';
    contentEl.style.display = 'block';

    document.getElementById('btc-price').textContent =
      '$' + data.bitcoin.usd.toLocaleString();

  } catch (error) {
    // Show error
    loadingEl.style.display = 'none';
    errorEl.style.display = 'block';
    errorEl.querySelector('.error-message').textContent =
      'Failed to load Bitcoin price: ' + error.message;
  }
}
</script>
パターン: パフォーマンスのためのキャッシュ
// Simple in-memory cache with TTL
class DataCache {
  constructor(ttlSeconds = 300) {
    this.cache = new Map();
    this.ttl = ttlSeconds * 1000;
  }

  get(key) {
    const item = this.cache.get(key);
    if (!item) return null;

    if (Date.now() > item.expires) {
      this.cache.delete(key);
      return null;
    }

    return item.data;
  }

  set(key, data) {
    this.cache.set(key, {
      data,
      expires: Date.now() + this.ttl
    });
  }

  clear() {
    this.cache.clear();
  }
}

const dataCache = new DataCache(300); // 5 minute TTL

async function fetchWithCache(url, cacheKey) {
  // Check cache first
  const cached = dataCache.get(cacheKey);
  if (cached) {
    console.log('Cache hit:', cacheKey);
    return cached;
  }

  // Fetch fresh data
  console.log('Cache miss:', cacheKey);
  const response = await fetch(url);
  const data = await response.json();

  // Store in cache
  dataCache.set(cacheKey, data);

  return data;
}

// Usage
const weather = await fetchWithCache(
  'https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true',
  'weather-berlin'
);

9. デプロイとホスティング

静的ホスティング(バックエンドなし)

ダッシュボードがクライアント側の JavaScript と公開 API のみを使用する場合:

選択肢:

  1. GitHub Pages - 無料、静的サイト向けでシンプル
  2. Netlify - 無料枠、即時デプロイ、カスタムドメイン
  3. Vercel - 無料枠、卓越したパフォーマンス
  4. Cloudflare Pages - 無料、高速なグローバル CDN

デプロイ手順(Netlify):

# 1. Build your dashboard
mkdir my-dashboard
cd my-dashboard
# Copy your HTML/CSS/JS files

# 2. Initialize git
git init
git add .
git commit -m "Initial dashboard"

# 3. Push to GitHub
gh repo create my-dashboard --public --source=. --push

# 4. Connect to Netlify (or use drag-and-drop on netlify.com)
netlify deploy --prod
バックエンドあり(SSE/API)

サーバー側のデータ処理や SSE が必要な場合:

シンプルな FastAPI バックエンド:

# backend/main.py
from fastapi import FastAPI
from fastapi.responses import StreamingResponse, FileResponse
from fastapi.staticfiles import StaticFiles
import asyncio
import json

app = FastAPI()

# Serve static dashboard files
app.mount("/static", StaticFiles(directory="dashboard"), name="static")

@app.get("/")
async def root():
    return FileResponse("dashboard/index.html")

# SSE endpoint for real-time updates
async def event_generator():
    while True:
        # Fetch data from external APIs
        data = {
            "users": 1250,
            "revenue": 45230,
            "conversion": 3.2,
            "active": 42,
            "timestamp": "12:00"
        }

        yield f"event: metrics\ndata: {json.dumps(data)}\n\n"
        await asyncio.sleep(5)

@app.get("/api/dashboard-updates")
async def stream():
    return StreamingResponse(event_generator(), media_type="text/event-stream")

# Run with: uvicorn main:app --reload

Render/Railway/Fly.io へのデプロイ: いずれも FastAPI をサポートする趣味のプロジェクト向けの無料枠を提供しています。


10. トラブルシューティング

よくある問題

CORS エラー:

// If API doesn't support CORS, use a proxy
const PROXY = 'https://corsproxy.io/?';
const response = await fetch(PROXY + encodeURIComponent(apiUrl));

レート制限:

// Implement exponential backoff
async function fetchWithRetry(url, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url);

      if (response.status === 429) {
        // Rate limited, wait and retry
        const waitTime = Math.pow(2, i) * 1000;
        console.log(`Rate limited, waiting ${waitTime}ms`);
        await new Promise(resolve => setTimeout(resolve, waitTime));
        continue;
      }

      return response;
    } catch (error) {
      if (i === maxRetries - 1) throw error;
    }
  }
}

チャートがレンダリングされない:

// Ensure canvas is visible before initialization
const canvas = document.getElementById('myChart');
if (canvas.offsetParent === null) {
  // Canvas is hidden, wait for it to be visible
  const observer = new MutationObserver(() => {
    if (canvas.offsetParent !== null) {
      initializeChart();
      observer.disconnect();
    }
  });
  observer.observe(canvas.parentElement, { attributes: true });
}

11. リソースと参考資料

公式ドキュメント
API ディレクトリ
デザインリソース
学習リソース

まとめ

ダッシュボードを作成する際は、次の点に留意してください。

  1. 要件から始める - ユーザーのニーズ、データソース、デバイスを理解します
  2. 賢く選択する - スピード重視ならテンプレート (Tabler)、制御重視ならカスタムです
  3. デフォルトは Chart.js - シンプルで信頼性が高く、90% のケースに適しています
  4. リアルタイムには SSE を使用する - ほとんどのダッシュボードでは WebSocket よりもシンプルです
  5. 常にモバイルファースト - 最も小さい画面を最初に設計します
  6. アクセシビリティは重要 - 色覚多様性への配慮、ARIA ラベル、キーボードナビゲーション
  7. パフォーマンスは重視すべき - データのキャッシュ、イベントのデバウンス、遅延読み込み
  8. データソースを支援する - ユーザーが API を必要とする場合は徹底的に調査します

黄金律: 要件を満たす最もシンプルなダッシュボードを構築してください。過剰な設計は避けましょう。

コメント