Published on

차트 라이브러리 없이 canvas 태그로 차트 그리기

Authors

Canvas 태그 속성과 활용법

HTML5의 canvas 태그는 그래픽을 그릴 수 있는 도구를 제공합니다. canvas 요소는 픽셀 기반의 비트맵 영역을 제공하며, JavaScript를 사용해 이 영역에 그림을 그릴 수 있습니다. canvas를 사용하면 2D 및 3D 그래픽을 생성할 수 있어 다양한 시각적 요소를 동적으로 추가할 수 있습니다.


Canvas 태그

<canvas id="myChart" width="800" height="600"></canvas>

canvas 태그는 width와 height 속성을 사용해 크기를 정의합니다. 이 속성들은 CSS를 통해 스타일링할 수 있지만, 속성 자체를 통해 정의된 크기와 스타일링된 크기 간에 차이가 있을 수 있으므로 주의가 필요합니다.

Canvas 컨텍스트(ctx) 속성 및 메서드

canvas 태그로 그래픽을 그리기 위해서는 getContext('2d') 메서드를 사용해 2D 렌더링 컨텍스트를 가져와야 합니다. 이 컨텍스트 객체는 다양한 그래픽 그리기 메서드와 속성을 제공합니다.

예제 코드

canvas를 이용하여 그린 차트 GIF

아래의 코드는 기본적인 선형 차트를 그리기 위한 전체 코드입니다.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Line Chart</title>
    <style>
      /* 스타일링을 위한 CSS */
      body {
        font-family: Arial, sans-serif;
        margin: 0;
        padding: 0;
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        background-color: #f4f4f4;
      }
      .chart-container {
        width: 80%;
        height: 500px;
        border: 1px solid #ccc;
        background: #fff;
        padding: 20px;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        position: relative;
      }
      canvas {
        width: 100%;
        height: 100%;
      }
      .tooltip {
        position: absolute;
        background: rgba(0, 0, 0, 0.7);
        color: #fff;
        padding: 5px 10px;
        border-radius: 3px;
        pointer-events: none;
        display: none;
      }
    </style>
  </head>
  <body>
    <div class="chart-container">
      <canvas id="myChart"></canvas>
      <div class="tooltip" id="tooltip"></div>
    </div>
    <script>
      // 데이터 정의
      const data = [100, 150, 50, 200, 250, 80]
      const labels = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']

      // Canvas 설정
      const canvas = document.getElementById('myChart')
      const ctx = canvas.getContext('2d')
      const tooltip = document.getElementById('tooltip')

      // 크기 설정
      const width = (canvas.width = canvas.offsetWidth)
      const height = (canvas.height = canvas.offsetHeight)

      // 차트 여백 설정
      const padding = 50
      const chartWidth = width - padding * 2
      const chartHeight = height - padding * 2

      // 데이터 범위 설정
      const maxValue = Math.max(...data)
      const minValue = Math.min(...data)

      // x축 간격 설정
      const xStep = chartWidth / (labels.length - 1)

      // 애니메이션 변수
      let progress = 0
      const animationDuration = 2000 // 애니메이션 지속 시간 (밀리초)
      let startTime = performance.now()
      let currentPointIndex = 0

      // 데이터를 캔버스 좌표로 변환
      function getCanvasCoordinates(index) {
        const x = padding + xStep * index
        const y =
          padding + chartHeight - ((data[index] - minValue) / (maxValue - minValue)) * chartHeight
        return { x, y }
      }

      // 라인 그리기
      function drawLine() {
        ctx.beginPath()
        ctx.moveTo(getCanvasCoordinates(0).x, getCanvasCoordinates(0).y)
        for (let i = 1; i < data.length; i++) {
          ctx.lineTo(getCanvasCoordinates(i).x, getCanvasCoordinates(i).y)
        }
        ctx.strokeStyle = 'rgba(75, 192, 192, 1)'
        ctx.lineWidth = 2
        ctx.stroke()
      }

      // 부드러운 라인 그리기 (애니메이션)
      function drawSmoothLine(animatedProgress) {
        ctx.beginPath()
        ctx.moveTo(getCanvasCoordinates(0).x, getCanvasCoordinates(0).y)

        for (let i = 1; i <= currentPointIndex; i++) {
          const previous = getCanvasCoordinates(i - 1)
          const current = getCanvasCoordinates(i)

          if (i === currentPointIndex) {
            const progressX = previous.x + (current.x - previous.x) * animatedProgress
            const progressY = previous.y + (current.y - previous.y) * animatedProgress
            ctx.lineTo(progressX, progressY)
          } else {
            ctx.lineTo(current.x, current.y)
          }
        }

        ctx.strokeStyle = 'rgba(75, 192, 192, 1)'
        ctx.lineWidth = 2
        ctx.stroke()
      }

      // 애니메이션 루프
      function animateChart(currentTime) {
        const elapsedTime = currentTime - startTime
        const segmentDuration = animationDuration / (data.length - 1)
        progress = (elapsedTime % segmentDuration) / segmentDuration

        if (
          elapsedTime >= segmentDuration * currentPointIndex &&
          currentPointIndex <= data.length - 1
        ) {
          currentPointIndex++
        }

        // 캔버스를 지우고 다시 그리기
        ctx.clearRect(0, 0, canvas.width, canvas.height)
        drawAxes()
        drawLabels()
        drawGridLines()
        drawSmoothLine(progress)

        if (currentPointIndex < data.length - 1 || progress < 1) {
          requestAnimationFrame(animateChart)
        }
      }

      // x축과 y축 그리기
      function drawAxes() {
        ctx.beginPath()
        ctx.moveTo(padding, padding)
        ctx.lineTo(padding, height - padding)
        ctx.lineTo(width - padding, height - padding)
        ctx.strokeStyle = '#000'
        ctx.lineWidth = 1
        ctx.stroke()
      }

      // 축 라벨 그리기
      function drawLabels() {
        ctx.fillStyle = '#000'
        ctx.textAlign = 'center'
        for (let i = 0; i < labels.length; i++) {
          const x = padding + xStep * i
          const y = height - padding + 20
          ctx.fillText(labels[i], x, y)
        }

        // y축 라벨 그리기
        ctx.textAlign = 'right'
        const yStep = (maxValue - minValue) / 5
        for (let i = 0; i <= 5; i++) {
          const y = padding + chartHeight - ((yStep * i) / (maxValue - minValue)) * chartHeight
          ctx.fillText((minValue + yStep * i).toFixed(0), padding - 10, y + 5)

          // 가로선 그리기
          ctx.beginPath()
          ctx.moveTo(padding, y)
          ctx.lineTo(width - padding, y)
          ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)'
          ctx.lineWidth = 1
          ctx.stroke()
        }
      }

      // 차트 그리기 (초기화 및 애니메이션 시작)
      function drawChart() {
        startTime = performance.now()
        currentPointIndex = 0
        progress = 0
        requestAnimationFrame(animateChart)
      }

      // 새로운 함수: 세로선 그리기
      function drawGridLines() {
        ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)'
        ctx.lineWidth = 1
        for (let i = 0; i < labels.length; i++) {
          const x = padding + xStep * i
          ctx.beginPath()
          ctx.moveTo(x, padding)
          ctx.lineTo(x, height - padding)
          ctx.stroke()
        }
      }

      // 툴팁 표시
      function showTooltip(event) {
        const rect = canvas.getBoundingClientRect()
        const mouseX = event.clientX - rect.left
        const mouseY = event.clientY - rect.top
        for (let i = 0; i < data.length; i++) {
          const { x, y } = getCanvasCoordinates(i)
          if (Math.abs(mouseX - x) < 10 && Math.abs(mouseY - y) < 10) {
            tooltip.style.left = `${x}px`
            tooltip.style.top = `${y - 30}px`
            tooltip.innerHTML = `Month: ${labels[i]}<br>Value: ${data[i]}`
            tooltip.style.display = 'block'
            return
          }
        }
        tooltip.style.display = 'none'
      }

      canvas.addEventListener('mousemove', showTooltip)

      drawChart()
    </script>
  </body>
</html>

ctx 객체의 주요 속성과 메서드

strokeStyle: 선의 색상을 지정합니다.

ctx.strokeStyle = 'rgba(75, 192, 192, 1)'

lineWidth: 선의 두께를 지정합니다.

ctx.lineWidth = 2

beginPath(): 새로운 경로를 시작합니다.

ctx.beginPath()

moveTo(x, y): 펜을 특정 좌표로 이동시킵니다.

ctx.moveTo(50, 50)

lineTo(x, y): 현재 위치에서 특정 좌표까지 선을 그립니다.

ctx.lineTo(100, 100)

stroke(): 현재 경로를 따라 선을 그립니다.

ctx.stroke()

fillText(text, x, y): 특정 위치에 텍스트를 그립니다.

ctx.fillText('Hello, World!', 150, 150)

clearRect(x, y, width, height): 특정 영역을 지웁니다.

ctx.clearRect(0, 0, canvas.width, canvas.height)

결론

canvas 태그와 ctx 객체를 사용하면 동적인 그래픽과 애니메이션을 웹 페이지에 추가할 수 있습니다. 위의 예제에서는 애니메이션과 툴팁 기능이 있는 선형 차트를 구현했습니다. 이를 통해 HTML5 캔버스의 다양한 기능과 활용 방법을 이해할 수 있었습니다.