𝕏 f B! L
案件・求人数 12,345
案件を探す(準備中) エージェントを探す(準備中) お役立ち情報 ログイン
案件・求人数 12,345
Codex CLI × データ可視化・ダッシュボード開発ガイド【D3.js・Chart.js対応】

Codex CLI × データ可視化・ダッシュボード開発ガイド【D3.js・Chart.js対応】

Codex CLIデータ可視化D3.jsChart.jsダッシュボード開発
目次
⚡ 3秒でわかる!この記事のポイント
  • Codex CLIはD3.js・Chart.js・Rechartsなど主要チャートライブラリのコードを自然言語から高精度に生成できる
  • CSVやJSON形式のデータを渡すだけで、データに最適なグラフ種類を自動選択・生成してくれる
  • リアルタイムダッシュボードの構築では、WebSocket連携やAPI統合のコードも一括で生成可能

「売上データのグラフを作りたいけど、D3.jsのAPI複雑すぎて毎回ドキュメント見直してる…」 「ダッシュボードのチャートコンポーネント、1つ作るのに半日かかる…」

データ可視化は多くのWebアプリに必須の機能ですが、チャートライブラリのAPI習得と細かなカスタマイズに時間を取られがちです。OpenAI Codex CLIを活用すれば、自然言語でグラフの要件を伝えるだけで、高品質なチャートコードを瞬時に生成できます。

この記事では、Codex CLIを使ったデータ可視化・ダッシュボード開発の実践テクニックを、主要ライブラリごとに詳しく解説します。

この記事でわかること
  • Codex CLIでD3.js・Chart.js・Rechartsのチャートを効率的に生成する方法
  • データ形式に応じた最適なグラフ種類の選択指針
  • リアルタイムダッシュボードの構築手順
  • SES現場でのデータ可視化案件に活かせるテクニック

データ可視化ライブラリの選択指針

主要ライブラリの比較

データ可視化に使えるJavaScriptライブラリは多数ありますが、SES現場で特によく使われるのは以下の4つです。

ライブラリ特徴学習コストカスタマイズ性React対応
D3.js最も柔軟、SVG直接操作高い◎ 無限△ ラッパー必要
Chart.js簡単・軽量・Canvas描画低い○ 十分○ react-chartjs-2
RechartsReact特化・宣言的API低い○ 十分◎ ネイティブ
Apache ECharts大規模データ対応中程度◎ 豊富○ echarts-for-react

Codex CLIはこれらすべてのライブラリに対応しており、プロンプトでライブラリを指定するだけで適切なコードを生成します。

Codex CLIでライブラリ選択を相談する

codex "以下のデータ可視化要件に最適なライブラリを選んで理由も教えて:
- 月別売上の棒グラフ(5系列)
- リアルタイム更新(5秒間隔)
- React + TypeScript環境
- ツールチップとドリルダウン機能
- データポイント数: 最大1000"

Codex CLIの回答例:

推奨: Recharts

理由:
1. React環境でのネイティブサポート - JSXで宣言的にチャートを定義可能
2. TypeScriptの型定義が充実
3. 1000データポイントはSVGベースで十分なパフォーマンス
4. ツールチップはビルトイン、ドリルダウンはonClickハンドラで実装可能
5. リアルタイム更新はReactの状態管理で自然に対応

D3.jsほどの自由度は不要な要件。Chart.jsはCanvas描画のため
ツールチップのカスタマイズがやや制限される。

Chart.jsでグラフを生成する

基本的な棒グラフの生成

まず、最も利用頻度の高いChart.jsでの棒グラフ生成から見ていきましょう。

codex "Chart.jsとreact-chartjs-2で月別売上棒グラフコンポーネントを作って。
TypeScriptで、以下の要件:
- 月別の売上データ(金額と件数の2軸)
- レスポンシブ対応
- ツールチップに通貨フォーマット(¥)
- 棒グラフの色はブランドカラー(#4285f4, #34a853)
- 件数は折れ線グラフで重ねる(複合チャート)"

Codex CLIが生成するコード:

// components/charts/MonthlySalesChart.tsx
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend,
} from 'chart.js';
import { Chart } from 'react-chartjs-2';
import type { ChartData, ChartOptions } from 'chart.js';

ChartJS.register(
  CategoryScale,
  LinearScale,
  BarElement,
  LineElement,
  PointElement,
  Title,
  Tooltip,
  Legend
);

interface SalesData {
  month: string;
  revenue: number;
  count: number;
}

interface Props {
  data: SalesData[];
  title?: string;
}

export function MonthlySalesChart({ data, title = '月別売上推移' }: Props) {
  const chartData: ChartData<'bar' | 'line'> = {
    labels: data.map(d => d.month),
    datasets: [
      {
        type: 'bar' as const,
        label: '売上金額',
        data: data.map(d => d.revenue),
        backgroundColor: 'rgba(66, 133, 244, 0.8)',
        borderColor: '#4285f4',
        borderWidth: 1,
        yAxisID: 'y',
        order: 2,
      },
      {
        type: 'line' as const,
        label: '受注件数',
        data: data.map(d => d.count),
        borderColor: '#34a853',
        backgroundColor: 'rgba(52, 168, 83, 0.1)',
        borderWidth: 2,
        pointRadius: 4,
        pointHoverRadius: 6,
        fill: true,
        yAxisID: 'y1',
        order: 1,
      },
    ],
  };

  const options: ChartOptions<'bar'> = {
    responsive: true,
    maintainAspectRatio: false,
    plugins: {
      title: {
        display: true,
        text: title,
        font: { size: 16, weight: 'bold' },
      },
      tooltip: {
        callbacks: {
          label: (context) => {
            const label = context.dataset.label ?? '';
            const value = context.parsed.y;
            if (label === '売上金額') {
              return `${label}: ¥${value.toLocaleString()}`;
            }
            return `${label}: ${value}件`;
          },
        },
      },
      legend: {
        position: 'top',
      },
    },
    scales: {
      y: {
        type: 'linear',
        display: true,
        position: 'left',
        title: { display: true, text: '売上金額 (¥)' },
        ticks: {
          callback: (value) => `¥${Number(value).toLocaleString()}`,
        },
      },
      y1: {
        type: 'linear',
        display: true,
        position: 'right',
        title: { display: true, text: '受注件数' },
        grid: { drawOnChartArea: false },
        ticks: {
          callback: (value) => `${value}件`,
        },
      },
    },
  };

  return (
    <div style={{ height: '400px', width: '100%' }}>
      <Chart type="bar" data={chartData} options={options} />
    </div>
  );
}

円グラフ・ドーナツグラフ

codex "Chart.jsでSES案件のスキル別割合をドーナツグラフで表示するコンポーネントを作って。
中央にパーセンテージ表示、クリックでドリルダウン対応"
// components/charts/SkillDistributionChart.tsx
import { Doughnut } from 'react-chartjs-2';
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js';
import { useState } from 'react';

ChartJS.register(ArcElement, Tooltip, Legend);

interface SkillData {
  skill: string;
  count: number;
  subSkills?: { name: string; count: number }[];
}

interface Props {
  data: SkillData[];
  onDrillDown?: (skill: string) => void;
}

export function SkillDistributionChart({ data, onDrillDown }: Props) {
  const [selectedSkill, setSelectedSkill] = useState<string | null>(null);
  const total = data.reduce((sum, d) => sum + d.count, 0);

  const colors = [
    '#4285f4', '#ea4335', '#fbbc04', '#34a853',
    '#ff6d01', '#46bdc6', '#7baaf7', '#f07b72',
  ];

  const chartData = {
    labels: data.map(d => d.skill),
    datasets: [{
      data: data.map(d => d.count),
      backgroundColor: colors.slice(0, data.length),
      borderWidth: 2,
      borderColor: '#ffffff',
    }],
  };

  const centerTextPlugin = {
    id: 'centerText',
    afterDraw(chart: ChartJS) {
      const { ctx, width, height } = chart;
      ctx.save();
      ctx.font = 'bold 24px sans-serif';
      ctx.fillStyle = '#333';
      ctx.textAlign = 'center';
      ctx.textBaseline = 'middle';
      ctx.fillText(`${total}件`, width / 2, height / 2 - 10);
      ctx.font = '14px sans-serif';
      ctx.fillStyle = '#666';
      ctx.fillText('総案件数', width / 2, height / 2 + 15);
      ctx.restore();
    },
  };

  const options = {
    responsive: true,
    cutout: '65%',
    plugins: {
      legend: { position: 'right' as const },
      tooltip: {
        callbacks: {
          label: (context: { label: string; parsed: number }) => {
            const pct = ((context.parsed / total) * 100).toFixed(1);
            return `${context.label}: ${context.parsed}件 (${pct}%)`;
          },
        },
      },
    },
    onClick: (_: unknown, elements: { index: number }[]) => {
      if (elements.length > 0) {
        const skill = data[elements[0].index].skill;
        setSelectedSkill(skill);
        onDrillDown?.(skill);
      }
    },
  };

  return (
    <div style={{ maxWidth: '500px', margin: '0 auto' }}>
      <Doughnut data={chartData} options={options} plugins={[centerTextPlugin]} />
      {selectedSkill && (
        <p className="text-center mt-2 text-blue-600">
          {selectedSkill}の詳細を表示中
        </p>
      )}
    </div>
  );
}

D3.jsで高度なチャートを作成

ヒートマップの生成

D3.jsは複雑なカスタムチャートを作る際に必要になります。Codex CLIにD3.jsのコードを生成させましょう。

codex "D3.jsでGitHubの草(contribution graph)のようなヒートマップコンポーネントをReact + TypeScriptで作って。
- 過去1年分のデータを週×日で表示
- 色の濃さで件数を表現(緑グラデーション)
- ツールチップで日付と件数を表示
- セルサイズは12px、gap 2px"
// components/charts/ContributionHeatmap.tsx
'use client';

import { useEffect, useRef } from 'react';
import * as d3 from 'd3';

interface DayData {
  date: string; // YYYY-MM-DD
  count: number;
}

interface Props {
  data: DayData[];
  colorScheme?: string[];
}

export function ContributionHeatmap({
  data,
  colorScheme = ['#ebedf0', '#9be9a8', '#40c463', '#30a14e', '#216e39'],
}: Props) {
  const svgRef = useRef<SVGSVGElement>(null);
  const tooltipRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!svgRef.current || !data.length) return;

    const cellSize = 12;
    const gap = 2;
    const margin = { top: 20, right: 10, bottom: 20, left: 30 };

    const svg = d3.select(svgRef.current);
    svg.selectAll('*').remove();

    const maxCount = d3.max(data, d => d.count) ?? 0;
    const colorScale = d3.scaleQuantize<string>()
      .domain([0, maxCount])
      .range(colorScheme);

    const dataMap = new Map(data.map(d => [d.date, d.count]));

    // Generate all days in the past year
    const today = new Date();
    const oneYearAgo = new Date(today);
    oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);

    const days: { date: Date; count: number }[] = [];
    const current = new Date(oneYearAgo);
    while (current <= today) {
      const key = current.toISOString().slice(0, 10);
      days.push({ date: new Date(current), count: dataMap.get(key) ?? 0 });
      current.setDate(current.getDate() + 1);
    }

    const weeks = d3.group(days, d => d3.timeWeek.count(oneYearAgo, d.date));
    const weekLabels = ['', '月', '', '水', '', '金', ''];

    // Day labels
    svg.selectAll('.day-label')
      .data(weekLabels)
      .join('text')
      .attr('class', 'day-label')
      .attr('x', margin.left - 5)
      .attr('y', (_, i) => margin.top + i * (cellSize + gap) + cellSize)
      .attr('text-anchor', 'end')
      .attr('font-size', '10px')
      .attr('fill', '#666')
      .text(d => d);

    const tooltip = d3.select(tooltipRef.current);

    // Cells
    weeks.forEach((weekDays, weekIndex) => {
      svg.selectAll(`.cell-${weekIndex}`)
        .data(weekDays)
        .join('rect')
        .attr('x', margin.left + weekIndex * (cellSize + gap))
        .attr('y', d => margin.top + d.date.getDay() * (cellSize + gap))
        .attr('width', cellSize)
        .attr('height', cellSize)
        .attr('rx', 2)
        .attr('fill', d => d.count === 0 ? colorScheme[0] : colorScale(d.count))
        .on('mouseover', (event, d) => {
          tooltip
            .style('display', 'block')
            .style('left', `${event.pageX + 10}px`)
            .style('top', `${event.pageY - 30}px`)
            .html(`<strong>${d.date.toLocaleDateString('ja-JP')}</strong><br/>${d.count}件`);
        })
        .on('mouseout', () => {
          tooltip.style('display', 'none');
        });
    });

    const totalWeeks = weeks.size;
    svg.attr('width', margin.left + totalWeeks * (cellSize + gap) + margin.right)
       .attr('height', margin.top + 7 * (cellSize + gap) + margin.bottom);

  }, [data, colorScheme]);

  return (
    <div style={{ position: 'relative', overflowX: 'auto' }}>
      <svg ref={svgRef} />
      <div
        ref={tooltipRef}
        style={{
          display: 'none',
          position: 'absolute',
          background: 'rgba(0,0,0,0.8)',
          color: 'white',
          padding: '6px 10px',
          borderRadius: '4px',
          fontSize: '12px',
          pointerEvents: 'none',
          zIndex: 10,
        }}
      />
    </div>
  );
}

Codex CLIによるデータ可視化開発の全体像

Rechartsでダッシュボードを構築

ダッシュボード全体の設計

Rechartsは宣言的なAPIでReactとの親和性が最も高く、ダッシュボード開発に最適です。

codex "Rechartsを使ったSES事業のダッシュボードを設計して。以下のチャートを含む:
1. 月別売上推移(棒グラフ + 目標ライン)
2. スキル別案件分布(円グラフ)
3. エンジニア稼働率推移(エリアチャート)
4. 地域別案件ヒートマップ
5. KPIカード(売上・利益率・稼働率・新規契約数)
全てTypeScript + レスポンシブ対応で"

KPIカードコンポーネント:

// components/dashboard/KpiCards.tsx
interface KpiData {
  title: string;
  value: string;
  change: number;
  changeLabel: string;
  icon: string;
}

export function KpiCards({ data }: { data: KpiData[] }) {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
      {data.map((kpi) => (
        <div key={kpi.title} className="bg-white rounded-lg shadow p-6">
          <div className="flex items-center justify-between">
            <span className="text-2xl">{kpi.icon}</span>
            <span className={`text-sm font-medium ${
              kpi.change >= 0 ? 'text-green-600' : 'text-red-600'
            }`}>
              {kpi.change >= 0 ? '↑' : '↓'} {Math.abs(kpi.change)}%
            </span>
          </div>
          <p className="mt-2 text-3xl font-bold text-gray-900">{kpi.value}</p>
          <p className="mt-1 text-sm text-gray-500">{kpi.title}</p>
          <p className="text-xs text-gray-400">{kpi.changeLabel}</p>
        </div>
      ))}
    </div>
  );
}

売上推移チャート(目標ライン付き):

// components/dashboard/RevenueChart.tsx
import {
  BarChart, Bar, XAxis, YAxis, CartesianGrid,
  Tooltip, Legend, ReferenceLine, ResponsiveContainer,
} from 'recharts';

interface RevenueData {
  month: string;
  revenue: number;
  target: number;
  profit: number;
}

interface Props {
  data: RevenueData[];
  monthlyTarget: number;
}

export function RevenueChart({ data, monthlyTarget }: Props) {
  const formatYen = (value: number) => `¥${(value / 10000).toFixed(0)}万`;

  return (
    <div className="bg-white rounded-lg shadow p-6">
      <h3 className="text-lg font-semibold mb-4">📈 月別売上推移</h3>
      <ResponsiveContainer width="100%" height={350}>
        <BarChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
          <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
          <XAxis dataKey="month" tick={{ fontSize: 12 }} />
          <YAxis tickFormatter={formatYen} tick={{ fontSize: 12 }} />
          <Tooltip
            formatter={(value: number, name: string) => [
              formatYen(value),
              name === 'revenue' ? '売上' : '利益',
            ]}
            labelStyle={{ fontWeight: 'bold' }}
          />
          <Legend formatter={(value) => (value === 'revenue' ? '売上' : '利益')} />
          <ReferenceLine
            y={monthlyTarget}
            label={{ value: '目標', position: 'right' }}
            stroke="#ea4335"
            strokeDasharray="5 5"
            strokeWidth={2}
          />
          <Bar dataKey="revenue" fill="#4285f4" radius={[4, 4, 0, 0]} />
          <Bar dataKey="profit" fill="#34a853" radius={[4, 4, 0, 0]} />
        </BarChart>
      </ResponsiveContainer>
    </div>
  );
}

エンジニア稼働率エリアチャート

// components/dashboard/UtilizationChart.tsx
import {
  AreaChart, Area, XAxis, YAxis, CartesianGrid,
  Tooltip, ResponsiveContainer, ReferenceLine,
} from 'recharts';

interface UtilizationData {
  month: string;
  utilization: number;
  benchmark: number;
}

export function UtilizationChart({ data }: { data: UtilizationData[] }) {
  return (
    <div className="bg-white rounded-lg shadow p-6">
      <h3 className="text-lg font-semibold mb-4">👥 エンジニア稼働率推移</h3>
      <ResponsiveContainer width="100%" height={300}>
        <AreaChart data={data}>
          <defs>
            <linearGradient id="utilizationGradient" x1="0" y1="0" x2="0" y2="1">
              <stop offset="5%" stopColor="#4285f4" stopOpacity={0.3} />
              <stop offset="95%" stopColor="#4285f4" stopOpacity={0} />
            </linearGradient>
          </defs>
          <CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
          <XAxis dataKey="month" tick={{ fontSize: 12 }} />
          <YAxis
            domain={[0, 100]}
            tickFormatter={(v) => `${v}%`}
            tick={{ fontSize: 12 }}
          />
          <Tooltip formatter={(value: number) => [`${value}%`, '稼働率']} />
          <ReferenceLine
            y={80}
            label="目標80%"
            stroke="#ea4335"
            strokeDasharray="3 3"
          />
          <Area
            type="monotone"
            dataKey="utilization"
            stroke="#4285f4"
            strokeWidth={2}
            fill="url(#utilizationGradient)"
          />
        </AreaChart>
      </ResponsiveContainer>
    </div>
  );
}

リアルタイムダッシュボードの構築

WebSocket連携パターン

リアルタイムデータの可視化は、SES管理ツールなどで需要の高い機能です。

codex "WebSocketでリアルタイム更新されるダッシュボードを作って。
- データソース: WebSocket (ws://localhost:8080)
- 受信メッセージ: {type: 'metric', name: string, value: number, timestamp: string}
- 直近60秒のデータをローリング表示
- 接続ステータス表示(接続中/切断/再接続中)
- 自動再接続(指数バックオフ)"
// hooks/useRealtimeMetrics.ts
import { useState, useEffect, useCallback, useRef } from 'react';

interface Metric {
  name: string;
  value: number;
  timestamp: string;
}

type ConnectionStatus = 'connecting' | 'connected' | 'disconnected' | 'reconnecting';

export function useRealtimeMetrics(url: string, windowSeconds = 60) {
  const [metrics, setMetrics] = useState<Metric[]>([]);
  const [status, setStatus] = useState<ConnectionStatus>('connecting');
  const wsRef = useRef<WebSocket | null>(null);
  const retryCountRef = useRef(0);

  const connect = useCallback(() => {
    const ws = new WebSocket(url);
    wsRef.current = ws;

    ws.onopen = () => {
      setStatus('connected');
      retryCountRef.current = 0;
    };

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      if (data.type === 'metric') {
        setMetrics(prev => {
          const cutoff = Date.now() - windowSeconds * 1000;
          const filtered = prev.filter(
            m => new Date(m.timestamp).getTime() > cutoff
          );
          return [...filtered, {
            name: data.name,
            value: data.value,
            timestamp: data.timestamp,
          }];
        });
      }
    };

    ws.onclose = () => {
      setStatus('reconnecting');
      const delay = Math.min(1000 * Math.pow(2, retryCountRef.current), 30000);
      retryCountRef.current += 1;
      setTimeout(connect, delay);
    };

    ws.onerror = () => ws.close();
  }, [url, windowSeconds]);

  useEffect(() => {
    connect();
    return () => wsRef.current?.close();
  }, [connect]);

  return { metrics, status };
}

リアルタイムチャートコンポーネント

// components/dashboard/RealtimeChart.tsx
'use client';

import { useRealtimeMetrics } from '@/hooks/useRealtimeMetrics';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, ResponsiveContainer } from 'recharts';

export function RealtimeChart() {
  const { metrics, status } = useRealtimeMetrics('ws://localhost:8080');

  const statusColor = {
    connecting: 'text-yellow-500',
    connected: 'text-green-500',
    disconnected: 'text-red-500',
    reconnecting: 'text-orange-500',
  };

  const chartData = metrics
    .filter(m => m.name === 'cpu_usage')
    .map(m => ({
      time: new Date(m.timestamp).toLocaleTimeString('ja-JP'),
      value: m.value,
    }));

  return (
    <div className="bg-white rounded-lg shadow p-6">
      <div className="flex items-center justify-between mb-4">
        <h3 className="text-lg font-semibold">⚡ リアルタイムメトリクス</h3>
        <span className={`text-sm ${statusColor[status]}`}>
          ● {status}
        </span>
      </div>
      <ResponsiveContainer width="100%" height={250}>
        <LineChart data={chartData}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="time" tick={{ fontSize: 10 }} />
          <YAxis domain={[0, 100]} tickFormatter={(v) => `${v}%`} />
          <Line
            type="monotone"
            dataKey="value"
            stroke="#4285f4"
            strokeWidth={2}
            dot={false}
            isAnimationActive={false}
          />
        </LineChart>
      </ResponsiveContainer>
    </div>
  );
}

CSVデータからチャートを自動生成

Codex CLIのデータ分析能力

Codex CLIにCSVファイルを渡して、適切なチャートを自動生成させることができます。

codex "このCSVデータを分析して、最適なチャートを3つ生成して。Rechartsで。

月,売上,コスト,利益,案件数,エンジニア数,稼働率
2026-01,15200000,11400000,3800000,42,35,85
2026-02,16800000,12600000,4200000,45,37,88
2026-03,14500000,10875000,3625000,38,33,82
...
"

Codex CLIは以下の分析を行い、チャートを生成します。

  1. トレンド分析: 売上・利益の推移 → 折れ線グラフ
  2. 構成比分析: コスト構造 → 積み上げ棒グラフ
  3. 相関分析: 稼働率と利益率の関係 → 散布図

SES現場でのデータ可視化案件

よくある要件パターン

SES現場でデータ可視化が求められる典型的な案件を紹介します。

1. 経営ダッシュボード

  • KPIの一覧表示(売上・利益・顧客数)
  • 前月比・前年比の比較チャート
  • 部門別のドリルダウン

2. BI(ビジネスインテリジェンス)ツール

  • SQLクエリ結果の自動可視化
  • フィルタ・ドリルダウン・エクスポート
  • ユーザーがチャート種類を選択可能

3. IoT/監視ダッシュボード

  • リアルタイムセンサーデータの可視化
  • 閾値超過時のアラート表示
  • 時系列データのズーム・パン操作

4. レポート自動生成

  • 定期レポートのPDF出力
  • チャートを含むメール配信
  • Slack/Teams連携での定期配信

単価への影響

データ可視化スキルはSES市場で評価が高まっています。

スキルセット月額単価相場(2026年)
フロントエンド(React基本)60〜75万円
D3.js / Chart.js 実装経験70〜90万円
ダッシュボード設計・構築75〜95万円
リアルタイムデータ可視化80〜100万円
上記 + AI活用(Codex CLI等)85〜105万円

パフォーマンス最適化テクニック

大量データの描画最適化

codex "10万データポイントのチャートパフォーマンスを最適化して。
現在Rechartsで描画に5秒かかっている。
ダウンサンプリング、仮想化、Canvas描画への切り替えを検討して"

Codex CLIの提案するアプローチ:

  1. ダウンサンプリング: LTTB(Largest-Triangle-Three-Buckets)アルゴリズムで視覚的に重要なポイントのみ残す
  2. Canvas描画: SVGからCanvas(EChartsまたはuPlot)に切り替え
  3. 仮想スクロール: テーブルデータはTanStack Virtual で仮想化
  4. メモ化: useMemoでチャートデータの再計算を防止
// utils/downsample.ts — LTTBアルゴリズム
export function lttbDownsample<T extends { x: number; y: number }>(
  data: T[],
  threshold: number
): T[] {
  if (data.length <= threshold) return data;

  const sampled: T[] = [data[0]];
  const bucketSize = (data.length - 2) / (threshold - 2);

  let a = 0;
  for (let i = 0; i < threshold - 2; i++) {
    const rangeStart = Math.floor((i + 1) * bucketSize) + 1;
    const rangeEnd = Math.min(
      Math.floor((i + 2) * bucketSize) + 1,
      data.length
    );

    // Average point of next bucket
    let avgX = 0, avgY = 0;
    for (let j = rangeStart; j < rangeEnd; j++) {
      avgX += data[j].x;
      avgY += data[j].y;
    }
    avgX /= (rangeEnd - rangeStart);
    avgY /= (rangeEnd - rangeStart);

    // Find point with largest triangle area
    let maxArea = -1;
    let maxIndex = rangeStart;
    for (let j = rangeStart; j < rangeEnd; j++) {
      const area = Math.abs(
        (data[a].x - avgX) * (data[j].y - data[a].y) -
        (data[a].x - data[j].x) * (avgY - data[a].y)
      ) * 0.5;
      if (area > maxArea) {
        maxArea = area;
        maxIndex = j;
      }
    }

    sampled.push(data[maxIndex]);
    a = maxIndex;
  }

  sampled.push(data[data.length - 1]);
  return sampled;
}

まとめ — データ可視化をAIで加速する

Codex CLIとデータ可視化ライブラリの組み合わせは、SES現場でのダッシュボード開発を大幅に加速します。

  • Chart.js: 手軽に美しいグラフを生成したい場合に最適。Codex CLIなら設定の細かなカスタマイズも瞬時に対応
  • D3.js: カスタムチャートが必要な場合。Codex CLIが複雑なSVG操作コードを自動生成
  • Recharts: Reactダッシュボードの定番。宣言的APIとCodex CLIの相性が抜群
  • リアルタイム対応: WebSocket連携・自動再接続も含めたコードを一括生成可能

Codex CLI入門ガイドでまず基本を押さえ、フロントエンド開発ガイドと合わせて学習を進めることで、データ可視化のスキルを効率的に高められます。TypeScript開発ガイドの型安全なパターンも参考にしてください。

SES案件をお探しですか?

SES記事をもっと読む →
🏗️

SES BASE 編集長

SES業界歴10年以上のメンバーが在籍する編集チーム。SES企業での営業・エンジニア経験、フリーランス独立経験を持つメンバーが、業界のリアルな情報をお届けします。

📊 業界データに基づく記事制作 🔍 IPA・経済産業省データ参照 💼 SES実務経験者が執筆・監修