D3.js
データ駆動ドキュメント作成のためのJavaScriptライブラリ。SVG、Canvas、HTMLを用いたカスタムデータビジュアライゼーション作成。柔軟性が高く、インタラクティブな可視化の業界標準ツール。
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
スター履歴
データ取得日時: 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();