본문 바로가기
Frontend2025년 2월 5일7분 읽기

Canvas API로 인터랙티브 차트 직접 구현하기

YS
김영삼
조회 217
Canvas API로 인터랙티브 차트 직접 구현하기

Canvas API 기본 이해

HTML5 Canvas는 JavaScript를 통해 2D 그래픽을 직접 그릴 수 있는 래스터 기반 요소입니다. Chart.js나 D3 같은 라이브러리 없이도 Canvas API만으로 충분히 인터랙티브한 차트를 만들 수 있습니다. Canvas의 좌표 체계와 렌더링 사이클을 이해하면 자유도 높은 시각화를 구현할 수 있습니다.

Canvas 초기 설정

const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');

// 고해상도 디스플레이 대응
const dpr = window.devicePixelRatio || 1;
canvas.width = canvas.clientWidth * dpr;
canvas.height = canvas.clientHeight * dpr;
ctx.scale(dpr, dpr);

// 차트 영역 정의
const padding = { top: 40, right: 30, bottom: 60, left: 70 };
const chartWidth = canvas.clientWidth - padding.left - padding.right;
const chartHeight = canvas.clientHeight - padding.top - padding.bottom;

바 차트 그리기

바 차트는 카테고리별 값을 비교하는 데 효과적입니다. X축에 카테고리, Y축에 값을 매핑하고 requestAnimationFrame을 활용하면 애니메이션까지 구현할 수 있습니다.

function drawBarChart(data) {
  const maxVal = Math.max(...data.map(d => d.value));
  const barWidth = chartWidth / data.length * 0.7;
  const gap = chartWidth / data.length * 0.3;

  // Y축 그리드 라인
  ctx.strokeStyle = '#e0e0e0';
  ctx.lineWidth = 0.5;
  for (let i = 0; i <= 5; i++) {
    const y = padding.top + (chartHeight / 5) * i;
    ctx.beginPath();
    ctx.moveTo(padding.left, y);
    ctx.lineTo(padding.left + chartWidth, y);
    ctx.stroke();
  }

  // 바 그리기
  data.forEach((item, i) => {
    const x = padding.left + i * (barWidth + gap) + gap / 2;
    const barHeight = (item.value / maxVal) * chartHeight;
    const y = padding.top + chartHeight - barHeight;

    // 그라디언트 적용
    const gradient = ctx.createLinearGradient(x, y, x, y + barHeight);
    gradient.addColorStop(0, '#4F46E5');
    gradient.addColorStop(1, '#7C3AED');
    ctx.fillStyle = gradient;
    ctx.fillRect(x, y, barWidth, barHeight);

    // 라벨
    ctx.fillStyle = '#374151';
    ctx.font = '12px sans-serif';
    ctx.textAlign = 'center';
    ctx.fillText(item.label, x + barWidth / 2, padding.top + chartHeight + 20);
  });
}

마우스 인터랙션 추가

canvas.addEventListener('mousemove', (e) => {
  const rect = canvas.getBoundingClientRect();
  const mouseX = e.clientX - rect.left;
  const mouseY = e.clientY - rect.top;

  // 어떤 바 위에 있는지 확인
  const hoveredBar = data.findIndex((item, i) => {
    const x = padding.left + i * (barWidth + gap) + gap / 2;
    const barHeight = (item.value / maxVal) * chartHeight;
    const y = padding.top + chartHeight - barHeight;
    return mouseX >= x && mouseX <= x + barWidth
        && mouseY >= y && mouseY <= y + chartHeight;
  });

  if (hoveredBar !== -1) {
    canvas.style.cursor = 'pointer';
    showTooltip(mouseX, mouseY, data[hoveredBar]);
  } else {
    canvas.style.cursor = 'default';
    hideTooltip();
  }
});

라인 차트와 애니메이션

라인 차트에서는 점 사이를 곡선으로 잇는 Bezier 커브와 requestAnimationFrame 기반 애니메이션이 핵심입니다.

function animateLineChart(data, progress = 0) {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  drawAxes();

  const visiblePoints = Math.floor(data.length * progress);

  ctx.beginPath();
  ctx.strokeStyle = '#10B981';
  ctx.lineWidth = 2;

  for (let i = 0; i < visiblePoints; i++) {
    const x = padding.left + (i / (data.length - 1)) * chartWidth;
    const y = padding.top + chartHeight - (data[i] / maxVal) * chartHeight;
    if (i === 0) ctx.moveTo(x, y);
    else ctx.lineTo(x, y);
  }
  ctx.stroke();

  if (progress < 1) {
    requestAnimationFrame(() => animateLineChart(data, progress + 0.02));
  }
}

성능 최적화 팁

기법설명효과
Off-screen Canvas정적 요소를 별도 Canvas에 미리 렌더링CPU 부하 50% 감소
Dirty Region변경된 영역만 다시 그리기대규모 차트에서 필수
Path2D 캐싱반복 그려지는 도형을 Path2D 객체로 캐싱hitTest 성능 향상
requestAnimationFramesetInterval 대신 rAF 사용프레임 드롭 방지

반응형 리사이즈 처리

const resizeObserver = new ResizeObserver(entries => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    canvas.width = width * dpr;
    canvas.height = height * dpr;
    ctx.scale(dpr, dpr);
    redrawChart();
  }
});
resizeObserver.observe(canvas.parentElement);

Canvas API를 직접 다루면 번들 사이즈를 크게 줄이면서도 완전한 커스터마이징이 가능합니다. 프로덕션에서는 접근성을 위해 ARIA 속성과 대체 데이터 테이블을 함께 제공하는 것을 권장합니다.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록