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

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

YS
김영삼
조회 197

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로 등록