Scatter Plot
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.
두 d3.scaleLinear() 스케일이 데이터의 X/Y 값을 픽셀 위치로 매핑합니다. 원은 cx, cy, r 속성을 사용합니다. 축은 d3.axisBottom()과 d3.axisLeft()로 생성하며 transform: translate(...)를 적용한 <g> 요소에 추가됩니다. 호버 시 가장 가까운 점의 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} | ${d.hours}h study | 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);