01ペンのように描かれる折れ線グラフ
12か月の訪問数をx・y計算で一本のpathにつなぎ、stroke-dashoffsetが左から端まで描く
使いどころ月次トラフィックレポートの要約、管理画面の最初のカード
stroke-dashoffsetpathLengthpointermove

<main class="stage">
<div class="cd is-demo" id="cd">
<div class="cd__card">
<div class="cd__head">
<p class="cd__title">月間訪問数</p>
<span class="cd__chip">直近12か月</span>
</div>
<div class="cd__plot" id="plot">
<svg class="cd__svg" id="chart" viewBox="0 0 440 190" role="img" aria-label="12か月の訪問数折れ線グラフ。11月の842が最高">
<!-- 가로 격자 4단 — y 스케일 눈금(200/400/600/800)과 같은 간격 -->
<g class="cd__grid">
<line x1="34" y1="10" x2="432" y2="10"></line>
<line x1="34" y1="56" x2="432" y2="56"></line>
<line x1="34" y1="102" x2="432" y2="102"></line>
<line x1="34" y1="148" x2="432" y2="148"></line>
</g>
<g class="cd__ticks">
<text x="28" y="14">800</text>
<text x="28" y="60">600</text>
<text x="28" y="106">400</text>
<text x="28" y="152">200</text>
</g>
<!-- 밑변 軸 -->
<line class="cd__axis" x1="34" y1="148" x2="432" y2="148"></line>
<g class="cd__labels">
<text x="34" y="170">1月</text>
<text x="69" y="170">2月</text>
<text x="104" y="170">3月</text>
<text x="139" y="170">4月</text>
<text x="174" y="170">5月</text>
<text x="209" y="170">6月</text>
<text x="244" y="170">7月</text>
<text x="279" y="170">8月</text>
<text x="314" y="170">9月</text>
<text x="349" y="170">10月</text>
<text x="384" y="170">11月</text>
<text x="419" y="170">12月</text>
</g>
<!-- 영역 채움 — 라인 아래 옅은 파랑 면. JS 가 d 를 채운다 -->
<path class="cd__area" id="area" d=""></path>
<!-- 주인공 라인. pathLength=100 이라 dashoffset 애니메이션이 길이와 무관하게 0~100으로 돈다 -->
<path class="cd__line" id="line" pathLength="100" d=""></path>
<!-- 최고점 마커 — 호버 점과 같은 모양. JS 가 위치시킨다 -->
<circle class="cd__peak" id="peak" r="4" cx="0" cy="0"></circle>
</svg>
<!-- 툴팁 — plot 안에서 절대 배치. 아래로 삐져나가지 않게 위쪽에만 뜬다 -->
<div class="cd__tip" id="tip" aria-hidden="true"><span class="cd__tipMon" id="tipMon">11月</span><strong class="cd__tipVal" id="tipVal">842</strong></div>
<!-- 스크린리더용 데이터 표 — 화면에 안 보인다 -->
<table class="cd__sr">
<caption>月間訪問数</caption>
<thead><tr><th>月</th><th>訪問数</th></tr></thead>
<tbody id="srBody"></tbody>
</table>
</div>
</div>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 펜처럼 그려지는 선 그래프 — 실제 사용에선 JS 가 buildPath() 로 d 를 채워 완성된 그래프가 곧 뜨고
// pointermove 로 점·툴팁이 따라온다. 격자 미리보기(.is-demo)는 같은 path 에
// stroke-dashoffset 키프레임을 얹어 좌에서 우로 긋는 펜 모양을 2초 루프로 보여 준다.
$duration: $dur-loop;
$easing: $ease-out;
$color: $chart-1;
@include stage($stage-paper);
.cd { display: flex; justify-content: center; width: 100%; padding-bottom: $sp-2; }
.cd__card {
width: min(100%, 420px); background: $subject-cream; border-radius: $r-card;
box-shadow: $shadow-raised; padding: $sp-3 $sp-4 $sp-2;
}
.cd__head { display: flex; align-items: center; justify-content: space-between; gap: $sp-2; margin-bottom: $sp-2; }
.cd__title { margin: 0; font-weight: 800; font-size: clamp(12px, 3vw, 14px); color: $subject-ink; }
.cd__chip {
font-weight: 700; font-size: clamp(9px, 2.4vw, 10px); color: rgba(23, 20, 26, .66);
border: 1px solid rgba(23, 20, 26, .28); border-radius: $r-pill; padding: $sp-1 $sp-2;
}
.cd__plot { position: relative; }
.cd__svg { display: block; width: 100%; height: auto; touch-action: none; cursor: crosshair; }
.cd__grid line { stroke: rgba(23, 20, 26, .12); stroke-width: 1; }
.cd__ticks text, .cd__labels text {
fill: rgba(23, 20, 26, .72); font-weight: 700; font-size: 9px;
text-anchor: end; dominant-baseline: middle;
}
.cd__labels text { text-anchor: middle; font-size: 9.5px; }
.cd__axis { stroke: rgba(23, 20, 26, .32); stroke-width: 1.5; }
.cd__area { fill: rgba(47, 109, 246, .08); }
.cd__line {
fill: none; stroke: $color; stroke-width: 3.5; stroke-linecap: round; stroke-linejoin: round;
}
// 최고점 마커 — 실사용(정적)에선 항상 보이고, 미리보기 루프에서만 애니메이션이 0~62% 를 지배한다
.cd__peak { fill: $stage-paper; stroke: $color; stroke-width: 2.5; opacity: 1; }
// 툴팁 — 절대 배치라 루트에 padding-bottom 이 있어야 높이가 무대를 넘기지 않는다
.cd__tip {
position: absolute; bottom: calc(100% - 4px); left: 50%; transform: translate(-50%, 4px);
display: flex; align-items: baseline; gap: $sp-1; pointer-events: none;
background: $subject-ink; color: $stage-paper; border-radius: $r-xs;
padding: $sp-1 $sp-2; font-weight: 700; font-size: 10px; white-space: nowrap;
opacity: 0;
}
.cd__tip.is-on { opacity: 1; transform: translate(-50%, 0); transition: opacity $dur-quick $ease-out, transform $dur-quick $ease-out; }
.cd__tipMon { opacity: .72; }
.cd__tipVal { font-size: 12px; }
// 스크린리더용 표 — absolute+clip 은 레이아웃 크기가 그대로 scrollHeight 에 잡혀 넘침(BD8)이 뜨므로
// fixed 로 캔버스 밖에 둔다. 스크린리더는 그대로 읽는다
.cd__sr {
position: fixed; top: 0; left: -640px; width: 320px; margin: 0;
}
// ── 미리보기 루프: 0~4% 빈 칸 → 6~58% 라인이 좌에서 끝까지 그어진다 →
// 64% 에 최고점 점과 툴팁이 컷으로 뜬다 → 86% 까지 머문 뒤 86~96% 펜을 되감아 빈 칸으로 돌아온다.
// 되감기(지우개)는 퇴장이 아니라 다음 등장을 위한 리셋 — 96% 이후는 빈 대 빈이라 이어붙는 지점이 안 튄다.
.cd.is-demo .cd__line { stroke-dasharray: 100; }
.cd.is-demo .cd__line {
animation-name: cdDraw;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
@keyframes cdDraw {
0%, 4% { stroke-dashoffset: 100; }
58%, 86% { stroke-dashoffset: 0; }
96%, 100% { stroke-dashoffset: 100; }
}
// 라인 아래 면적은 처음부터 다 보이면 그리는 중인 라인과 어긋난다 —
// 같은 타이밍에 clip-path inset 으로 좌에서 우로 채운다 (clip 위프는 이징 짝으로. steps 와 함께 쓰면 컷으로 죽는다)
.cd.is-demo .cd__area {
animation-name: cdArea;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
}
@keyframes cdArea {
0%, 4% { clip-path: inset(0 100% 0 0); }
58%, 86% { clip-path: inset(0 0 0 0); }
96%, 100% { clip-path: inset(0 100% 0 0); }
}
// 최고점 점은 64% 에 컷으로 등장 (요소 중심에서 커지는 게 아니라 그냥 나타나는 게 마커답다)
.cd.is-demo .cd__peak {
animation-name: cdPeak;
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes cdPeak {
0%, 62% { opacity: 0; }
64%, 90% { opacity: 1; }
92%, 100% { opacity: 0; }
}
// 미리보기 툴팁은 최고점에 미리 놓아 둔다 (JS 가 11월로 세팅) — 점과 같은 타이밍에 컷
.cd.is-demo .cd__tip { left: 87.3%; }
.cd.is-demo .cd__tip {
animation-name: cdTip;
animation-duration: $duration;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes cdTip {
0%, 62% { opacity: 0; }
64%, 90% { opacity: 1; }
92%, 100% { opacity: 0; }
}
@media (max-width: 340px) {
// 320px 에서 12개 라벨이 겹친다 — 홀수 달만 남긴다 (10월 라벨이 사라져도 표·툴팁이 값을 갖고 있다)
.cd__labels text:nth-child(even) { display: none; }
.cd__labels text { font-size: 9px; }
.cd__card { padding: $sp-2 $sp-3 $sp-1; }
}
@media (prefers-reduced-motion: reduce) {
.cd.is-demo .cd__line,
.cd.is-demo .cd__peak,
.cd.is-demo .cd__tip { animation: none !important; }
// 움직임 없이 다 그어진 최종 상태로 점프한다
.cd.is-demo .cd__line { stroke-dashoffset: 0 !important; }
.cd.is-demo .cd__peak { opacity: 1 !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffffff;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.cd {
display: flex;
justify-content: center;
width: 100%;
padding-bottom: 8px;
}
.cd__card {
width: min(100%, 420px);
background: #fff7e6;
border-radius: 16px;
box-shadow: 0 14px 24px rgba(23, 20, 26, 0.32);
padding: 12px 16px 8px;
}
.cd__head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.cd__title {
margin: 0;
font-weight: 800;
font-size: clamp(12px, 3vw, 14px);
color: #17141a;
}
.cd__chip {
font-weight: 700;
font-size: clamp(9px, 2.4vw, 10px);
color: rgba(23, 20, 26, 0.66);
border: 1px solid rgba(23, 20, 26, 0.28);
border-radius: 999px;
padding: 4px 8px;
}
.cd__plot {
position: relative;
}
.cd__svg {
display: block;
width: 100%;
height: auto;
touch-action: none;
cursor: crosshair;
}
.cd__grid line {
stroke: rgba(23, 20, 26, 0.12);
stroke-width: 1;
}
.cd__ticks text, .cd__labels text {
fill: rgba(23, 20, 26, 0.72);
font-weight: 700;
font-size: 9px;
text-anchor: end;
dominant-baseline: middle;
}
.cd__labels text {
text-anchor: middle;
font-size: 9.5px;
}
.cd__axis {
stroke: rgba(23, 20, 26, 0.32);
stroke-width: 1.5;
}
.cd__area {
fill: rgba(47, 109, 246, 0.08);
}
.cd__line {
fill: none;
stroke: #2f6df6;
stroke-width: 3.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.cd__peak {
fill: #ffffff;
stroke: #2f6df6;
stroke-width: 2.5;
opacity: 1;
}
.cd__tip {
position: absolute;
bottom: calc(100% - 4px);
left: 50%;
transform: translate(-50%, 4px);
display: flex;
align-items: baseline;
gap: 4px;
pointer-events: none;
background: #17141a;
color: #ffffff;
border-radius: 4px;
padding: 4px 8px;
font-weight: 700;
font-size: 10px;
white-space: nowrap;
opacity: 0;
}
.cd__tip.is-on {
opacity: 1;
transform: translate(-50%, 0);
transition: opacity 220ms ease-out, transform 220ms ease-out;
}
.cd__tipMon {
opacity: 0.72;
}
.cd__tipVal {
font-size: 12px;
}
.cd__sr {
position: fixed;
top: 0;
left: -640px;
width: 320px;
margin: 0;
}
.cd.is-demo .cd__line {
stroke-dasharray: 100;
}
.cd.is-demo .cd__line {
animation-name: cdDraw;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
@keyframes cdDraw {
0%, 4% {
stroke-dashoffset: 100;
}
58%, 86% {
stroke-dashoffset: 0;
}
96%, 100% {
stroke-dashoffset: 100;
}
}
.cd.is-demo .cd__area {
animation-name: cdArea;
animation-duration: 2s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
}
@keyframes cdArea {
0%, 4% {
clip-path: inset(0 100% 0 0);
}
58%, 86% {
clip-path: inset(0 0 0 0);
}
96%, 100% {
clip-path: inset(0 100% 0 0);
}
}
.cd.is-demo .cd__peak {
animation-name: cdPeak;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes cdPeak {
0%, 62% {
opacity: 0;
}
64%, 90% {
opacity: 1;
}
92%, 100% {
opacity: 0;
}
}
.cd.is-demo .cd__tip {
left: 87.3%;
}
.cd.is-demo .cd__tip {
animation-name: cdTip;
animation-duration: 2s;
animation-timing-function: steps(1, end);
animation-iteration-count: infinite;
}
@keyframes cdTip {
0%, 62% {
opacity: 0;
}
64%, 90% {
opacity: 1;
}
92%, 100% {
opacity: 0;
}
}
@media (max-width: 340px) {
.cd__labels text:nth-child(even) {
display: none;
}
.cd__labels text {
font-size: 9px;
}
.cd__card {
padding: 8px 12px 4px;
}
}
@media (prefers-reduced-motion: reduce) {
.cd.is-demo .cd__line,
.cd.is-demo .cd__peak,
.cd.is-demo .cd__tip {
animation: none !important;
}
.cd.is-demo .cd__line {
stroke-dashoffset: 0 !important;
}
.cd.is-demo .cd__peak {
opacity: 1 !important;
}
}var DATA = [430, 510, 620, 590, 545, 610, 660, 705, 690, 760, 842, 815];
var MON = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
var PAD_L = 34, PAD_R = 432, TOP = 10, BASE = 148; // viewBox 좌표
var MAX_Y = 900; // y 스케일 상한 — 눈금 200 간격과 맞춘 값
var cd = document.getElementById('cd');
var plot = document.getElementById('plot');
var svg = document.getElementById('chart');
var line = document.getElementById('line');
var area = document.getElementById('area');
var peak = document.getElementById('peak');
var tip = document.getElementById('tip');
var tipMon = document.getElementById('tipMon');
var tipVal = document.getElementById('tipVal');
// 스케일 함수 — 데이터 값 → viewBox 좌표. 이 두 함수가 그래프의 전부다
function scaleX(i) { return PAD_L + (PAD_R - PAD_L) * i / (DATA.length - 1); }
function scaleY(v) { return BASE - (BASE - TOP) * v / MAX_Y; }
// 인접 점의 중점을 잇고 실제 점은 제어점으로 쓰는 부드러운 선 — 꺾임이 살면서 물결지지 않는다
function buildPath() {
var pts = DATA.map(function (v, i) { return [scaleX(i), scaleY(v)]; });
var d = 'M' + pts[0][0] + ',' + pts[0][1];
for (var i = 1; i < pts.length - 1; i++) {
var mx = (pts[i][0] + pts[i + 1][0]) / 2, my = (pts[i][1] + pts[i + 1][1]) / 2;
d += ' Q' + pts[i][0] + ',' + pts[i][1] + ' ' + mx + ',' + my;
}
var last = pts[pts.length - 1];
d += ' L' + last[0] + ',' + last[1];
return d;
}
var pathD = buildPath();
line.setAttribute('d', pathD);
area.setAttribute('d', pathD + ' L' + PAD_R + ',' + BASE + ' L' + PAD_L + ',' + BASE + ' Z');
var peakIdx = DATA.indexOf(Math.max.apply(null, DATA));
var px = scaleX(peakIdx), py = scaleY(DATA[peakIdx]);
peak.setAttribute('cx', px); peak.setAttribute('cy', py);
// 스크린리더 표 채우기
var srBody = document.getElementById('srBody');
DATA.forEach(function (v, i) {
var tr = document.createElement('tr');
var th = document.createElement('th'); th.setAttribute('scope', 'row'); th.textContent = MON[i];
var td = document.createElement('td'); td.textContent = v;
tr.appendChild(th); tr.appendChild(td); srBody.appendChild(tr);
});
// 툴팁을 viewBox x 좌표에 맞춰 놓는다 — plot 은 SVG 왈폭이므로 비율로 환산
function moveTip(i) {
var ratio = scaleX(i) / 440;
tip.style.left = (ratio * 100) + '%';
tipMon.textContent = MON[i];
tipVal.textContent = DATA[i].toLocaleString();
}
// 사람이 조작하면 미리보기 루프를 끄고 진짜 툴팁이 움직인다
svg.addEventListener('pointermove', function (e) {
if (cd.classList.contains('is-demo')) { cd.classList.remove('is-demo'); }
var r = svg.getBoundingClientRect();
var vx = (e.clientX - r.left) / r.width * 440; // 화면 좌표 → viewBox 좌표
var i = Math.round((vx - PAD_L) / ((PAD_R - PAD_L) / (DATA.length - 1)));
i = Math.max(0, Math.min(DATA.length - 1, i));
peak.setAttribute('cx', scaleX(i)); peak.setAttribute('cy', scaleY(DATA[i]));
moveTip(i);
tip.classList.add('is-on');
});
svg.addEventListener('pointerleave', function () {
tip.classList.remove('is-on');
peak.setAttribute('cx', px); peak.setAttribute('cy', py); // 최고점으로 돌아간다
});// ChartLinePath — vanilla 판(run/289/demo/01-chart-draw-line-path/index.html) 의 React 포트.
// 12개월 방문 수를 x·y 스케일 함수로 path 하나에 잇고, 진입은 stroke-dashoffset 이 좌에서 끝까지 긋는다.
// pointermove 로 가까운 달의 점·툴팁이 따라온다. 값·키프레임은 vanilla 와 같은 글자.
import { useEffect, useRef, useState } from "react";
import type { CSSProperties, PointerEvent as ReactPointerEvent } from "react";
import styles from "./ChartLinePath.module.scss";
export type ChartLinePathProps = {
demo?: boolean;
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const DATA = [430, 510, 620, 590, 545, 610, 660, 705, 690, 760, 842, 815];
const MON = ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"];
const PAD_L = 34, PAD_R = 432, TOP = 10, BASE = 148;
const MAX_Y = 900;
// 스케일 함수 — 데이터 값 → viewBox 좌표. 이 두 함수가 그래프의 전부다
function scaleX(i: number): number {
return PAD_L + (PAD_R - PAD_L) * i / (DATA.length - 1);
}
function scaleY(v: number): number {
return BASE - (BASE - TOP) * v / MAX_Y;
}
// 인접 점의 중점을 잇고 실제 점은 제어점으로 쓰는 부드러운 선
function buildPath(): string {
const pts = DATA.map((v, i) => [scaleX(i), scaleY(v)] as [number, number]);
let d = "M" + pts[0][0] + "," + pts[0][1];
for (let i = 1; i < pts.length - 1; i++) {
const mx = (pts[i][0] + pts[i + 1][0]) / 2;
const my = (pts[i][1] + pts[i + 1][1]) / 2;
d += " Q" + pts[i][0] + "," + pts[i][1] + " " + mx + "," + my;
}
const last = pts[pts.length - 1];
d += " L" + last[0] + "," + last[1];
return d;
}
const PATH_D = buildPath();
const AREA_D = PATH_D + " L" + PAD_R + "," + BASE + " L" + PAD_L + "," + BASE + " Z";
const PEAK_IDX = DATA.indexOf(Math.max(...DATA));
export default function ChartLinePath({ demo = false, duration, easing, color }: ChartLinePathProps) {
const rootRef = useRef<HTMLDivElement | null>(null);
const [peakAt, setPeakAt] = useState(PEAK_IDX);
const [tip, setTip] = useState({ on: false, idx: PEAK_IDX });
const vars: Vars = {
"--duration": duration != null ? duration + "ms" : undefined,
"--easing": easing,
"--color": color,
};
useEffect(() => {
// 사람이 조작하면 미리보기 루프를 끊고 진짜 툴팁 경로로 들어간다
const root = rootRef.current;
if (!root || !demo) return;
const stop = (e: ReactPointerEvent<SVGSVGElement>) => {
if (root.classList.contains(styles.isDemo)) root.classList.remove(styles.isDemo);
e.preventDefault();
};
const svg = root.querySelector("svg");
// ReactPointerEvent 는 Event 와 겹치지 않아 단번에 캐스팅되지 않는다 — unknown 을 거친다
if (svg) svg.addEventListener("pointerdown", stop as unknown as EventListener);
return () => { if (svg) svg.removeEventListener("pointerdown", stop as unknown as EventListener); };
}, [demo]);
function onMove(e: ReactPointerEvent<SVGSVGElement>): void {
if (rootRef.current?.classList.contains(styles.isDemo)) {
rootRef.current.classList.remove(styles.isDemo);
}
const r = e.currentTarget.getBoundingClientRect();
const vx = (e.clientX - r.left) / r.width * 440;
let i = Math.round((vx - PAD_L) / ((PAD_R - PAD_L) / (DATA.length - 1)));
i = Math.max(0, Math.min(DATA.length - 1, i));
setPeakAt(i);
setTip({ on: true, idx: i });
}
const cls = [styles.root, demo ? styles.isDemo : ""].filter(Boolean).join(" ");
const px = scaleX(peakAt), py = scaleY(DATA[peakAt]);
return (
<main className={styles.stage} style={vars}>
<div className={cls} ref={rootRef}>
<div className={styles.card}>
<div className={styles.head}>
<p className={styles.title}>월간 방문 수</p>
<span className={styles.chip}>최근 12개월</span>
</div>
<div className={styles.plot}>
<svg className={styles.svg} viewBox="0 0 440 190" role="img" aria-label="12개월 방문 수 꺾은선 그래프, 11월에 842로 가장 높음" onPointerMove={onMove} onPointerLeave={() => { setPeakAt(PEAK_IDX); setTip({ on: false, idx: PEAK_IDX }); }}>
<g className={styles.grid}>
<line x1="34" y1="10" x2="432" y2="10"></line>
<line x1="34" y1="56" x2="432" y2="56"></line>
<line x1="34" y1="102" x2="432" y2="102"></line>
<line x1="34" y1="148" x2="432" y2="148"></line>
</g>
<g className={styles.ticks}>
<text x="28" y="14">800</text>
<text x="28" y="60">600</text>
<text x="28" y="106">400</text>
<text x="28" y="152">200</text>
</g>
<line className={styles.axis} x1="34" y1="148" x2="432" y2="148"></line>
<g className={styles.labels}>
{MON.map((m, i) => <text key={m} x={scaleX(i)} y="170">{m}</text>)}
</g>
<path className={styles.area} d={AREA_D}></path>
<path className={styles.line} pathLength="100" d={PATH_D}></path>
<circle className={styles.peak} r="4" cx={px} cy={py}></circle>
</svg>
<div className={[styles.tip, tip.on ? styles.tipOn : ""].filter(Boolean).join(" ")} aria-hidden="true">
<span className={styles.tipMon}>{MON[tip.idx]}</span>
<strong className={styles.tipVal}>{DATA[tip.idx].toLocaleString()}</strong>
</div>
<table className={styles.sr}>
<caption>월간 방문 수</caption>
<thead><tr><th scope="col">월</th><th scope="col">방문 수</th></tr></thead>
<tbody>
{DATA.map((v, i) => <tr key={MON[i]}><th scope="row">{MON[i]}</th><td>{v}</td></tr>)}
</tbody>
</table>
</div>
</div>
</div>
</main>
);
}01 ペンのように描かれる折れ線グラフ — 計測 累積面積 25.276% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · css グラフ をSVGで描く9種 — ライブラリなし
このデモが属する記事
css グラフ をSVGで描く9種 — ライブラリなしcss グラフとは、数値を線・棒・面という目に見えるかたちに変える、いちばん軽い方法のことです。ここにある9種(折れ線・縦棒・ドーナツ・レーダー・ヒートマップ・スパークライン・積み上げ面・ツリーマップ・ゲージ)は、ライブラリなしでSVGの座 この記事のデモをすべて見る


