Scatter Plot

Lesson 4

Two d3.scaleLinear() scales map data X/Y values to pixel positions. Circles use cx, cy, r attributes. Axes are created with d3.axisBottom() and d3.axisLeft() appended to <g> elements with transform: translate(...). Hover highlights the nearest point by changing fill opacity.

Source Code script.js
const data = [
  { hours: 2,  score: 45, group: 'morning' },
  { hours: 3,  score: 55, group: 'morning' },
  { hours: 4,  score: 62, group: 'morning' },
  { hours: 5,  score: 70, group: 'morning' },
  { hours: 6,  score: 75, group: 'morning' },
  { hours: 7,  score: 80, group: 'morning' },
  { hours: 8,  score: 85, group: 'morning' },
  { hours: 9,  score: 88, group: 'morning' },
  { hours: 10, score: 92, group: 'morning' },
  { hours: 11, score: 95, group: 'morning' },
  { hours: 2,  score: 40, group: 'evening' },
  { hours: 3,  score: 50, group: 'evening' },
  { hours: 4,  score: 58, group: 'evening' },
  { hours: 5,  score: 65, group: 'evening' },
  { hours: 6,  score: 70, group: 'evening' },
  { hours: 7,  score: 74, group: 'evening' },
  { hours: 8,  score: 79, group: 'evening' },
  { hours: 9,  score: 83, group: 'evening' },
  { hours: 10, score: 88, group: 'evening' },
  { hours: 11, score: 90, group: 'evening' },
];

const margin = { top: 20, right: 30, bottom: 50, left: 55 };
const containerWidth = document.getElementById('chart').clientWidth || 700;
const width = containerWidth - margin.left - margin.right;
const height = 380 - margin.top - margin.bottom;

const tooltip = d3.select('#tooltip');

const svg = d3.select('#chart')
  .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})`);

// Scales
const x = d3.scaleLinear()
  .domain([1, 12])
  .range([0, width]);

const y = d3.scaleLinear()
  .domain([35, 100])
  .range([height, 0]);

// Horizontal grid lines
g.append('g')
  .attr('class', 'grid')
  .call(
    d3.axisLeft(y)
      .tickSize(-width)
      .tickFormat('')
  );

// Vertical grid lines
g.append('g')
  .attr('class', 'grid')
  .attr('transform', `translate(0, ${height})`)
  .call(
    d3.axisBottom(x)
      .tickSize(-height)
      .tickFormat('')
  );

// X axis
g.append('g')
  .attr('class', 'axis')
  .attr('transform', `translate(0, ${height})`)
  .call(d3.axisBottom(x).ticks(11));

// Y axis
g.append('g')
  .attr('class', 'axis')
  .call(d3.axisLeft(y).ticks(8));

// X axis label
g.append('text')
  .attr('class', 'axis-label')
  .attr('x', width / 2)
  .attr('y', height + 42)
  .attr('text-anchor', 'middle')
  .text('Study hours');

// Y axis label
g.append('text')
  .attr('class', 'axis-label')
  .attr('transform', 'rotate(-90)')
  .attr('x', -(height / 2))
  .attr('y', -42)
  .attr('text-anchor', 'middle')
  .text('Score');

// Scatter dots
g.selectAll('.dot-plot')
  .data(data)
  .enter()
  .append('circle')
  .attr('class', 'dot-plot')
  .attr('cx', d => x(d.hours))
  .attr('cy', d => y(d.score))
  .attr('r', 0)
  .attr('fill', d => d.group === 'morning' ? '#222' : '#aaa')
  .attr('fill-opacity', 0.85)
  .on('mouseover', function(event, d) {
    tooltip
      .style('opacity', 1)
      .html(`${d.group} &nbsp;|&nbsp; ${d.hours}h study &nbsp;|&nbsp; score: ${d.score}`);
  })
  .on('mousemove', function(event) {
    tooltip
      .style('left', (event.clientX + 14) + 'px')
      .style('top', (event.clientY - 28) + 'px');
  })
  .on('mouseout', function() {
    tooltip.style('opacity', 0);
  })
  .transition()
  .duration(400)
  .delay((d, i) => i * 20)
  .attr('r', 5);