D3.js

データ駆動ドキュメント作成のためのJavaScriptライブラリ。SVG、Canvas、HTMLを用いたカスタムデータビジュアライゼーション作成。柔軟性が高く、インタラクティブな可視化の業界標準ツール。

JavaScriptデータビジュアライゼーションSVGCanvasインタラクティブチャートグラフWeb

GitHub概要

d3/d3

Bring data to life with SVG, Canvas and HTML. :bar_chart::chart_with_upwards_trend::tada:

リポジトリ:https://github.com/d3/d3
ホームページ:https://d3js.org
スター111,068
ウォッチ3,586
フォーク22,848
作成日:2010年9月27日
言語:Shell
ライセンス:ISC License

トピックス

chartchartsd3data-visualizationsvgvisualization

スター履歴

d3/d3 Star History
データ取得日時: 2025/7/17 10:32

フレームワーク

D3.js

概要

D3.js(Data-Driven Documents)は、Webブラウザでインタラクティブなデータビジュアライゼーションを作成するためのJavaScriptライブラリです。

詳細

D3.js(ディースリー・ドット・ジェーエス)は、2011年にMike Bostockによって開発されたデータビジュアライゼーション専用のJavaScriptライブラリです。「Data-Driven Documents」の略称で、データに基づいてDOM要素を動的に操作し、美しく機能的な可視化を実現します。SVG、HTML、CSSを駆使してカスタムチャートやインタラクティブな図表を作成でき、棒グラフ、線グラフ、散布図から複雑なネットワーク図、地理的マップ、ツリーマップまで幅広い可視化に対応しています。データとDOM要素の結合(Data Join)、スケール関数による座標変換、アニメーション機能、豊富なレイアウトアルゴリズム、強力なイベントハンドリングなどが特徴です。New York Times、The Guardian、Observable、多くの研究機関や企業のダッシュボードで活用されており、データジャーナリズムや学術研究での可視化のデファクトスタンダードとなっています。

メリット・デメリット

メリット

  • 完全なカスタマイズ性: 既存チャートの制約なく独自の可視化を実現可能
  • Web標準技術: SVG、HTML、CSSベースでWebの標準技術と完全互換
  • 高性能: 大量のデータを効率的に処理・描画可能
  • インタラクティブ性: マウスイベント、アニメーション、リアルタイム更新
  • 豊富なレイアウト: 力学シミュレーション、階層レイアウト、地図投影法等
  • データ処理機能: CSV、JSON、TSV等の読み込み・変換機能内蔵
  • アニメーション: 滑らかな遷移とトランジション効果

デメリット

  • 学習コストが高い: 習得に時間がかかり、初心者には敷居が高い
  • 開発時間: 簡単なチャートでも他のライブラリと比較して開発時間が長い
  • メンテナンス性: 複雑な可視化はコードの保守が困難
  • モバイル対応: タッチイベントやレスポンシブデザインの実装が複雑
  • デバッグの困難さ: 複雑なアニメーションやレイアウトのデバッグが大変
  • バンドルサイズ: 機能が豊富な分、ライブラリサイズが大きい

主要リンク

書き方の例

Hello World

<!DOCTYPE html>
<html>
<head>
    <script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
    <svg width="300" height="100"></svg>
    
    <script>
        // D3.jsの基本的な使用方法
        console.log("D3.js version:", d3.version);
        
        // SVG要素にテキストを追加
        d3.select("svg")
          .append("text")
          .attr("x", 50)
          .attr("y", 50)
          .attr("font-family", "Arial")
          .attr("font-size", "20px")
          .attr("fill", "blue")
          .text("Hello, D3.js!");
        
        // 円を描画
        d3.select("svg")
          .append("circle")
          .attr("cx", 200)
          .attr("cy", 50)
          .attr("r", 20)
          .attr("fill", "red");
    </script>
</body>
</html>

棒グラフの作成

// データ準備
const data = [
    { name: 'Apple', value: 30 },
    { name: 'Banana', value: 25 },
    { name: 'Cherry', value: 40 },
    { name: 'Date', value: 15 },
    { name: 'Elderberry', value: 35 }
];

// 基本設定
const margin = { top: 20, right: 30, bottom: 40, left: 90 };
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;

// SVG要素を作成
const svg = d3.select("#chart")
    .append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", `translate(${margin.left},${margin.top})`);

// スケールを設定
const xScale = d3.scaleLinear()
    .domain([0, d3.max(data, d => d.value)])
    .range([0, width]);

const yScale = d3.scaleBand()
    .domain(data.map(d => d.name))
    .range([0, height])
    .padding(0.1);

// 軸を作成
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);

// 軸を描画
svg.append("g")
    .attr("transform", `translate(0,${height})`)
    .call(xAxis);

svg.append("g")
    .call(yAxis);

// 棒グラフを描画
svg.selectAll(".bar")
    .data(data)
    .enter().append("rect")
    .attr("class", "bar")
    .attr("x", 0)
    .attr("y", d => yScale(d.name))
    .attr("width", d => xScale(d.value))
    .attr("height", yScale.bandwidth())
    .attr("fill", "steelblue")
    .on("mouseover", function(event, d) {
        // ホバー効果
        d3.select(this).attr("fill", "orange");
        
        // ツールチップ表示
        const tooltip = d3.select("body").append("div")
            .attr("class", "tooltip")
            .style("position", "absolute")
            .style("background", "black")
            .style("color", "white")
            .style("padding", "5px")
            .style("border-radius", "3px")
            .style("pointer-events", "none")
            .style("left", (event.pageX + 10) + "px")
            .style("top", (event.pageY - 20) + "px")
            .text(`${d.name}: ${d.value}`);
        
        setTimeout(() => tooltip.remove(), 2000);
    })
    .on("mouseout", function() {
        d3.select(this).attr("fill", "steelblue");
    });

// アニメーション付きで棒グラフを表示
svg.selectAll(".bar")
    .attr("width", 0)
    .transition()
    .duration(1000)
    .attr("width", d => xScale(d.value));

インタラクティブな散布図

class InteractiveScatterPlot {
    constructor(containerId, data) {
        this.data = data;
        this.containerId = containerId;
        this.margin = { top: 20, right: 20, bottom: 50, left: 50 };
        this.width = 600 - this.margin.left - this.margin.right;
        this.height = 400 - this.margin.top - this.margin.bottom;
        
        this.init();
    }
    
    init() {
        // SVG要素を作成
        this.svg = d3.select(`#${this.containerId}`)
            .append("svg")
            .attr("width", this.width + this.margin.left + this.margin.right)
            .attr("height", this.height + this.margin.top + this.margin.bottom);
        
        this.g = this.svg.append("g")
            .attr("transform", `translate(${this.margin.left},${this.margin.top})`);
        
        // スケールを設定
        this.xScale = d3.scaleLinear()
            .domain(d3.extent(this.data, d => d.x))
            .range([0, this.width]);
        
        this.yScale = d3.scaleLinear()
            .domain(d3.extent(this.data, d => d.y))
            .range([this.height, 0]);
        
        this.colorScale = d3.scaleOrdinal(d3.schemeCategory10)
            .domain([...new Set(this.data.map(d => d.category))]);
        
        this.sizeScale = d3.scaleSqrt()
            .domain(d3.extent(this.data, d => d.size))
            .range([5, 20]);
        
        this.createAxes();
        this.createDots();
        this.createLegend();
        this.createBrush();
    }
    
    createAxes() {
        // X軸
        this.g.append("g")
            .attr("transform", `translate(0,${this.height})`)
            .call(d3.axisBottom(this.xScale))
            .append("text")
            .attr("x", this.width / 2)
            .attr("y", 35)
            .attr("fill", "black")
            .style("text-anchor", "middle")
            .text("X軸");
        
        // Y軸
        this.g.append("g")
            .call(d3.axisLeft(this.yScale))
            .append("text")
            .attr("transform", "rotate(-90)")
            .attr("y", -35)
            .attr("x", -this.height / 2)
            .attr("fill", "black")
            .style("text-anchor", "middle")
            .text("Y軸");
    }
    
    createDots() {
        this.dots = this.g.selectAll(".dot")
            .data(this.data)
            .enter().append("circle")
            .attr("class", "dot")
            .attr("cx", d => this.xScale(d.x))
            .attr("cy", d => this.yScale(d.y))
            .attr("r", d => this.sizeScale(d.size))
            .attr("fill", d => this.colorScale(d.category))
            .attr("opacity", 0.7)
            .attr("stroke", "white")
            .attr("stroke-width", 2)
            .on("mouseover", this.handleMouseOver.bind(this))
            .on("mouseout", this.handleMouseOut.bind(this))
            .on("click", this.handleClick.bind(this));
        
        // アニメーション
        this.dots
            .attr("r", 0)
            .transition()
            .duration(1000)
            .delay((d, i) => i * 10)
            .attr("r", d => this.sizeScale(d.size));
    }
    
    createLegend() {
        const legend = this.svg.append("g")
            .attr("class", "legend")
            .attr("transform", `translate(${this.width + this.margin.left + 10}, ${this.margin.top})`);
        
        const categories = [...new Set(this.data.map(d => d.category))];
        
        const legendItems = legend.selectAll(".legend-item")
            .data(categories)
            .enter().append("g")
            .attr("class", "legend-item")
            .attr("transform", (d, i) => `translate(0, ${i * 20})`);
        
        legendItems.append("circle")
            .attr("r", 6)
            .attr("fill", d => this.colorScale(d));
        
        legendItems.append("text")
            .attr("x", 12)
            .attr("y", 0)
            .attr("dy", "0.35em")
            .style("font-size", "12px")
            .text(d => d);
    }
    
    createBrush() {
        const brush = d3.brush()
            .extent([[0, 0], [this.width, this.height]])
            .on("start brush end", this.handleBrush.bind(this));
        
        this.g.append("g")
            .attr("class", "brush")
            .call(brush);
    }
    
    handleMouseOver(event, d) {
        // ドットを強調
        d3.select(event.target)
            .transition()
            .duration(200)
            .attr("r", this.sizeScale(d.size) * 1.5)
            .attr("opacity", 1);
        
        // ツールチップ表示
        this.tooltip = d3.select("body").append("div")
            .attr("class", "tooltip")
            .style("position", "absolute")
            .style("background", "rgba(0, 0, 0, 0.8)")
            .style("color", "white")
            .style("padding", "10px")
            .style("border-radius", "5px")
            .style("pointer-events", "none")
            .style("font-size", "12px")
            .html(`
                <strong>${d.label}</strong><br/>
                X: ${d.x}<br/>
                Y: ${d.y}<br/>
                Size: ${d.size}<br/>
                Category: ${d.category}
            `)
            .style("left", (event.pageX + 10) + "px")
            .style("top", (event.pageY - 10) + "px");
    }
    
    handleMouseOut(event, d) {
        d3.select(event.target)
            .transition()
            .duration(200)
            .attr("r", this.sizeScale(d.size))
            .attr("opacity", 0.7);
        
        if (this.tooltip) {
            this.tooltip.remove();
        }
    }
    
    handleClick(event, d) {
        console.log("Clicked:", d);
        alert(`選択されたデータ: ${d.label}`);
    }
    
    handleBrush(event) {
        const selection = event.selection;
        
        if (selection) {
            const [[x0, y0], [x1, y1]] = selection;
            
            // 選択範囲内のドットを強調
            this.dots.classed("selected", d => {
                const x = this.xScale(d.x);
                const y = this.yScale(d.y);
                return x >= x0 && x <= x1 && y >= y0 && y <= y1;
            });
            
            // 選択されたデータを出力
            const selectedData = this.data.filter(d => {
                const x = this.xScale(d.x);
                const y = this.yScale(d.y);
                return x >= x0 && x <= x1 && y >= y0 && y <= y1;
            });
            
            console.log("Selected data:", selectedData);
        } else {
            this.dots.classed("selected", false);
        }
    }
    
    updateData(newData) {
        this.data = newData;
        
        // スケールを更新
        this.xScale.domain(d3.extent(this.data, d => d.x));
        this.yScale.domain(d3.extent(this.data, d => d.y));
        
        // 軸を更新
        this.g.select(".axis--x").transition().duration(1000).call(d3.axisBottom(this.xScale));
        this.g.select(".axis--y").transition().duration(1000).call(d3.axisLeft(this.yScale));
        
        // ドットを更新
        const dots = this.g.selectAll(".dot").data(this.data);
        
        dots.exit().remove();
        
        dots.enter().append("circle")
            .attr("class", "dot")
            .merge(dots)
            .transition()
            .duration(1000)
            .attr("cx", d => this.xScale(d.x))
            .attr("cy", d => this.yScale(d.y))
            .attr("r", d => this.sizeScale(d.size))
            .attr("fill", d => this.colorScale(d.category));
    }
}

// 使用例
const sampleData = [
    { x: 10, y: 20, size: 15, category: "A", label: "データ1" },
    { x: 25, y: 30, size: 25, category: "B", label: "データ2" },
    { x: 40, y: 15, size: 20, category: "A", label: "データ3" },
    { x: 35, y: 40, size: 30, category: "C", label: "データ4" },
    { x: 20, y: 35, size: 18, category: "B", label: "データ5" }
];

const scatterPlot = new InteractiveScatterPlot("scatter-chart", sampleData);

リアルタイムデータ可視化

class RealTimeLineChart {
    constructor(containerId) {
        this.containerId = containerId;
        this.data = [];
        this.maxDataPoints = 50;
        this.margin = { top: 20, right: 20, bottom: 30, left: 50 };
        this.width = 800 - this.margin.left - this.margin.right;
        this.height = 400 - this.margin.top - this.margin.bottom;
        
        this.init();
        this.startSimulation();
    }
    
    init() {
        // SVG要素を作成
        this.svg = d3.select(`#${this.containerId}`)
            .append("svg")
            .attr("width", this.width + this.margin.left + this.margin.right)
            .attr("height", this.height + this.margin.top + this.margin.bottom);
        
        this.g = this.svg.append("g")
            .attr("transform", `translate(${this.margin.left},${this.margin.top})`);
        
        // スケールを設定
        this.xScale = d3.scaleTime()
            .range([0, this.width]);
        
        this.yScale = d3.scaleLinear()
            .range([this.height, 0]);
        
        // ライン生成器
        this.line = d3.line()
            .x(d => this.xScale(d.time))
            .y(d => this.yScale(d.value))
            .curve(d3.curveCardinal);
        
        // 軸を作成
        this.xAxisGroup = this.g.append("g")
            .attr("transform", `translate(0,${this.height})`);
        
        this.yAxisGroup = this.g.append("g");
        
        // ラインパスを作成
        this.path = this.g.append("path")
            .attr("class", "line")
            .attr("fill", "none")
            .attr("stroke", "steelblue")
            .attr("stroke-width", 2);
        
        // エリアチャート
        this.area = d3.area()
            .x(d => this.xScale(d.time))
            .y0(this.height)
            .y1(d => this.yScale(d.value))
            .curve(d3.curveCardinal);
        
        this.areaPath = this.g.append("path")
            .attr("class", "area")
            .attr("fill", "steelblue")
            .attr("opacity", 0.3);
        
        // グリッドライン
        this.createGridLines();
        
        // リアルタイム値表示
        this.currentValueText = this.g.append("text")
            .attr("x", this.width - 10)
            .attr("y", 15)
            .attr("text-anchor", "end")
            .attr("class", "current-value")
            .style("font-size", "16px")
            .style("font-weight", "bold")
            .style("fill", "steelblue");
    }
    
    createGridLines() {
        // 水平グリッドライン
        this.horizontalGrid = this.g.append("g")
            .attr("class", "grid horizontal-grid")
            .style("stroke-dasharray", "3,3")
            .style("opacity", 0.3);
        
        // 垂直グリッドライン
        this.verticalGrid = this.g.append("g")
            .attr("class", "grid vertical-grid")
            .style("stroke-dasharray", "3,3")
            .style("opacity", 0.3);
    }
    
    addDataPoint(value) {
        const now = new Date();
        
        // 新しいデータポイントを追加
        this.data.push({
            time: now,
            value: value
        });
        
        // 古いデータを削除
        if (this.data.length > this.maxDataPoints) {
            this.data.shift();
        }
        
        this.updateChart();
    }
    
    updateChart() {
        if (this.data.length === 0) return;
        
        // スケールドメインを更新
        this.xScale.domain(d3.extent(this.data, d => d.time));
        this.yScale.domain(d3.extent(this.data, d => d.value));
        
        // 軸を更新
        this.xAxisGroup
            .transition()
            .duration(500)
            .call(d3.axisBottom(this.xScale)
                .tickFormat(d3.timeFormat("%H:%M:%S")));
        
        this.yAxisGroup
            .transition()
            .duration(500)
            .call(d3.axisLeft(this.yScale));
        
        // グリッドラインを更新
        this.horizontalGrid
            .transition()
            .duration(500)
            .call(d3.axisLeft(this.yScale)
                .tickSize(-this.width)
                .tickFormat("")
            );
        
        this.verticalGrid
            .transition()
            .duration(500)
            .call(d3.axisBottom(this.xScale)
                .tickSize(-this.height)
                .tickFormat("")
            );
        
        // ラインを更新
        this.path
            .datum(this.data)
            .transition()
            .duration(500)
            .attr("d", this.line);
        
        // エリアを更新
        this.areaPath
            .datum(this.data)
            .transition()
            .duration(500)
            .attr("d", this.area);
        
        // 現在値を表示
        const currentValue = this.data[this.data.length - 1].value;
        this.currentValueText.text(`現在値: ${currentValue.toFixed(2)}`);
        
        // データポイントを表示
        const circles = this.g.selectAll(".data-point")
            .data(this.data);
        
        circles.enter()
            .append("circle")
            .attr("class", "data-point")
            .attr("r", 3)
            .attr("fill", "steelblue")
            .merge(circles)
            .transition()
            .duration(500)
            .attr("cx", d => this.xScale(d.time))
            .attr("cy", d => this.yScale(d.value));
        
        circles.exit().remove();
    }
    
    startSimulation() {
        let value = 50;
        
        setInterval(() => {
            // ランダムな変化を生成
            const change = (Math.random() - 0.5) * 10;
            value = Math.max(0, Math.min(100, value + change));
            
            this.addDataPoint(value);
        }, 1000);
    }
    
    stop() {
        clearInterval(this.simulationInterval);
    }
    
    clear() {
        this.data = [];
        this.updateChart();
    }
    
    exportData() {
        const csv = d3.csvFormat(this.data.map(d => ({
            time: d.time.toISOString(),
            value: d.value
        })));
        
        const blob = new Blob([csv], { type: 'text/csv' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = 'realtime_data.csv';
        a.click();
        URL.revokeObjectURL(url);
    }
}

// 使用例
const realtimeChart = new RealTimeLineChart("realtime-chart");

// 制御ボタンの例
document.getElementById("clearButton").addEventListener("click", () => {
    realtimeChart.clear();
});

document.getElementById("exportButton").addEventListener("click", () => {
    realtimeChart.exportData();
});

力学シミュレーション(ネットワーク図)

class ForceDirectedNetwork {
    constructor(containerId, nodes, links) {
        this.containerId = containerId;
        this.nodes = nodes;
        this.links = links;
        this.width = 800;
        this.height = 600;
        
        this.init();
    }
    
    init() {
        // SVG要素を作成
        this.svg = d3.select(`#${this.containerId}`)
            .append("svg")
            .attr("width", this.width)
            .attr("height", this.height);
        
        // ズーム機能
        const zoom = d3.zoom()
            .scaleExtent([0.1, 3])
            .on("zoom", (event) => {
                this.container.attr("transform", event.transform);
            });
        
        this.svg.call(zoom);
        
        this.container = this.svg.append("g");
        
        // 力学シミュレーションを設定
        this.simulation = d3.forceSimulation(this.nodes)
            .force("link", d3.forceLink(this.links).id(d => d.id).distance(100))
            .force("charge", d3.forceManyBody().strength(-300))
            .force("center", d3.forceCenter(this.width / 2, this.height / 2))
            .force("collision", d3.forceCollide().radius(20));
        
        this.createVisualization();
        this.simulation.on("tick", () => this.tick());
    }
    
    createVisualization() {
        // リンクを描画
        this.linkElements = this.container.selectAll(".link")
            .data(this.links)
            .enter().append("line")
            .attr("class", "link")
            .attr("stroke", "#999")
            .attr("stroke-opacity", 0.6)
            .attr("stroke-width", d => Math.sqrt(d.value || 1));
        
        // ノードグループを作成
        this.nodeElements = this.container.selectAll(".node")
            .data(this.nodes)
            .enter().append("g")
            .attr("class", "node")
            .call(d3.drag()
                .on("start", this.dragStarted.bind(this))
                .on("drag", this.dragged.bind(this))
                .on("end", this.dragEnded.bind(this)));
        
        // ノードの円を描画
        this.nodeElements.append("circle")
            .attr("r", d => d.size || 10)
            .attr("fill", d => d.color || "#69b3a2")
            .attr("stroke", "#fff")
            .attr("stroke-width", 2);
        
        // ノードのラベルを描画
        this.nodeElements.append("text")
            .text(d => d.name)
            .attr("x", 0)
            .attr("y", 0)
            .attr("dy", "0.35em")
            .attr("text-anchor", "middle")
            .style("font-size", "12px")
            .style("fill", "#333")
            .style("pointer-events", "none");
        
        // イベントハンドラーを追加
        this.nodeElements
            .on("mouseover", this.handleMouseOver.bind(this))
            .on("mouseout", this.handleMouseOut.bind(this))
            .on("click", this.handleClick.bind(this));
    }
    
    tick() {
        // リンクの位置を更新
        this.linkElements
            .attr("x1", d => d.source.x)
            .attr("y1", d => d.source.y)
            .attr("x2", d => d.target.x)
            .attr("y2", d => d.target.y);
        
        // ノードの位置を更新
        this.nodeElements
            .attr("transform", d => `translate(${d.x},${d.y})`);
    }
    
    dragStarted(event, d) {
        if (!event.active) this.simulation.alphaTarget(0.3).restart();
        d.fx = d.x;
        d.fy = d.y;
    }
    
    dragged(event, d) {
        d.fx = event.x;
        d.fy = event.y;
    }
    
    dragEnded(event, d) {
        if (!event.active) this.simulation.alphaTarget(0);
        d.fx = null;
        d.fy = null;
    }
    
    handleMouseOver(event, d) {
        // ノードを強調
        d3.select(event.currentTarget)
            .select("circle")
            .transition()
            .duration(200)
            .attr("r", (d.size || 10) * 1.5)
            .attr("fill", "#ff6b6b");
        
        // 接続されたリンクを強調
        this.linkElements
            .style("stroke", link => {
                return (link.source === d || link.target === d) ? "#ff6b6b" : "#999";
            })
            .style("stroke-width", link => {
                return (link.source === d || link.target === d) ? 3 : Math.sqrt(link.value || 1);
            });
        
        // 接続されたノードを強調
        const connectedNodes = new Set();
        this.links.forEach(link => {
            if (link.source === d) connectedNodes.add(link.target);
            if (link.target === d) connectedNodes.add(link.source);
        });
        
        this.nodeElements.select("circle")
            .style("fill", node => {
                if (node === d) return "#ff6b6b";
                if (connectedNodes.has(node)) return "#ffa500";
                return node.color || "#69b3a2";
            });
    }
    
    handleMouseOut(event, d) {
        // すべての要素を元に戻す
        this.nodeElements.select("circle")
            .transition()
            .duration(200)
            .attr("r", d => d.size || 10)
            .style("fill", d => d.color || "#69b3a2");
        
        this.linkElements
            .style("stroke", "#999")
            .style("stroke-width", d => Math.sqrt(d.value || 1));
    }
    
    handleClick(event, d) {
        console.log("Clicked node:", d);
        
        // ノードの詳細情報を表示
        alert(`ノード情報:\nID: ${d.id}\n名前: ${d.name}\nサイズ: ${d.size || 10}`);
    }
    
    addNode(node) {
        this.nodes.push(node);
        this.updateVisualization();
    }
    
    addLink(link) {
        this.links.push(link);
        this.updateVisualization();
    }
    
    updateVisualization() {
        // 新しいデータでシミュレーションを更新
        this.simulation.nodes(this.nodes);
        this.simulation.force("link").links(this.links);
        
        // ビジュアル要素を更新
        const linkElements = this.container.selectAll(".link")
            .data(this.links);
        
        linkElements.enter().append("line")
            .attr("class", "link")
            .attr("stroke", "#999")
            .attr("stroke-opacity", 0.6);
        
        linkElements.exit().remove();
        
        const nodeElements = this.container.selectAll(".node")
            .data(this.nodes);
        
        const nodeEnter = nodeElements.enter().append("g")
            .attr("class", "node");
        
        nodeEnter.append("circle")
            .attr("r", d => d.size || 10)
            .attr("fill", d => d.color || "#69b3a2");
        
        nodeEnter.append("text")
            .text(d => d.name)
            .attr("text-anchor", "middle");
        
        nodeElements.exit().remove();
        
        // シミュレーションを再開
        this.simulation.alpha(1).restart();
    }
}

// 使用例
const networkNodes = [
    { id: "A", name: "Node A", size: 15, color: "#ff6b6b" },
    { id: "B", name: "Node B", size: 20, color: "#4ecdc4" },
    { id: "C", name: "Node C", size: 12, color: "#45b7d1" },
    { id: "D", name: "Node D", size: 18, color: "#f9ca24" },
    { id: "E", name: "Node E", size: 14, color: "#6c5ce7" }
];

const networkLinks = [
    { source: "A", target: "B", value: 3 },
    { source: "B", target: "C", value: 2 },
    { source: "C", target: "D", value: 1 },
    { source: "D", target: "E", value: 4 },
    { source: "E", target: "A", value: 2 }
];

const network = new ForceDirectedNetwork("network-chart", networkNodes, networkLinks);

CSV・JSONデータの読み込みと可視化

class DataLoader {
    constructor() {
        this.data = null;
    }
    
    async loadCSV(url) {
        try {
            this.data = await d3.csv(url, d => {
                // データの型変換
                return {
                    date: d3.timeParse("%Y-%m-%d")(d.date),
                    value: +d.value,
                    category: d.category,
                    name: d.name
                };
            });
            
            console.log("CSV読み込み完了:", this.data.length, "行");
            return this.data;
            
        } catch (error) {
            console.error("CSV読み込みエラー:", error);
            throw error;
        }
    }
    
    async loadJSON(url) {
        try {
            this.data = await d3.json(url);
            console.log("JSON読み込み完了:", this.data);
            return this.data;
            
        } catch (error) {
            console.error("JSON読み込みエラー:", error);
            throw error;
        }
    }
    
    async loadMultipleFiles(urls) {
        try {
            const promises = urls.map(url => {
                if (url.endsWith('.csv')) {
                    return d3.csv(url);
                } else if (url.endsWith('.json')) {
                    return d3.json(url);
                } else {
                    return d3.text(url);
                }
            });
            
            const results = await Promise.all(promises);
            console.log("複数ファイル読み込み完了:", results.length, "ファイル");
            return results;
            
        } catch (error) {
            console.error("複数ファイル読み込みエラー:", error);
            throw error;
        }
    }
    
    filterData(filterFunction) {
        if (!this.data) {
            throw new Error("データが読み込まれていません");
        }
        
        return this.data.filter(filterFunction);
    }
    
    groupData(groupKey) {
        if (!this.data) {
            throw new Error("データが読み込まれていません");
        }
        
        return d3.group(this.data, d => d[groupKey]);
    }
    
    aggregateData(groupKey, valueKey, aggregateFunction = d3.sum) {
        if (!this.data) {
            throw new Error("データが読み込まれていません");
        }
        
        const grouped = d3.group(this.data, d => d[groupKey]);
        const aggregated = Array.from(grouped, ([key, values]) => ({
            key: key,
            value: aggregateFunction(values, d => d[valueKey]),
            count: values.length
        }));
        
        return aggregated;
    }
    
    createDashboard(containerId) {
        if (!this.data) {
            throw new Error("データが読み込まれていません");
        }
        
        const container = d3.select(`#${containerId}`);
        
        // 基本統計を表示
        const stats = this.calculateStatistics();
        this.displayStatistics(container, stats);
        
        // 複数のチャートを作成
        this.createTimeSeriesChart(container.append("div").attr("id", "timeseries"));
        this.createCategoryChart(container.append("div").attr("id", "category"));
        this.createHistogram(container.append("div").attr("id", "histogram"));
    }
    
    calculateStatistics() {
        const numericData = this.data.map(d => d.value).filter(v => !isNaN(v));
        
        return {
            count: this.data.length,
            mean: d3.mean(numericData),
            median: d3.median(numericData),
            min: d3.min(numericData),
            max: d3.max(numericData),
            std: d3.deviation(numericData)
        };
    }
    
    displayStatistics(container, stats) {
        const statsDiv = container.append("div")
            .attr("class", "statistics")
            .style("margin", "20px")
            .style("padding", "15px")
            .style("border", "1px solid #ccc")
            .style("border-radius", "5px");
        
        statsDiv.append("h3").text("データ統計");
        
        Object.entries(stats).forEach(([key, value]) => {
            statsDiv.append("p")
                .html(`<strong>${key}:</strong> ${typeof value === 'number' ? value.toFixed(2) : value}`);
        });
    }
    
    createTimeSeriesChart(container) {
        if (!this.data.some(d => d.date)) return;
        
        const margin = { top: 20, right: 30, bottom: 40, left: 50 };
        const width = 600 - margin.left - margin.right;
        const height = 300 - margin.top - margin.bottom;
        
        const svg = container.append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom);
        
        const g = svg.append("g")
            .attr("transform", `translate(${margin.left},${margin.top})`);
        
        const timeData = this.data.filter(d => d.date && !isNaN(d.value));
        
        const xScale = d3.scaleTime()
            .domain(d3.extent(timeData, d => d.date))
            .range([0, width]);
        
        const yScale = d3.scaleLinear()
            .domain(d3.extent(timeData, d => d.value))
            .range([height, 0]);
        
        const line = d3.line()
            .x(d => xScale(d.date))
            .y(d => yScale(d.value))
            .curve(d3.curveMonotoneX);
        
        g.append("g")
            .attr("transform", `translate(0,${height})`)
            .call(d3.axisBottom(xScale));
        
        g.append("g")
            .call(d3.axisLeft(yScale));
        
        g.append("path")
            .datum(timeData)
            .attr("fill", "none")
            .attr("stroke", "steelblue")
            .attr("stroke-width", 2)
            .attr("d", line);
        
        container.append("h4").text("時系列チャート");
    }
    
    createCategoryChart(container) {
        const categoryData = this.aggregateData("category", "value");
        
        const margin = { top: 20, right: 30, bottom: 40, left: 100 };
        const width = 600 - margin.left - margin.right;
        const height = 300 - margin.top - margin.bottom;
        
        const svg = container.append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom);
        
        const g = svg.append("g")
            .attr("transform", `translate(${margin.left},${margin.top})`);
        
        const xScale = d3.scaleLinear()
            .domain([0, d3.max(categoryData, d => d.value)])
            .range([0, width]);
        
        const yScale = d3.scaleBand()
            .domain(categoryData.map(d => d.key))
            .range([0, height])
            .padding(0.1);
        
        g.append("g")
            .attr("transform", `translate(0,${height})`)
            .call(d3.axisBottom(xScale));
        
        g.append("g")
            .call(d3.axisLeft(yScale));
        
        g.selectAll(".bar")
            .data(categoryData)
            .enter().append("rect")
            .attr("class", "bar")
            .attr("x", 0)
            .attr("y", d => yScale(d.key))
            .attr("width", d => xScale(d.value))
            .attr("height", yScale.bandwidth())
            .attr("fill", "steelblue");
        
        container.append("h4").text("カテゴリ別チャート");
    }
    
    createHistogram(container) {
        const numericData = this.data.map(d => d.value).filter(v => !isNaN(v));
        
        const margin = { top: 20, right: 30, bottom: 40, left: 50 };
        const width = 600 - margin.left - margin.right;
        const height = 300 - margin.top - margin.bottom;
        
        const svg = container.append("svg")
            .attr("width", width + margin.left + margin.right)
            .attr("height", height + margin.top + margin.bottom);
        
        const g = svg.append("g")
            .attr("transform", `translate(${margin.left},${margin.top})`);
        
        const xScale = d3.scaleLinear()
            .domain(d3.extent(numericData))
            .range([0, width]);
        
        const histogram = d3.histogram()
            .value(d => d)
            .domain(xScale.domain())
            .thresholds(xScale.ticks(20));
        
        const bins = histogram(numericData);
        
        const yScale = d3.scaleLinear()
            .domain([0, d3.max(bins, d => d.length)])
            .range([height, 0]);
        
        g.append("g")
            .attr("transform", `translate(0,${height})`)
            .call(d3.axisBottom(xScale));
        
        g.append("g")
            .call(d3.axisLeft(yScale));
        
        g.selectAll(".bar")
            .data(bins)
            .enter().append("rect")
            .attr("class", "bar")
            .attr("x", d => xScale(d.x0))
            .attr("y", d => yScale(d.length))
            .attr("width", d => xScale(d.x1) - xScale(d.x0) - 1)
            .attr("height", d => height - yScale(d.length))
            .attr("fill", "steelblue");
        
        container.append("h4").text("ヒストグラム");
    }
}

// 使用例
async function createDataDashboard() {
    const loader = new DataLoader();
    
    try {
        // CSVファイルを読み込み
        await loader.loadCSV("data/sample_data.csv");
        
        // ダッシュボードを作成
        loader.createDashboard("dashboard");
        
        // カスタムフィルタリング
        const filteredData = loader.filterData(d => d.value > 50);
        console.log("フィルタ後のデータ:", filteredData.length, "行");
        
        // データのグループ化
        const groupedData = loader.groupData("category");
        console.log("グループ化されたデータ:", groupedData);
        
    } catch (error) {
        console.error("ダッシュボード作成エラー:", error);
    }
}

createDataDashboard();