02전송 속도 값 그래프 갱신 — 초당 전송량 값이 그래프 형태로 계속 다시 그려지는 방식
초당 전송량 값이 일정 간격마다 다시 계산되어, 그 값들이 이어진 모양이 계속 갱신됩니다.
쓰임대용량 파일 전송 대시보드, 네트워크 상태 표시 패널
.dsg__graphN.dsg__btn

<main class="stage">
<div class="dsg is-demo" id="dsg">
<div class="dsg__head">
<span class="dsg__label">전송 속도</span>
<span class="dsg__val" id="dsgVal">0MB/s</span>
</div>
<svg class="dsg__graph" viewBox="0 0 220 80" preserveAspectRatio="none">
<polyline class="dsg__raw" id="dsgRaw" points=""></polyline>
<polyline class="dsg__avg" id="dsgAvg" points=""></polyline>
</svg>
<button class="dsg__btn" id="dsgBtn" type="button">일시정지</button>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 계산 겹: 원시 표본의 이동평균(최근 N개) 폴리라인 — index.html movingAvg() 참조
@include stage($stage-paper);
.dsg {
width: 100%;
max-width: 260px;
display: grid;
gap: $sp-2;
}
.dsg__head {
display: flex;
align-items: baseline;
justify-content: space-between;
font: 600 13px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: $subject-ink;
}
.dsg__val { font-variant-numeric: tabular-nums; color: $subject-blue-deep; }
.dsg__graph {
width: 100%;
height: 96px;
border-radius: $r-sm;
background: $subject-cream;
}
.dsg__raw {
fill: none;
stroke: $chart-2;
stroke-width: 1;
opacity: .35;
vector-effect: non-scaling-stroke;
}
.dsg__avg {
fill: none;
stroke: $chart-1;
stroke-width: 2.4;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
.dsg__btn {
width: 100%;
border: 0;
border-radius: $r-control;
padding: $sp-2 $sp-4;
background: $subject-ink;
color: $subject-cream;
font: 700 12px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
cursor: pointer;
transition: transform $dur-instant $ease-out;
&:active { transform: scale(.96); }
}
@media (max-width: 340px) {
.dsg { max-width: 200px; }
.dsg__graph { height: 72px; }
}
@media (prefers-reduced-motion: reduce) {
.dsg__avg, .dsg__raw { transition: none; }
.dsg__btn { transition: none; }
}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);
}
.dsg {
width: 100%;
max-width: 260px;
display: grid;
gap: 8px;
}
.dsg__head {
display: flex;
align-items: baseline;
justify-content: space-between;
font: 600 13px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
color: #17141a;
}
.dsg__val {
font-variant-numeric: tabular-nums;
color: #0a4fe8;
}
.dsg__graph {
width: 100%;
height: 96px;
border-radius: 8px;
background: #fff7e6;
}
.dsg__raw {
fill: none;
stroke: #c2361a;
stroke-width: 1;
opacity: 0.35;
vector-effect: non-scaling-stroke;
}
.dsg__avg {
fill: none;
stroke: #2f6df6;
stroke-width: 2.4;
stroke-linejoin: round;
stroke-linecap: round;
vector-effect: non-scaling-stroke;
}
.dsg__btn {
width: 100%;
border: 0;
border-radius: 12px;
padding: 8px 16px;
background: #17141a;
color: #fff7e6;
font: 700 12px/1 -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
cursor: pointer;
transition: transform 140ms ease-out;
}
.dsg__btn:active {
transform: scale(0.96);
}
@media (max-width: 340px) {
.dsg {
max-width: 200px;
}
.dsg__graph {
height: 72px;
}
}
@media (prefers-reduced-motion: reduce) {
.dsg__avg, .dsg__raw {
transition: none;
}
.dsg__btn {
transition: none;
}
}(function () {
var root = document.getElementById('dsg');
var val = document.getElementById('dsgVal');
var rawLine = document.getElementById('dsgRaw');
var avgLine = document.getElementById('dsgAvg');
// 시드 고정: 초당 전송량 원시 표본(MB/s ×10, 정수). 실 네트워크 측정 0
var SAMPLES = [12, 18, 15, 26, 32, 28, 40, 36, 44, 52, 48, 58, 54, 62, 46, 38, 44, 50, 42, 34, 28, 22, 16, 20];
var N = 5; // 이동평균 창(최근 N 개 표본)
var W = 220, H = 80, MAXV = 65;
var TICK_MS = 160;
var buf = [];
var i = 0;
var paused = false;
var timer = 0;
var btn = document.getElementById('dsgBtn');
function pt(idx, v, count) {
var x = count <= 1 ? W : (idx / (count - 1)) * W;
var y = H - (v / MAXV) * H;
return x.toFixed(1) + ',' + y.toFixed(1);
}
// 계산 겹: 최근 N개 원시 표본의 이동평균 — 그래프 선은 이 평균값을 잇는다(원시값 자체가 아님)
function movingAvg(arr) {
var out = [];
for (var k = 0; k < arr.length; k++) {
var s = Math.max(0, k - N + 1);
var sum = 0, c = 0;
for (var j = s; j <= k; j++) { sum += arr[j]; c++; }
out.push(sum / c);
}
return out;
}
function paint() {
var avg = movingAvg(buf);
var rawPts = [], avgPts = [];
for (var k = 0; k < buf.length; k++) {
rawPts.push(pt(k, buf[k], buf.length));
avgPts.push(pt(k, avg[k], buf.length));
}
rawLine.setAttribute('points', rawPts.join(' '));
avgLine.setAttribute('points', avgPts.join(' '));
var last = avg.length ? avg[avg.length - 1] : 0;
val.textContent = (last / 10).toFixed(1) + 'MB/s';
}
function tick() {
if (paused) return;
buf.push(SAMPLES[i % SAMPLES.length]);
if (buf.length > 14) buf.shift();
i++;
paint();
timer = setTimeout(tick, TICK_MS);
}
tick();
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
paused = !paused;
btn.textContent = paused ? '재개' : '일시정지';
if (!paused) tick();
else clearTimeout(timer);
});
})();import { useEffect, useRef } from "react";
import styles from "./DownloadManagerSpeedGraph.module.scss";
type Box<T> = { current: T };
export default function DownloadManagerSpeedGraph() {
const rootRef = useRef(null) as unknown as Box<HTMLDivElement | null>;
useEffect(() => {
const root = rootRef.current;
if (!root) return;
const val = root.querySelector(`.${styles.val}`) as HTMLElement | null;
const rawLine = root.querySelector(`.${styles.raw}`) as SVGPolylineElement | null;
const avgLine = root.querySelector(`.${styles.avg}`) as SVGPolylineElement | null;
const btn = root.querySelector(`.${styles.btn}`) as HTMLElement | null;
if (!val || !rawLine || !avgLine || !btn) return;
// 시드 고정: 초당 전송량 원시 표본(MB/s ×10, 정수). 실 네트워크 측정 0
const SAMPLES = [12, 18, 15, 26, 32, 28, 40, 36, 44, 52, 48, 58, 54, 62, 46, 38, 44, 50, 42, 34, 28, 22, 16, 20];
const N = 5;
const W = 220, H = 80, MAXV = 65;
const TICK_MS = 160;
let buf: number[] = [];
let i = 0;
let paused = false;
let timer = 0;
let stopped = false;
function pt(idx: number, v: number, count: number) {
const x = count <= 1 ? W : (idx / (count - 1)) * W;
const y = H - (v / MAXV) * H;
return x.toFixed(1) + "," + y.toFixed(1);
}
// 계산 겹: 최근 N개 원시 표본의 이동평균 — 그래프 선은 이 평균값을 잇는다(원시값 자체가 아님)
function movingAvg(arr: number[]) {
const out: number[] = [];
for (let k = 0; k < arr.length; k++) {
const s = Math.max(0, k - N + 1);
let sum = 0, c = 0;
for (let j = s; j <= k; j++) { sum += arr[j]; c++; }
out.push(sum / c);
}
return out;
}
function paint() {
const avg = movingAvg(buf);
const rawPts: string[] = [], avgPts: string[] = [];
for (let k = 0; k < buf.length; k++) {
rawPts.push(pt(k, buf[k], buf.length));
avgPts.push(pt(k, avg[k], buf.length));
}
rawLine!.setAttribute("points", rawPts.join(" "));
avgLine!.setAttribute("points", avgPts.join(" "));
const last = avg.length ? avg[avg.length - 1] : 0;
val!.textContent = (last / 10).toFixed(1) + "MB/s";
}
function tick() {
if (paused || stopped) return;
buf.push(SAMPLES[i % SAMPLES.length]);
if (buf.length > 14) buf.shift();
i++;
paint();
timer = window.setTimeout(tick, TICK_MS);
}
tick();
function onClick() {
root!.classList.remove(styles["is-demo"]);
paused = !paused;
btn!.textContent = paused ? "재개" : "일시정지";
if (!paused) tick();
else window.clearTimeout(timer);
}
btn.addEventListener("click", onClick);
return () => {
stopped = true;
window.clearTimeout(timer);
btn.removeEventListener("click", onClick);
};
}, []);
return (
<div ref={rootRef} className={`${styles.stage} ${styles.dsg} ${styles["is-demo"]}`}>
<div className={styles.head}>
<span className={styles.label}>전송 속도</span>
<span className={styles.val}>0MB/s</span>
</div>
<svg className={styles.graph} viewBox="0 0 220 80" preserveAspectRatio="none">
<polyline className={styles.raw} points="" />
<polyline className={styles.avg} points="" />
</svg>
<button className={styles.btn} type="button">일시정지</button>
</div>
);
}02 전송 속도 값 그래프 갱신 — 초당 전송량 값이 그래프 형태로 계속 다시 그려지는 방식 — 측정 누적면적 8.108% · 움직인 프레임 16/23GODRICH 원작 · zip 에 SCSS·React 포함 · 다운로드 진행 상황 표시 9종 — 값이 퍼센트 단위로 갱신되는 아홉 가지 방식
이 데모가 속한 글
다운로드 진행 상황 표시 9종 — 값이 퍼센트 단위로 갱신되는 아홉 가지 방식다운로드 진행 상황 표시는 지금 받아지는 파일이 어느 상태에 있는지를 값 하나로 계산해 화면에 드러내는 인터랙션을 말하는데, 아홉 개 중 하나는 여러 파일이 동시에 받아질 때 각 파일의 진행 값이 목록에서 나란히 오 이 글의 데모 전부 보기


