D3.js
JavaScript library for data-driven document creation. Creates custom data visualizations using SVG, Canvas, and HTML. Highly flexible industry-standard tool for interactive visualizations.
GitHub Overview
d3/d3
Bring data to life with SVG, Canvas and HTML. :bar_chart::chart_with_upwards_trend::tada:
Topics
Star History
Framework
D3.js
Overview
D3.js (Data-Driven Documents) is a JavaScript library for creating interactive data visualizations in web browsers.
Details
D3.js (D3) is an open-source JavaScript library developed by Mike Bostock in 2011 for creating data visualizations on the web. Standing for "Data-Driven Documents," D3 enables dynamic manipulation of DOM elements based on data to create beautiful and functional visualizations. It leverages SVG, HTML, and CSS to create custom charts and interactive diagrams, supporting everything from basic bar and line charts to complex network diagrams, geographic maps, and tree maps. Key features include data binding (Data Join), scale functions for coordinate transformation, animation capabilities, rich layout algorithms, and powerful event handling. D3 is widely used by major publications like The New York Times and The Guardian, as well as in Observable notebooks, research institutions, and corporate dashboards. It has become the de facto standard for data visualization in journalism and academic research due to its advantages in privacy protection (data doesn't leave the client), low latency (no network communication required), and cross-platform compatibility (browsers, Node.js, React Native).
Pros and Cons
Pros
- Complete Customization: Create unique visualizations without chart library constraints
- Web Standards Based: Built on SVG, HTML, CSS for full web compatibility
- High Performance: Efficiently processes and renders large datasets
- Interactivity: Supports mouse events, animations, and real-time updates
- Rich Layouts: Physics simulations, hierarchical layouts, map projections, etc.
- Data Processing: Built-in CSV, JSON, TSV loading and transformation functions
- Smooth Animations: Fluid transitions and animation effects
Cons
- Steep Learning Curve: High barrier to entry, takes time to master
- Development Time: Longer development time compared to other chart libraries
- Maintenance Complexity: Complex visualizations can be difficult to maintain
- Mobile Support: Touch events and responsive design implementation is complex
- Debugging Difficulty: Complex animations and layouts can be hard to debug
- Bundle Size: Large library size due to comprehensive feature set
Key Links
- D3.js Official Website
- D3.js GitHub Repository
- D3.js API Documentation
- Observable - D3.js Gallery
- D3.js Tutorials
- D3.js Examples
Code Examples
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>
// Basic D3.js usage
console.log("D3.js version:", d3.version);
// Add text to SVG element
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!");
// Draw a circle
d3.select("svg")
.append("circle")
.attr("cx", 200)
.attr("cy", 50)
.attr("r", 20)
.attr("fill", "red");
</script>
</body>
</html>
Creating a Bar Chart
// Data preparation
const data = [
{ name: 'Apple', value: 30 },
{ name: 'Banana', value: 25 },
{ name: 'Cherry', value: 40 },
{ name: 'Date', value: 15 },
{ name: 'Elderberry', value: 35 }
];
// Basic configuration
const margin = { top: 20, right: 30, bottom: 40, left: 90 };
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
// Create SVG element
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})`);
// Set up scales
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);
// Create axes
const xAxis = d3.axisBottom(xScale);
const yAxis = d3.axisLeft(yScale);
// Draw axes
svg.append("g")
.attr("transform", `translate(0,${height})`)
.call(xAxis);
svg.append("g")
.call(yAxis);
// Draw bars
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) {
// Hover effect
d3.select(this).attr("fill", "orange");
// Show tooltip
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");
});
// Animate bars
svg.selectAll(".bar")
.attr("width", 0)
.transition()
.duration(1000)
.attr("width", d => xScale(d.value));
Interactive Scatter Plot
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() {
// Create SVG element
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})`);
// Set up scales
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-axis
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-axis");
// Y-axis
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-axis");
}
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));
// Animation
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) {
// Highlight dot
d3.select(event.target)
.transition()
.duration(200)
.attr("r", this.sizeScale(d.size) * 1.5)
.attr("opacity", 1);
// Show tooltip
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(`Selected data: ${d.label}`);
}
handleBrush(event) {
const selection = event.selection;
if (selection) {
const [[x0, y0], [x1, y1]] = selection;
// Highlight dots within 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;
});
// Output selected data
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;
// Update scales
this.xScale.domain(d3.extent(this.data, d => d.x));
this.yScale.domain(d3.extent(this.data, d => d.y));
// Update axes
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));
// Update dots
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));
}
}
// Usage example
const sampleData = [
{ x: 10, y: 20, size: 15, category: "A", label: "Data 1" },
{ x: 25, y: 30, size: 25, category: "B", label: "Data 2" },
{ x: 40, y: 15, size: 20, category: "A", label: "Data 3" },
{ x: 35, y: 40, size: 30, category: "C", label: "Data 4" },
{ x: 20, y: 35, size: 18, category: "B", label: "Data 5" }
];
const scatterPlot = new InteractiveScatterPlot("scatter-chart", sampleData);
Real-time Data Visualization
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() {
// Create SVG element
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})`);
// Set up scales
this.xScale = d3.scaleTime()
.range([0, this.width]);
this.yScale = d3.scaleLinear()
.range([this.height, 0]);
// Line generator
this.line = d3.line()
.x(d => this.xScale(d.time))
.y(d => this.yScale(d.value))
.curve(d3.curveCardinal);
// Create axes
this.xAxisGroup = this.g.append("g")
.attr("transform", `translate(0,${this.height})`);
this.yAxisGroup = this.g.append("g");
// Create line path
this.path = this.g.append("path")
.attr("class", "line")
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 2);
// Area chart
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);
// Grid lines
this.createGridLines();
// Current value display
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() {
// Horizontal grid lines
this.horizontalGrid = this.g.append("g")
.attr("class", "grid horizontal-grid")
.style("stroke-dasharray", "3,3")
.style("opacity", 0.3);
// Vertical grid lines
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();
// Add new data point
this.data.push({
time: now,
value: value
});
// Remove old data
if (this.data.length > this.maxDataPoints) {
this.data.shift();
}
this.updateChart();
}
updateChart() {
if (this.data.length === 0) return;
// Update scale domains
this.xScale.domain(d3.extent(this.data, d => d.time));
this.yScale.domain(d3.extent(this.data, d => d.value));
// Update axes
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));
// Update grid lines
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("")
);
// Update line
this.path
.datum(this.data)
.transition()
.duration(500)
.attr("d", this.line);
// Update area
this.areaPath
.datum(this.data)
.transition()
.duration(500)
.attr("d", this.area);
// Display current value
const currentValue = this.data[this.data.length - 1].value;
this.currentValueText.text(`Current: ${currentValue.toFixed(2)}`);
// Display data points
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(() => {
// Generate random change
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);
}
}
// Usage example
const realtimeChart = new RealTimeLineChart("realtime-chart");
// Control button examples
document.getElementById("clearButton").addEventListener("click", () => {
realtimeChart.clear();
});
document.getElementById("exportButton").addEventListener("click", () => {
realtimeChart.exportData();
});
Force-Directed Network Simulation
class ForceDirectedNetwork {
constructor(containerId, nodes, links) {
this.containerId = containerId;
this.nodes = nodes;
this.links = links;
this.width = 800;
this.height = 600;
this.init();
}
init() {
// Create SVG element
this.svg = d3.select(`#${this.containerId}`)
.append("svg")
.attr("width", this.width)
.attr("height", this.height);
// Zoom functionality
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");
// Set up force simulation
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() {
// Draw links
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));
// Create node groups
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)));
// Draw node circles
this.nodeElements.append("circle")
.attr("r", d => d.size || 10)
.attr("fill", d => d.color || "#69b3a2")
.attr("stroke", "#fff")
.attr("stroke-width", 2);
// Draw node labels
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");
// Add event handlers
this.nodeElements
.on("mouseover", this.handleMouseOver.bind(this))
.on("mouseout", this.handleMouseOut.bind(this))
.on("click", this.handleClick.bind(this));
}
tick() {
// Update link positions
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);
// Update node positions
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) {
// Highlight node
d3.select(event.currentTarget)
.select("circle")
.transition()
.duration(200)
.attr("r", (d.size || 10) * 1.5)
.attr("fill", "#ff6b6b");
// Highlight connected links
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);
});
// Highlight connected nodes
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) {
// Reset all elements
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);
// Display node details
alert(`Node Information:\nID: ${d.id}\nName: ${d.name}\nSize: ${d.size || 10}`);
}
addNode(node) {
this.nodes.push(node);
this.updateVisualization();
}
addLink(link) {
this.links.push(link);
this.updateVisualization();
}
updateVisualization() {
// Update simulation with new data
this.simulation.nodes(this.nodes);
this.simulation.force("link").links(this.links);
// Update visual elements
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();
// Restart simulation
this.simulation.alpha(1).restart();
}
}
// Usage example
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 Data Loading and Visualization
class DataLoader {
constructor() {
this.data = null;
}
async loadCSV(url) {
try {
this.data = await d3.csv(url, d => {
// Data type conversion
return {
date: d3.timeParse("%Y-%m-%d")(d.date),
value: +d.value,
category: d.category,
name: d.name
};
});
console.log("CSV loaded:", this.data.length, "rows");
return this.data;
} catch (error) {
console.error("CSV loading error:", error);
throw error;
}
}
async loadJSON(url) {
try {
this.data = await d3.json(url);
console.log("JSON loaded:", this.data);
return this.data;
} catch (error) {
console.error("JSON loading error:", 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("Multiple files loaded:", results.length, "files");
return results;
} catch (error) {
console.error("Multiple file loading error:", error);
throw error;
}
}
filterData(filterFunction) {
if (!this.data) {
throw new Error("Data not loaded");
}
return this.data.filter(filterFunction);
}
groupData(groupKey) {
if (!this.data) {
throw new Error("Data not loaded");
}
return d3.group(this.data, d => d[groupKey]);
}
aggregateData(groupKey, valueKey, aggregateFunction = d3.sum) {
if (!this.data) {
throw new Error("Data not loaded");
}
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("Data not loaded");
}
const container = d3.select(`#${containerId}`);
// Display basic statistics
const stats = this.calculateStatistics();
this.displayStatistics(container, stats);
// Create multiple charts
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("Data Statistics");
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("Time Series Chart");
}
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("Category Chart");
}
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("Histogram");
}
}
// Usage example
async function createDataDashboard() {
const loader = new DataLoader();
try {
// Load CSV file
await loader.loadCSV("data/sample_data.csv");
// Create dashboard
loader.createDashboard("dashboard");
// Custom filtering
const filteredData = loader.filterData(d => d.value > 50);
console.log("Filtered data:", filteredData.length, "rows");
// Group data
const groupedData = loader.groupData("category");
console.log("Grouped data:", groupedData);
} catch (error) {
console.error("Dashboard creation error:", error);
}
}
createDataDashboard();