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.

JavaScriptData VisualizationSVGCanvasInteractiveChartsGraphsWeb

GitHub Overview

d3/d3

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

Stars111,068
Watchers3,586
Forks22,848
Created:September 27, 2010
Language:Shell
License:ISC License

Topics

chartchartsd3data-visualizationsvgvisualization

Star History

d3/d3 Star History
Data as of: 7/17/2025, 10:32 AM

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

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();