Skill: wu-uk/data-to-d3-d3-visualization (version 0.1.0)

--- name: d3js-visualization description: Build deterministic, verifiable data visualizations with D3.js (v6). Generate standalone HTML/SVG (and optional PNG) from local data files without external network dependencies. Use when tasks require charts, plots, axes/scales, legends, tooltips, or data-driven SVG output. --- # D3.js Visualization Skill Use this skill to turn structured data (CSV/TSV/JSON) into **clean, reproducible** visualizations using **D3.js**. The goal is to produce **stable outputs** that can be verified by diffing files or hashing. ## When to use Activate this skill when the user asks for any of the following: - “Make a chart/plot/graph/visualization” - bar/line/scatter/area/histogram/box/violin/heatmap - timelines, small multiples, faceting - axis ticks, scales, legends, tooltips - data-driven SVG output for a report or web page - converting data to a static SVG or HTML visualization If the user only needs a quick table or summary, **don’t** use D3—use a spreadsheet or plain markdown instead. --- ## Inputs you should expect - One or more local data files: `*.csv`, `*.tsv`, `*.json` - A chart intent: - chart type (or you infer the best type) - x/y fields and aggregation rules - sorting/filtering rules - dimensions (width/height) and margins - color rules (categorical / sequential) - any labeling requirements (title, axis labels, units) - Output constraints: - “static only”, “no animation”, “must be deterministic”, “offline”, etc. If details are missing, **make reasonable defaults** and document them in comments near the top of the output file. --- ## Outputs you should produce Prefer producing **all of** the following when feasible: 1. `dist/chart.html` — standalone HTML that renders the visualization 2. `dist/chart.svg` — exported SVG (stable and diff-friendly) 3. (Optional) `dist/chart.png` — if the task explicitly needs a raster image Always keep outputs in a predictable folder (default: `dist/`), unless the task specifies paths. --- ## Determinism rules (non-negotiable) To keep results stable across runs and machines: ### Data determinism - **Sort** input rows deterministically before binding to marks (e.g., by x then by category). - Use stable grouping order (explicit `Array.from(grouped.keys()).sort()`). - Avoid locale-dependent formatting unless fixed (use `d3.format`, `d3.timeFormat` with explicit formats). ### Rendering determinism - **No randomness**: do not use `Math.random()` or `d3-random`. - **No transitions/animations** by default (transitions can introduce timing variance). - **Fixed** `width`, `height`, `margin`, `viewBox`. - Use **explicit tick counts** only when needed; otherwise rely on D3 defaults but keep domains fixed. - Avoid layout algorithms with non-deterministic iteration unless you control seeds/iterations (e.g., force simulation). If a force layout is required: - fix the tick count, - fix initial positions deterministically (e.g., sorted nodes placed on a grid), - run exactly N ticks and stop. ### Offline + dependency determinism - Do **not** load D3 from a CDN. - Pin D3 to a specific version (default: **d3@7.9.0**). - Prefer vendoring a minified D3 bundle (e.g., `vendor/d3.v7.9.0.min.js`) or bundling with a lockfile. ### File determinism - Stable SVG output: - Avoid auto-generated IDs that may change. - If you must use IDs (clipPath, gradients), derive them from stable strings (e.g., `"clip-plot"`). - Use LF line endings. - Keep numeric precision consistent (e.g., round to 2–4 decimals if needed). --- ## Recommended project layout If the task doesn't specify an existing structure, use: ``` dist/ chart.html # standalone HTML with inline or linked JS/CSS chart.svg # exported SVG (optional but nice) chart.png # rasterized (optional) vendor/ d3.v7.9.0.min.js # pinned D3 library ``` --- ## Interactive features (tooltips, click handlers, hover effects) When the task requires interactivity (e.g., tooltips on hover, click to highlight): ### Tooltip pattern (recommended) 1. **Create a tooltip element** in HTML: ```html <div id="tooltip" class="tooltip"></div> ``` 2. **Style with CSS** using `.visible` class for show/hide: ```css .tooltip { position: absolute; padding: 10px; background: rgba(0, 0, 0, 0.8); color: white; border-radius: 4px; pointer-events: none; /* Prevent mouse interference */ opacity: 0; transition: opacity 0.2s; z-index: 1000; } .tooltip.visible { opacity: 1; /* Show when .visible class is added */ } ``` 3. **Add event handlers** to SVG elements: ```javascript svg.selectAll('circle') .on('mouseover', function(event, d) { d3.select('#tooltip') .classed('visible', true) // Add .visible class .html(`<strong>${d.name}</strong><br/>${d.value}`) .style('left', (event.pageX + 10) + 'px') .style('top', (event.pageY - 10) + 'px'); }) .on('mouseout', function() { d3.select('#tooltip').classed('visible', false); // Remove .visible class }); ``` **Key points:** - Use `opacity: 0` by default (not `display: none`) for smooth transitions - Use `.classed('visible', true/false)` to toggle visibility - `pointer-events: none` prevents tooltip from blocking mouse events - Position tooltip relative to mouse with `event.pageX/pageY` ### Click handlers for selection/highlighting ```javascript // Add 'selected' class on click svg.selectAll('.bar') .on('click', function(event, d) { // Remove previous selection d3.selectAll('.bar').classed('selected', false); // Add to clicked element d3.select(this).classed('selected', true); }); ``` CSS for highlighting: ```css .bar.selected { stroke: #000; stroke-width: 3px; } ``` ### Conditional interactivity Sometimes only certain elements should be interactive: ```javascript .on('mouseover', function(event, d) { // Example: Don't show tooltip for certain categories if (d.category === 'excluded') { return; // Exit early, no tooltip } // Show tooltip for others showTooltip(event, d); }) ``` ---

path: scripts/bubble_chart_example.js
content: /**
 * Complete Bubble Chart Example with Force Simulation
 *
 * This example shows how to create a clustered bubble chart with:
 * - Force-directed layout
 * - Collision detection
 * - Color coding by category
 * - Size scaling by value
 */

// Configuration
const width = 800;
const height = 600;
const margin = {top: 20, right: 20, bottom: 20, left: 20};

// Create SVG
const svg = d3.select('#chart')
    .append('svg')
    .attr('width', width)
    .attr('height', height);

// Load and process data
d3.csv('/data/stocks.csv').then(data => {
    // Parse numbers
    data.forEach(d => {
        d.value = d.marketCap ? +d.marketCap : null;
        d.ticker = d.ticker;
        d.sector = d.sector;
    });

    createBubbleChart(data);
});

function createBubbleChart(data) {
    // Setup scales
    const radiusScale = d3.scaleSqrt()
        .domain([0, d3.max(data, d => d.value || 0)])
        .range([5, 50]);

    const colorScale = d3.scaleOrdinal()
        .domain([...new Set(data.map(d => d.sector))])
        .range(d3.schemeCategory10);

    // Calculate radius for each data point
    data.forEach(d => {
        if (d.value === null || isNaN(d.value)) {
            d.radius = 10; // Uniform size for missing data
            d.hasValue = false;
        } else {
            d.radius = radiusScale(d.value);
            d.hasValue = true;
        }
    });

    // Create force simulation
    const simulation = d3.forceSimulation(data)
        .force('center', d3.forceCenter(width / 2, height / 2))
        .force('collide', d3.forceCollide(d => d.radius + 2))
        .force('charge', d3.forceManyBody().strength(-30));

    // Optional: Cluster by sector
    const sectors = [...new Set(data.map(d => d.sector))];
    const sectorPositions = {};
    sectors.forEach((sector, i) => {
        const angle = (i / sectors.length) * 2 * Math.PI;
        sectorPositions[sector] = {
            x: width / 2 + Math.cos(angle) * 150,
            y: height / 2 + Math.sin(angle) * 150
        };
    });

    simulation
        .force('x', d3.forceX(d => sectorPositions[d.sector].x).strength(0.5))
        .force('y', d3.forceY(d => sectorPositions[d.sector].y).strength(0.5));

    // Create bubbles
    const bubbles = svg.selectAll('circle')
        .data(data)
        .join('circle')
        .attr('class', 'bubble')
        .attr('r', d => d.radius)
        .attr('fill', d => colorScale(d.sector))
        .attr('opacity', 0.7)
        .on('mouseover', function(event, d) {
            // Only show tooltip if has complete data
            if (d.sector !== 'ETF') {
                showTooltip(event, d);
            }
        })
        .on('mouseout', hideTooltip)
        .on('click', function(event, d) {
            selectBubble(d.ticker);
        });

    // Update positions on each tick
    simulation.on('tick', () => {
        bubbles
            .attr('cx', d => d.x)
            .attr('cy', d => d.y);
    });
}

function showTooltip(event, d) {
    const tooltip = d3.select('#tooltip');
    tooltip
        .classed('visible', true)
        .html(`
            <strong>${d.ticker}</strong><br/>
            ${d.name || 'N/A'}<br/>
            Sector: ${d.sector}
        `)
        .style('left', (event.pageX + 10) + 'px')
        .style('top', (event.pageY - 10) + 'px');
}

function hideTooltip() {
    d3.select('#tooltip').classed('visible', false);
}

function selectBubble(ticker) {
    // Highlight selected bubble
    d3.selectAll('.bubble')
        .classed('selected', d => d.ticker === ticker);

    // Trigger table highlight if exists
    if (typeof highlightTableRow === 'function') {
        highlightTableRow(ticker);
    }
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)