GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

canvas 그래프 그리기 — 라이브러리 없이 그리는 애니메이션 차트 9종

canvas 그래프 그리기는 데이터 배열을 x·y 좌표로 바꿔 캔버스 위에 선·막대·원호로 얹는 일입니다. 라이브러리와 CDN 없이 값이 그려지고 차오르고 모여드는 차트 아홉 종을 만들었고, 파일만 열면 복사한 그대로 돌아갑니다.

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

순서는 난이도가 아니라 캔버스에 잉크를 놓는 도구가 하나씩 늘어나는 순서입니다. 획 하나로 시작한 선(01)이 직사각형 채움을 얻어 막대가 되고(02), 원호를 얻어 도넛이 됩니다(03). 여기서 점 40개를 흩뿌리면 산포도(04), 면 두 장을 겹치면 영역 그래프(05)로 확장됩니다. 다음 넷은 그 잉크를 어떻게 움직이는가입니다 — 바늘 하나가 감쇠 진동하며 값 하나를 말하고(06), 시간 창이 밀리며 미니 선 다섯이 살아 숨 쉬고(07), 다각형이 정점마다 다른 속도로 변형하며(08), 마지막으로 56개의 셀이 파도를 이루며 채워집니다(09). 그리는 쪽이 아니라 픽셀을 계산하는 쪽이 궁금하다면 html5 canvas 애니메이션 9종에서 물방울·블랙홀 장면 효과를, 벡터로 그리는 쪽은 css 그래프 만들기 SVG로 9종에서, 이미 그려진 차트를 마우스로 만지는 쪽은 대시보드 차트 디자인 인터랙션 9종에서 다룹니다.

01선 그래프 그리기 — 클립이 열리며 드러나는 12주

무엇이 보이나

다시 그리기 단추를 누르면 12주 판매량 [128, 176, 142, 210, 248, 204, 292, 336, 288, 372, 414, 468] 이 왼쪽에서 오른쪽으로 이어서 그려지고, 펜이 지나온 꼭짓점마다 종이색 마커가 박힌 뒤 끝점이 반지름 2.6에서 5.5로 커지며 멈춥니다.

선 그래프 그리기
새 창에서 열기

01 · 선 그래프 그리기 — 측정 누적면적 20.228% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

quadraticCurveTocliplineTo

계산을 볼 곳

2초 루프에서 움직인 프레임 23/24 · 누적면적 20.228% 를 기록했고, 주간·월간 지표 보고서의 첫 줄에 어울립니다 — 진행 p를 주 11개 구간에 얹어 현재 변과 끝점 좌표를 계산하는 것이 이 애니메이션의 심장입니다.

// 그리기 진행 p(0..1) → { i, k, ex, ey, f }
//   f = 0..11 정규화 경과, i·k = 현재 변, (ex,ey) = 끝점 좌표
function headAt(p) {
  var n = WEEKS.length;
  var f = Math.min(1, p / DRAW_AT) * (n - 1);
  var i = Math.min(n - 2, Math.floor(f));
  var k = f - i;
  var ex = xAt(i) + (xAt(i + 1) - xAt(i)) * k;
  var ey = yAt(WEEKS[i]) + (yAt(WEEKS[i + 1]) - yAt(WEEKS[i])) * k;
  return { i: i, k: k, f: f, ex: ex, ey: ey };
}
// 지나온 꼭짓점은 제어점으로 삼아 다음 꼭짓점과의 중점까지 quadraticCurveTo 로 누른다(정점을 스치듯)
function tracePath(p) {
  var d = headAt(p);
  ctx.beginPath();
  ctx.moveTo(xAt(0), yAt(WEEKS[0]));
  for (var j = 1; j < d.i; j++) {
    ctx.quadraticCurveTo(xAt(j), yAt(WEEKS[j]), (xAt(j) + xAt(j + 1)) / 2, (yAt(WEEKS[j]) + yAt(WEEKS[j + 1])) / 2);
  }
  if (d.i >= 1) {
    ctx.quadraticCurveTo(xAt(d.i), yAt(WEEKS[d.i]), d.ex, d.ey);
  } else {
    ctx.lineTo(d.ex, d.ey);
  }
  return d;
}

꼭짓점을 제어점으로 삼아 인접 꼭짓점과의 중점까지 곡선을 누르는 이 방식은 정점을 스치고 지나가므로, 계단처럼 튀지 않으면서도 데이터 위치를 정확히 지키고 클리핑 사각형이 같은 속도로 열려 선이 지나온 순서대로 드러납니다.

02막대 그래프 성장 — 시차로 번지는 파도

무엇이 보이나

검색 420 · 소셜 310 · 이메일 265 · 직접 195 · 파트너 140(만원) 다섯 막대가 전체의 7.5% 간격으로 시차를 두고 바닥에서 자라 파도를 이루고, 착지한 막대부터 값 레이블이 카운트업됩니다.

막대 그래프 성장
새 창에서 열기

02 · 막대 그래프 성장 — 측정 누적면적 14.631% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

staggereaseOutCubicfillRect

계산을 볼 곳

움직인 프레임 23/24 · 누적면적 14.631% · 강도 평균 118.5 를 기록했고, 채널·지점 비교 대시보드에 씁니다 — html 막대 그래프 라면 누구나 떠올리는 이 모양이 캔버스에서는 fillRect 네 번의 인자 계산이라는 점을 코드가 보여 줍니다.

// 막대 다섯 개 — i 번째는 i*STAGGER 에 시작해 GROW 동안 자란다(stagger 파도)
for (var i = 0; i < n; i++) {
  var row = ROWS[i];
  var cx = PAD_L + slot * (i + 0.5);
  var fullH = plotH * (row.v / 450);
  var local = Math.max(0, Math.min(1, (p - i * STAGGER) / GROW));
  var eased = easeOutCubic(local);
  var h = fullH * eased;
  var y = baseY - h;
  ctx.fillStyle = row.c;
  ctx.fillRect(cx - barW / 2, y, barW, h);
}

막대 하나가 자라는 구간 GROW는 전체 2초의 46%, 시차 STAGGER는 7.5%라 마지막 막대는 0.30 지점에 출발하고 폭은 min(44, max(18, slot * 0.62)) 로 상하한을 두어 채널이 늘어나도 빗살처럼 얇아지지 않게 했습니다.

03도넛 그래프 스윕 — 끝각에서 출발하는 다음 조각

무엇이 보이나

검색 10,800(45%) · 소셜 8,400(35%) · 직접 4,800(20%) 세 조각이 12시 방향부터 이어져 멈춤 없이 흐르듯 채워지고, 링이 다 찬 뒤 중앙의 「채우는 중」 이 총 방문 24,000 으로 세로 슬라이드로 뒤집힙니다.

도넛 그래프 스윕
새 창에서 열기

03 · 도넛 그래프 스윕 — 측정 누적면적 11.843% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

arcstartAngleendAngle

계산을 볼 곳

움직인 프레임 23/24 · 누적면적 11.843% · 강도 평균 169.0 — 아홉 개 중 가장 강한 변화율을 기록했고, 유입 경로·구성비를 한눈에 보는 자리에 씁니다 — 진행을 점유합에 분배해 현재 조각의 끝각이 다음 조각의 시작각이 되는 구조입니다.

var q = sweepAngle(p);
var acc = 0;
for (var i = 0; i < SRC.length; i++) {
  var share = SRC[i].v / TOTAL;
  var segEnd = acc + share;
  if (q >= segEnd) {
    var a0 = START + acc * TAU + GAP / 2;
    var a1 = START + segEnd * TAU - GAP / 2;
    slice(cx, cy, R, ring, a0, a1, SRC[i].c, 1);
    acc = segEnd;
  } else if (q > acc) {
    var a1c = START + q * TAU;
    slice(cx, cy, R, ring, START + acc * TAU, a1c, SRC[i].c, 1);
    acc = segEnd;
  } else {
    break;
  }
}

조각 사이 틈은 붙어 있으면 경계가 읽히지 않기 때문에 0.035 라디안으로 고정했고, 트랙(옅게 깔아 두는 자리)도 첫 프레임에 몰아 깔지 않고 펜이 지나가는 만큼만 열립니다.

04산포 그래프 등장 — lerp 로 모여드는 40개

무엇이 보이나

40개 상품 점이 흩어진 자리에서 시작해 반복 보간으로 가격(0.5~19.5만원)-리뷰수(10~480) 평면의 제자리로 모여들고, 마우스를 올리면 가장 가까운 점이 링에 잡히며 상품명과 가격·리뷰수가 뜹니다.

산포 그래프 등장
새 창에서 열기

04 · 산포 그래프 등장 — 측정 누적면적 19.836% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

lerpMath.expdevicePixelRatio

계산을 볼 곳

움직인 프레임 21/24 · 누적면적 19.836% 를 기록했고, 가격-만족도 같은 상관 관계를 탐색하는 자리에 씁니다 — 점마다 수렴 속도 lam 을 3.0~6.0 으로 다르게 줘 일제히 도착하지 않고 쏟아져 내리듯 멈춥니다.

// 닫힌형 lerp 수렴 — 경과시간 e(초)에서 점 위치를 결정적으로 계산해 cx·cy 에 저장, 진행도 t 반환
function converge(pt, e) {
  var t = 1 - Math.exp(-pt.lam * e);
  var x0 = PAD_L + (W - PAD_L - PAD_R) * pt.sx;
  var y0 = PAD_T + (H - PAD_T - PAD_B) * pt.sy;
  pt.cx = x0 + (px(pt.price) - x0) * t;
  pt.cy = y0 + (py(pt.rv) - y0) * t;
  return t;
}

40개의 목표 좌표와 시작 좌표는 시드 3050416 의 LCG(곱 48271 · 모듈러스 2147483647) 호출 순서가 고정이라, 누가 언제 열어도 같은 그림이 재현됩니다 — 수렴은 1.6초, 전체 루프 2.4초입니다.

05영역 그래프 채움 — 교차하는 달이 밝아지는 두 계열

무엇이 보이나

방문 [210, 248, 226, 292, 334, 306, 368, 412, 384, 446, 482, 456] 과 주문 [96, 118, 142, 176, 208, 236, 272, 308, 344, 386, 424, 468] 두 계열의 면이 바닥에서 위로 차오르며, 주문이 방문을 앞지르는 11→12월 구간의 위 계열이 한 단 밝아지고, 채움이 끝나면 평균선이 그 위를 가로지릅니다.

영역 그래프 채움
새 창에서 열기

05 · 영역 그래프 채움 — 측정 누적면적 25.02% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

globalAlphaclipquadraticCurveTo

계산을 볼 곳

움직인 프레임 23/24 · 누적면적 25.02% 를 기록했고, 목표 대비 실적·누적 추이에 씁니다 — 감속 이징 하나로 클리핑 상승과 면 높이를 함께 누르는 것이 이 데모의 계산입니다.

// 채움 진행률 — 감속 이징 하나로 클립과 면 높이를 함께 누른다
var f = easeOutCubic(Math.min(1, p / FILL_AT));
var s = f * f;                                   // 면 높이 — 시작이 급하고 끝에 붙는 감
var cutY = (H - PAD_B) - (H - PAD_T - PAD_B) * f;

ctx.save();
ctx.beginPath();
ctx.rect(0, cutY - 1, W, H);
ctx.clip();

areaPath(B, s);
ctx.fillStyle = C_2F;
ctx.fill();
areaPath(A, s);
ctx.fillStyle = C_1F;
ctx.fill();

채움은 전체 2초의 85% 지점에 끝나고(FILL_AT) 평균선은 93%(LINE_AT) 에 확정되며, 면 높이를 f * f 로 한 번 더 눌러 시작이 급하고 끝에 붙는 무게감을 냈습니다.

06게이지 바늘 회전 — 목표를 지나쳐 돌아오는 감쇠 진동

무엇이 보이나

값 72 를 가리키는 바늘이 목표각을 지나쳤다가 스프링 감쇠로 되돌아와 멈추고, 눈금 세 구간(0-40-70-100)은 바늘이 닿은 만큼만 채색되며 값은 최종 각도에서 확정됩니다.

게이지 바늘 회전
새 창에서 열기

06 · 게이지 바늘 회전 — 측정 누적면적 10.783% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

Math.cosarctextAlign

계산을 볼 곳

움직인 프레임 21/24 · 누적면적 10.783% · 강도 평균 119.3 을 기록했고, KPI 달성률·서비스 상태 표시에 씁니다 — 오버슈트를 CSS 이징이 아니라 물리식, 감쇠 지수와 진동 각진동수로 계산한 것이 특징입니다.

// 스윕 진행 u(0..1) → 바늘 값(0~100+). 감쇠 사인파로 목표를 지나쳤다 되돌아온다:
//   목표 이상 성분 VALUE * (1 - e^(-DECAY*u) * cos(OMEGA*u)) — u=0 에서 0, u→1 에서 VALUE 로 수렴
function needleVal(u) {
  return VALUE * (1 - Math.exp(-DECAY * u) * Math.cos(OMEGA * u));
}

// 값(0~100) → 캔버스 각도(라디안). 캔버스는 y 축이 아래로 열려 있으므로
// 위쪽 반원은 PI(왼쪽) → 1.5PI(꼭대기) → 2PI(오른쪽) 순으로 돈다 — 시계 방향(각도 증가)
function angOf(v) { return Math.PI + Math.PI * (v / 100); }
function pt(v, r) {
  var a = angOf(v);
  return { x: cx() + Math.cos(a) * r, y: cy() + Math.sin(a) * r };
}

감쇠 계수 DECAY 5.2 와 진동 각진동수 OMEGA 10.5 가 바늘이 몇 번 흔들릴지를 정하는데, 값이 커질수록 진폭이 줄어드는 지수 감쇠라 마지막 흔들림은 거의 눈에 띄지 않습니다.

07스파크라인 흐름 — 창을 밀며 사는 다섯 카드

무엇이 보이나

방문자·체류·주문·쿠폰·읽기 다섯 지표 카드의 미니 선이 2초 루프 안에서 0.28 · 0.55 · 0.82 세 지점에 창을 밀어 새 값을 받고, 마지막 점이 값 도착 직후 세게 맥박치다 잔 진동만 남기며 값이 갱신됩니다.

스파크라인 흐름
새 창에서 열기

07 · 스파크라인 흐름 — 측정 누적면적 11.706% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

easeOutCubicrequestAnimationFramepath

계산을 볼 곳

움직인 프레임 23/24 · 누적면적 11.706% 를 기록했고, 목록 행·카드 안의 미니 지표에 씁니다 — 카드 틀·라인·숫자까지 전부 캔버스가 그리며, 넓을 때 2+3 두 줄 · 좁으면 세로 한 줄로 스스로 배치를 바꿉니다.

// 카드 i 의 루프 시각 p(0..1) 상태 — 몇 번 밀렸는지(shift)와 현재 밀기 진행(slide 0..1, 이즈 적용)
function swapState(i, p) {
  var shift = 0;
  if (p >= SWAP_AT[1]) shift = 2;
  else if (p >= SWAP_AT[0]) shift = 1;
  if (shift === 0) return { shift: shift, slide: 0 };
  var lastAt = SWAP_AT[shift - 1] + i * STAG;
  var raw = Math.min(1, Math.max(0, (p - lastAt) / SWAP_DUR));
  return { shift: shift, slide: easeOutCubic(raw) };
}
// 맥박 점 — 마지막 점의 반지름이 진동한다. 값 도착 직후 강하게, 사이엔 잔 진동
var phase = p * Math.PI * 2;
var beating = st.slide > 0 && st.slide < 1;
var beat = beating ? (1 - st.slide) : 0.25;
var pr = 2.2 + Math.abs(Math.sin(phase * 3)) * 2.4 * beat;

시간 창은 최근 12개 값, 한 번 미는 데 전체의 10%(SWAP_DUR) 를 쓰고 카드마다 1.8%(STAG) 시차를 둬 서로 어긋나게 했습니다.

08레이더 그래프 변형 — 정점마다 다른 시계

무엇이 보이나

속도·정확도·안정성·탐색·협업·지속 여섯 축의 다각형이 팀 A(88, 82, 92, 78, 90, 84)에서 팀 B(34, 26, 40, 22, 30, 28)로 정점마다 다른 시작 시차와 속도 배율로 모핑하고, 반투명 두 장이 겹쳐 공유하는 역량이 진하게 보입니다.

레이더 그래프 변형
새 창에서 열기

08 · 레이더 그래프 변형 — 측정 누적면적 7.3% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

polygoneaseOutCubicglobalAlpha

계산을 볼 곳

움직인 프레임 23/24 · 누적면적 7.3% — 아홉 개 중 가장 조용한 움직임이고, 역량 비교·두 후보 대조에 씁니다 — 모핑이 끝나는 62% 지점 뒤 0.75~0.90 구간에서 같은 배율로 다시 A 로 복귀합니다.

var morphing = A_VAL.slice();
var back = Math.max(0, (p - 0.75) / (BACK1 - 0.75));   // 0.75~0.90 복귀
for (var m = 0; m < N; m++) {
  var fwd = easeOutCubic(vertexP(m, p, MORPH1 - MORPH0, MORPH0));
  var v = A_VAL[m] + (B_VAL[m] - A_VAL[m]) * fwd;
  // 복귀 — B 에 도달한 뒤 다시 A 로. 복귀도 정점 배율을 그대로 쓴다
  var bk = easeOutCubic(Math.max(0, (back - VTX[m].off * 0.5) * VTX[m].rate));
  morphing[m] = v + (A_VAL[m] - v) * Math.min(1, bk);
}

여섯 정점의 시차는 0.00~0.06, 속도 배율은 0.70~1.35 인데, 일괄 트윈이면 다각형이 통째로 움직여 비교 대상이라는 인상을 주지 못하고 정점이 어긋나면 두 팀의 차이가 먼저 드러납니다.

09히트맵 파동 — 대각선으로 번지는 56셀

무엇이 보이나

월~일 7일 × 09~16시 8시간대의 56셀이 왼쪽 위부터 대각선 파도 타이밍으로 하나씩 차오르고, 색은 값 0~100 을 20점 간격 다섯 단계로 매핑하며, 셀에 마우스를 올리면 요일·시간대·값이 캔버스 안의 흰 카드에 뜹니다.

히트맵 파동
새 창에서 열기

09 · 히트맵 파동 — 측정 누적면적 34.969% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

scalergbapointermove

계산을 볼 곳

움직인 프레임 21/24 · 누적면적 34.969% — 아홉 개 중 가장 넓게 살아 있고, 방문 시간대·사용 패턴 탐색에 씁니다 — 평균·최고 라벨은 전부 찬 뒤 80% 지점에 뜨며 최고 셀은 일요일 13시 92 입니다.

// 값(0~100) → 5단 농도 ramp 인덱스(0..4). 20점 간격 계량(scale)
function rampAt(v) {
  return Math.min(4, Math.floor(v / 20));
}

// 셀(i,j) 의 채워짐 진행(0..1) — 대각선 파도: (i+j)*계수 + 사인 위상
function cellAt(i, j, p) {
  var phase = Math.sin((i * 3.7 + j * 2.9)) * WAVE;   // 셀마다 정해진 위상 어긋남(시드)
  var start = (i + j) * DIAG + phase;
  return Math.max(0, Math.min(1, (p - start) / GROW));
}

대각선 계수 DIAG 0.045 로 같은 대각선끼리 병행하고 사인 위상 폭 WAVE 0.06 이 같은 대각선 안에서도 어긋나게 해 순차와 병행이 섞인 파도를 만들며, 셀 하나가 차오르는 구간은 10%(GROW) 입니다.

canvas 그래프 그리기를 처음 잡을 때 무엇부터 정해야 하나?

축 척도를 먼저 못 박는다

html 그래프 그리기 는 캔버스 한 장과 스크립트 하나로 시작하지만, 좌표 계산 전에 정할 것이 둘 있습니다. y 축의 하한과 범위입니다 — 이 편에서는 01 이 100~500, 05 가 50~500, 04 는 y 0~500 에 x 0~20(만원)로 각자 못 박았습니다. 이 숫자를 데이터의 최솟값·최댓값 그대로 두면 여백은 줄지만 눈금 숫자가 어중간해지고, 반올림된 딱 떨어지는 수로 잡으면 그림이 안정됩니다. 막대(02)는 눈금을 100 단위 네 줄로 고정해 데이터가 바뀌어도 축이 흔들리지 않게 했습니다.

이징 함수가 그림의 성격을 정한다

같은 데이터라도 이징이 다르면 다른 그래프가 됩니다. 이 편의 아홉 개 중 다섯(02·05·07·08·09)이 1 - (1-t)^3 인 감속 이징 하나를 공유하는데, 처음에 빨랐다가 목표에서 미끄러지듯 멈추는 이 곡선이 「값이 쌓여 안정되는」 차트의 성격과 맞기 때문입니다. 반면 06 바늘은 감쇠 사인파, 01 끝점은 easeOutBack 으로 저마다 다른 함수를 썼습니다. 자바스크립트 그래프 구현 에서 이징을 어디에 둘지는 치장이 아니라 그림이 무엇을 말하는지의 문제입니다.

devicePixelRatio 로 물리 픽셀을 맞춘다

아홉 개 전부 창 크기가 바뀔 때 fit() 을 다시 돌며, 그 안에서 cv.width = Math.round(W * dpr) 에 이어 ctx.setTransform(dpr, 0, 0, dpr, 0, 0) 로 좌표계를 맞춥니다. 이 한 줄이 빠지면 2배 밀도 화면에서 축 라벨과 값 숫자가 물먹은 것처럼 흐려집니다 — 그래픽 코드는 늘 논리 픽셀로 쓰고 물리 해상도는 시작할 때 한 번만 건드리는 구조입니다.

어디서 깨지는가 — 함정

가장 자주 겪은 것은 캔버스의 y 축이 아래로 열려 있다는 사실입니다. 데이터 값을 y 에 그대로 얹으면 판매량이 커질수록 선이 아래로 내려가는 그래프가 됩니다 — 01 의 yAt 이 1 - (v - 하한) / 범위 로 뒤집는 이유이고, 06 이 위쪽 반원을 PI(왼쪽) → 1.5PI(꼭대기) → 2PI(오른쪽) 순으로 세는 것도 같은 좌표계의 결과입니다. 각도를 -90도부터 시작해 감소 방향으로 세면 반시계로 돌아 조각 순서가 거꾸로 흐릅니다. 두 번째는 트랙(옅게 깔아 두는 자리)입니다 — 03 을 첫 프레임에 트랙 전체를 깔아 두면 그 자리는 끝까지 변하지 않아 화면의 절반이 죽어 있습니다. 펜이 지나가는 만큼만 트랙을 여는 지금 구조로 바꾸고 나서 누적면적이 살아났습니다.

무엇이 깨지나 어떻게 나타나나 어떻게 다뤘나
y 좌표에 값 그대로 얹기 클수록 아래로 내려가는 그래프 1-(v-하한)/범위 로 뒤집기
dpr 미설정 2배 화면에서 라벨·숫자가 흐릿 setTransform(dpr,0,0,dpr,0,0)
첫 프레임에 트랙 전체 깔기 그 자리가 끝까지 변하지 않음 펜이 지나간 만큼만 트랙 개방 (03)
roundRect 미지원 브라우저 오류로 캔버스 전체가 비어 있음 typeof 검사 후 rect 폴백 (04)
칩 색 위의 숫자 라벨 대비가 기준에 못 미침 흰 무대 배경 위에 놓기 (09)

아홉 개를 한 파일로 받는 묶음의 열람 암호는 6ypv8dkk 이고, 편을 펼치면 항목마다 브라우저에서 곧바로 열리는 HTML 과 스타일 한 벌씩 들어 있습니다.

접근성

캔버스는 그림이므로 낭독기에는 빈 상자입니다 — 그래서 아홉 개 전부 캔버스에 role="img" 와 무엇이 그려지는지를 적은 aria-label 을 붙이고(MDN aria-label), 옆에는 화면에서 숨긴 sr-only 표로 같은 데이터를 글자로 제공합니다(04 는 40행이 기니 대표 8개, 09 는 시간대를 둘씩 묶은 요약). 색으로만 의미를 전하지 않는다는 WCAG 1.4.1 원칙에 따라 03 도넛은 범례에 이름과 퍼센트를, 09 히트맵은 평균·최고 셀을 글자로 겹니다.

prefers-reduced-motion: reduce 에서는 아홉 개 모두 루프를 걷어내고 완성 상태 한 장만 그립니다 — 01 은 12주 전체가 그려지고 끝점이 커진 장면, 02 는 다 자란 막대와 카운트업 끝난 값, 03 은 링이 다 찬 채 총량 24,000 이 떠 있는 장면입니다. 04 와 09 는 정지 화면에서도 mousemove 가 오면 그때만 다시 그려 응답하므로, 움직임을 줄인 사람에게도 하이라이트와 툴팁은 살아 있습니다. 06 은 바늘이 72 에 확정된 장면(0.95), 08 은 두 다각형이 겹친 B 팀 도달 장면(0.75)으로 각각 멈춥니다.

FAQ

그래프 그리기 사이트처럼 접속해서 바로 써 보는 화면으로도 쓸 수 있나요?

가능합니다. 각 항목의 HTML 은 파일을 열면 그대로 도는 한 장짜리 문서라, 받아 둔 파일을 브라우저로 여는 것만으로 값 입력 없이 데모 화면이 살아 움직입니다. 이 글의 데모 창도 같은 파일을 그대로 박아 넣은 것이므로, 페이지에서 확인하고 파일에서 가져가는 그림이 같습니다.

Chart.js 같은 라이브러리를 쓰는 것과 뭐가 다른가요?

라이브러리는 축·범례·툴팁을 한 번에 얻는 대신 그리는 방식을 라이브러리의 방식에 맡깁니다. 이 편의 아홉 개는 외부 의존이 0 이라 파일 하나를 복사하면 어떤 페이지에도 붙고, 눈금이 몇 개인지(02 는 100 단위 네 줄)·조각 틈이 얼마인지(03 은 0.035 라디안)·바늘이 몇 번 흔들리는지(06 은 감쇠 5.2 · 각진동수 10.5)를 전부 코드에서 직접 정합니다. 대시보드 규모가 커져 교차 차트·툴팁 동기화가 필요해지면 그때 라이브러리가 맞고, 한두 개 차트를 내 것으로 만들 때는 이 쪽이 짧습니다.

실제 데이터로 바꾸려면 어디를 고치면 되나요?

각 파일 맨 위의 데이터 배열이 전부입니다 — 01 의 WEEKS, 02 의 ROWS, 03 의 SRC, 09 의 GRID 처럼 값을 적은 배열을 당신의 데이터로 바꾸면 좌표 계산·애니메이션·레이블이 같이 따라옵니다. 단 y 축 하한과 범위(01 은 Y_TOP 100 · Y_SPAN 500)는 데이터에 맞게 손봐야 그림이 찌그러지지 않고, 그 기준은 위 「처음 잡을 때」 절에 정리해 두었습니다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.