Files
gh-lyndonkl-claude/skills/d3-visualization/resources/workflows.md
2025-11-30 08:38:26 +08:00

11 KiB

Workflows

Bar Chart Workflow

Problem

Display categorical data with quantitative values as vertical bars.

Steps

  1. Prepare data
const data = [
  {category: 'A', value: 30},
  {category: 'B', value: 80},
  {category: 'C', value: 45}
];
  1. Create SVG container
const margin = {top: 20, right: 20, bottom: 30, left: 40};
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;

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})`);
  1. Create scales
const xScale = d3.scaleBand()
  .domain(data.map(d => d.category))
  .range([0, width])
  .padding(0.1);

const yScale = d3.scaleLinear()
  .domain([0, d3.max(data, d => d.value)])
  .range([height, 0]);
  1. Create axes
svg.append('g')
  .attr('class', 'x-axis')
  .attr('transform', `translate(0, ${height})`)
  .call(d3.axisBottom(xScale));

svg.append('g')
  .attr('class', 'y-axis')
  .call(d3.axisLeft(yScale));
  1. Draw bars
svg.selectAll('rect')
  .data(data)
  .join('rect')
    .attr('x', d => xScale(d.category))
    .attr('y', d => yScale(d.value))
    .attr('width', xScale.bandwidth())
    .attr('height', d => height - yScale(d.value))
    .attr('fill', 'steelblue');

Key Concepts

  • scaleBand: Positions categorical bars with automatic spacing
  • Inverted Y range: [height, 0] puts origin at bottom-left
  • Height calculation: height - yScale(d.value) computes bar height

Line Chart Workflow

Problem

Show trends over time or continuous data.

Steps

  1. Prepare temporal data
const data = [
  {date: new Date(2020, 0, 1), value: 30},
  {date: new Date(2020, 1, 1), value: 80},
  {date: new Date(2020, 2, 1), value: 45}
];
  1. Create scales
const xScale = d3.scaleTime()
  .domain(d3.extent(data, d => d.date))
  .range([0, width]);

const yScale = d3.scaleLinear()
  .domain([0, d3.max(data, d => d.value)])
  .range([height, 0]);
  1. Create line generator
const line = d3.line()
  .x(d => xScale(d.date))
  .y(d => yScale(d.value))
  .curve(d3.curveMonotoneX);
  1. Draw line
svg.append('path')
  .datum(data)
  .attr('d', line)
  .attr('fill', 'none')
  .attr('stroke', 'steelblue')
  .attr('stroke-width', 2);
  1. Add axes
svg.append('g')
  .attr('transform', `translate(0, ${height})`)
  .call(d3.axisBottom(xScale).ticks(5));

svg.append('g')
  .call(d3.axisLeft(yScale));

Key Concepts

  • scaleTime: Handles Date objects automatically
  • d3.extent: Returns [min, max] for domain
  • .datum() vs .data(): Use .datum() for single path, .data() for multiple paths
  • fill='none': Lines need stroke, not fill

Scatter Plot Workflow

Problem

Visualize relationship between two quantitative variables.

Steps

// 1. Prepare data and scales
const data = [{x: 10, y: 20}, {x: 40, y: 90}, {x: 80, y: 50}];

const xScale = d3.scaleLinear()
  .domain([0, d3.max(data, d => d.x)])
  .range([0, width]);

const yScale = d3.scaleLinear()
  .domain([0, d3.max(data, d => d.y)])
  .range([height, 0]);

// 2. Draw points
svg.selectAll('circle')
  .data(data)
  .join('circle')
    .attr('cx', d => xScale(d.x))
    .attr('cy', d => yScale(d.y))
    .attr('r', 5)
    .attr('fill', 'steelblue')
    .attr('opacity', 0.7);

// 3. Add axes
svg.append('g').attr('transform', `translate(0, ${height})`).call(d3.axisBottom(xScale));
svg.append('g').call(d3.axisLeft(yScale));

Key Concepts

  • Both linear scales: X and Y are quantitative
  • Opacity for overlaps: Helps see density

Update Pattern Workflow

Problem

Dynamically update visualization when data changes.

Steps

  1. Initial render
const svg = d3.select('#chart').append('svg')...

function render(data) {
  // Update scale domains
  yScale.domain([0, d3.max(data, d => d.value)]);

  // Update axis
  svg.select('.y-axis')
    .transition()
    .duration(750)
    .call(d3.axisLeft(yScale));

  // Update bars
  svg.selectAll('rect')
    .data(data, d => d.id)  // Key function!
    .join(
      enter => enter.append('rect')
        .attr('x', d => xScale(d.category))
        .attr('y', height)
        .attr('width', xScale.bandwidth())
        .attr('height', 0)
        .attr('fill', 'steelblue')
        .call(enter => enter.transition()
          .duration(750)
          .attr('y', d => yScale(d.value))
          .attr('height', d => height - yScale(d.value))),

      update => update
        .call(update => update.transition()
          .duration(750)
          .attr('y', d => yScale(d.value))
          .attr('height', d => height - yScale(d.value))),

      exit => exit
        .call(exit => exit.transition()
          .duration(750)
          .attr('y', height)
          .attr('height', 0)
          .remove())
    );
}

// Initial render
render(initialData);
  1. Update on new data
// Later, when data changes
render(newData);

Key Concepts

  • Encapsulate in function: Reusable render logic
  • Key function: Tracks element identity across updates
  • Enter/Update/Exit: Custom animations for each lifecycle
  • Update domain first: Recalculate scales before rendering

Network Visualization Workflow

Problem

Display relationships between entities (nodes and links).

Steps

  1. Prepare data
const nodes = [
  {id: 'A', group: 1},
  {id: 'B', group: 1},
  {id: 'C', group: 2}
];

const links = [
  {source: 'A', target: 'B'},
  {source: 'B', target: 'C'}
];
  1. Create force simulation
const simulation = d3.forceSimulation(nodes)
  .force('link', d3.forceLink(links).id(d => d.id).distance(100))
  .force('charge', d3.forceManyBody().strength(-200))
  .force('center', d3.forceCenter(width / 2, height / 2))
  .force('collide', d3.forceCollide().radius(20));
  1. Draw links
const link = svg.append('g')
  .selectAll('line')
  .data(links)
  .join('line')
    .attr('stroke', '#999')
    .attr('stroke-width', 2);
  1. Draw nodes
const node = svg.append('g')
  .selectAll('circle')
  .data(nodes)
  .join('circle')
    .attr('r', 10)
    .attr('fill', d => d.group === 1 ? 'steelblue' : 'orange');
  1. Update positions on tick
simulation.on('tick', () => {
  link
    .attr('x1', d => d.source.x)
    .attr('y1', d => d.source.y)
    .attr('x2', d => d.target.x)
    .attr('y2', d => d.target.y);

  node
    .attr('cx', d => d.x)
    .attr('cy', d => d.y);
});
  1. Add drag behavior
function drag(simulation) {
  function dragstarted(event) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    event.subject.fx = event.subject.x;
    event.subject.fy = event.subject.y;
  }

  function dragged(event) {
    event.subject.fx = event.x;
    event.subject.fy = event.y;
  }

  function dragended(event) {
    if (!event.active) simulation.alphaTarget(0);
    event.subject.fx = null;
    event.subject.fy = null;
  }

  return d3.drag()
    .on('start', dragstarted)
    .on('drag', dragged)
    .on('end', dragended);
}

node.call(drag(simulation));

Key Concepts

  • forceSimulation: Physics-based layout
  • tick handler: Updates positions every frame
  • id accessor: Links reference nodes by ID
  • fx/fy: Fixed positions during drag

Hierarchy Visualization (Treemap) Workflow

Problem

Show hierarchical data with nested rectangles.

Steps

// 1. Prepare and process hierarchy
const data = {name: 'root', children: [{name: 'A', value: 100}, {name: 'B', value: 200}]};

const root = d3.hierarchy(data)
  .sum(d => d.value)
  .sort((a, b) => b.value - a.value);

// 2. Apply layout
d3.treemap().size([width, height]).padding(1)(root);

// 3. Draw rectangles
const colorScale = d3.scaleOrdinal(d3.schemeCategory10);

svg.selectAll('rect')
  .data(root.leaves())
  .join('rect')
    .attr('x', d => d.x0)
    .attr('y', d => d.y0)
    .attr('width', d => d.x1 - d.x0)
    .attr('height', d => d.y1 - d.y0)
    .attr('fill', d => colorScale(d.parent.data.name));

Key Concepts

  • d3.hierarchy: Creates hierarchy from nested object
  • .sum(): Aggregates values up tree
  • x0, y0, x1, y1: Layout computes rectangle bounds

Geographic Map (Choropleth) Workflow

Problem

Visualize regional data on a map.

Steps

// 1. Load data
Promise.all([d3.json('countries.geojson'), d3.csv('data.csv')])
  .then(([geojson, csvData]) => {
    // 2. Setup projection and path
    const projection = d3.geoMercator().fitExtent([[0, 0], [width, height]], geojson);
    const path = d3.geoPath().projection(projection);

    // 3. Create color scale and data lookup
    const colorScale = d3.scaleSequential(d3.interpolateBlues)
      .domain([0, d3.max(csvData, d => +d.value)]);
    const dataById = new Map(csvData.map(d => [d.id, +d.value]));

    // 4. Draw map
    svg.selectAll('path')
      .data(geojson.features)
      .join('path')
        .attr('d', path)
        .attr('fill', d => dataById.get(d.id) ? colorScale(dataById.get(d.id)) : '#ccc')
        .attr('stroke', '#fff');
  });

Key Concepts

  • fitExtent: Auto-scales projection to fit bounds
  • geoPath: Converts GeoJSON to SVG paths
  • Map for lookup: Fast data join by ID

Real-Time Updates Workflow

Problem

Continuously update visualization with streaming data.

Steps

// 1. Setup sliding window
const maxPoints = 50;
let data = [];

function update(newValue) {
  data.push({time: new Date(), value: newValue});
  if (data.length > maxPoints) data.shift();

  // 2. Update scales and render
  xScale.domain(d3.extent(data, d => d.time));
  yScale.domain([0, d3.max(data, d => d.value)]);

  svg.select('.x-axis').transition().duration(200).call(d3.axisBottom(xScale));
  svg.select('.y-axis').transition().duration(200).call(d3.axisLeft(yScale));

  const line = d3.line().x(d => xScale(d.time)).y(d => yScale(d.value));
  svg.select('.line').datum(data).transition().duration(200).attr('d', line);
}

// 3. Poll data
setInterval(() => update(Math.random() * 100), 1000);

Key Concepts

  • Sliding window: Fixed-size buffer
  • Short transitions: 200ms feels responsive

Linked Views Workflow

Problem

Coordinate highlighting across multiple charts.

Steps

// 1. Shared event handlers
function highlight(id) {
  svg1.selectAll('circle').attr('opacity', d => d.id === id ? 1 : 0.3);
  svg2.selectAll('rect').attr('opacity', d => d.id === id ? 1 : 0.3);
}

function unhighlight() {
  svg1.selectAll('circle').attr('opacity', 1);
  svg2.selectAll('rect').attr('opacity', 1);
}

// 2. Attach to elements
svg1.selectAll('circle')
  .data(data).join('circle')
  .on('mouseover', (event, d) => highlight(d.id))
  .on('mouseout', unhighlight);

svg2.selectAll('rect')
  .data(data).join('rect')
  .on('mouseover', (event, d) => highlight(d.id))
  .on('mouseout', unhighlight);

Key Concepts

  • Shared state: Common ID across views
  • Coordinated updates: Single event updates all charts

Next Steps