# 실시간 차트 (스트리밍)

> 실시간 차트: append() 로 이어 붙이고 오래된 점은 창 밖으로, update() 는 부드럽게 전환. 14가지 차트 모두, SSE·WebSocket 예제.

Source: https://82chart.com/docs/realtime/

실시간 차트란 데이터가 들어오는 대로 다시 그려 지금 상태를 보여 주는 차트입니다(접속자 수, 서버 자원, 체결 가격, 민원 접수 현황). 82chart 는 `realtime` 옵션 하나로 14가지 차트 모두 실시간으로 쓸 수 있습니다. 두 가지 방법이 있습니다.

- **이어 붙이기** — `chart.append(points)`: 선·영역·막대·산점도(시간 축이나 카테고리 축), 콤보, 달력 히트맵. 새 점은 오른쪽에서 미끄러져 들어오고 오래된 점은 창(`window` 점 개수, `duration` 밀리초) 밖으로 빠집니다. 값 축은 새 범위로 부드럽게 넓어지거나 좁아집니다(`yAxis.min` / `max` 를 주면 고정).
- **바꾸기** — `chart.update(data)` 나 값 하나만 바꾸는 `chart.setValue(시리즈, 순서, 값)`: 모든 종류. 파이·도넛 조각 각도, 게이지 값, 방사형 다각형, 막대·피라미드·퍼널·폭포 길이, 히트맵·달력·트리맵 색과 칸 위치, 가운데 합계와 라벨 숫자가 이전 값에서 새 값으로 이어지며 바뀝니다. 새 항목은 0에서 자라납니다.

```js
const chart = createChart('#chart', {
  type: 'line',
  title: '실시간 접속자',
  unit: '명',
  xAxis: { type: 'time' },
  zoom: true,
  realtime: { duration: 60000 },
  data: { series: [{ name: '접속자', data: [] }, { name: '로그인', data: [] }] },
});
let users = 1200, logins = 400;
setInterval(() => {
  users = Math.max(0, users + Math.round((Math.random() - 0.48) * 80));
  logins = Math.max(0, Math.round(users * (0.3 + Math.random() * 0.05)));
  chart.append({ series: [[[Date.now(), users]], [[Date.now(), logins]]] });
}, 500);
```

위 차트는 0.5초마다 점을 붙이고 최근 1분(`duration: 60000`)만 남깁니다. 마우스를 올리면 그림이 멈춰 툴팁이 그 점에 머무르고, 손을 떼면 그동안 들어온 데이터를 따라잡습니다(`pauseOnHover`). 확대(+)하면 그 구간을 보고 있는 동안에는 따라가지 않고 "최신으로" 단추가 나옵니다.

## 바꾸기: 모든 차트가 부드럽게

```js
const chart = createChart('#chart', {
  type: 'donut',
  title: '결제 수단 (실시간)',
  unit: '건',
  realtime: { tween: 400 },
  data: { labels: ['카드', '간편결제', '계좌이체', '휴대폰'], series: [{ name: '결제', data: [520, 310, 120, 50] }] },
});
const counts = [520, 310, 120, 50];
setInterval(() => {
  counts[Math.floor(Math.random() * 4)] += Math.round(Math.random() * 40);
  chart.update({ labels: ['카드', '간편결제', '계좌이체', '휴대폰'], series: [{ name: '결제', data: [...counts] }] });
}, 800);
```

```js
const chart = createChart('#chart', {
  type: 'gauge',
  title: 'CPU 사용률',
  unit: '%',
  realtime: { tween: 500 },
  gauge: { max: 100, target: 80, targetLabel: '경고', bands: [{ to: 60, color: '#1baf7a' }, { to: 80, color: '#eda100' }, { to: 100, color: '#e34948' }] },
  data: { series: [{ name: 'CPU', data: [42] }] },
});
let cpu = 42;
setInterval(() => {
  cpu = Math.min(100, Math.max(3, cpu + (Math.random() - 0.5) * 18));
  chart.setValue(0, 0, Math.round(cpu));
}, 1000);
```

```js
const chart = createChart('#chart', {
  type: 'treemap',
  title: '업종 등락 (실시간)',
  colorBy: 'change',
  deltaColors: 'korea',
  realtime: { tween: 600 },
  data: { series: [
    { name: '반도체', data: [{ x: '삼성전자', y: 420, change: 0.8 }, { x: 'SK하이닉스', y: 140, change: -1.2 }] },
    { name: '자동차', data: [{ x: '현대차', y: 52, change: 0.4 }, { x: '기아', y: 41, change: -0.3 }] },
    { name: '인터넷', data: [{ x: 'NAVER', y: 33, change: 1.5 }, { x: '카카오', y: 18, change: -2.1 }] },
  ] },
});
setInterval(() => {
  const d = chart.getOptions().data;
  chart.update({ series: d.series.map(s => ({ ...s, data: s.data.map(p => ({ ...p, change: +(p.change + (Math.random() - 0.5) * 0.6).toFixed(2) })) })) });
}, 1500);
```

## realtime 옵션

| 옵션 | 기본값 | 설명 |
| --- | --- | --- |
| `realtime` | - | `true` 또는 아래 객체. 없으면 `update()` 는 지금처럼 바로 다시 그림 |
| `window` | - | 시리즈마다 남길 최대 점 수 (달력은 날 수) |
| `duration` | - | 시간 축: 최근 이 밀리초만 남김 (예: 60000 = 1분) |
| `pauseOnHover` | true | 마우스가 데이터 위에 있는 동안 그림을 멈춤 (데이터는 계속 받음) |
| `fps` | 30 | 초당 최대 그리기 횟수. 한 프레임 안에 들어온 append·update 는 한 번에 그림 |
| `tween` | 300 | `update()`·`setValue()` 가 새 값으로 바뀌는 시간(ms). 0 이면 바로. "동작 줄이기" 설정이면 언제나 바로 |

## API

| 메서드 · 이벤트 | 설명 |
| --- | --- |
| `append(points, { series })` | 점 하나 `{ x, y }`, 한 시리즈의 목록 `[[x, y], …]`, 또는 여러 시리즈 `{ labels?, series: [[…], […]] }`. 숫자 x 는 `xAxis: { type: 'time' }` 에서 에포크 밀리초 |
| `update(data)` | `realtime` 이면 프레임에 맞춰 묶고 부드럽게 바꿈 |
| `setValue(series, index, y)` | 값 하나만 바꿈 (게이지, 조각 하나, 칸 하나) |
| `clear()` | 모든 점을 지움 (시리즈 이름과 옵션은 그대로) |
| `isLive()` / `follow()` | 최신 데이터를 따라가는 중인지 / 확대를 풀고 다시 따라가기 ("최신으로" 단추와 같음) |
| `announce()` | 지금 요약을 스크린리더에 바로 읽어 줌 |
| `on('append', ({ added, total }) => …)` | append 할 때마다 |

## 서버에서 받기: SSE 와 WebSocket

```js
// 서버가 보내는 이벤트(SSE): data: {"t": 1792800000000, "v": 1234}
const chart = createChart('#chart', { type: 'line', xAxis: { type: 'time' }, realtime: { duration: 5 * 60000 }, data: { series: [{ name: '요청 수', data: [] }] } });
const es = new EventSource('/api/stream');
es.onmessage = (e) => { const { t, v } = JSON.parse(e.data); chart.append([[t, v]]); };
```

```js
// WebSocket: 한 메시지에 여러 시리즈
const ws = new WebSocket('wss://example.go.kr/metrics');
ws.onmessage = (e) => {
  const m = JSON.parse(e.data);                // { t, cpu, mem }
  chart.append({ series: [[[m.t, m.cpu]], [[m.t, m.mem]]] });
};
// 화면을 떠날 때
// ws.close(); chart.destroy();
```

## 접근성과 성능

- 스크린리더 알림(`aria-live`)은 데이터가 들어올 때마다가 아니라 많아야 10초에 한 번입니다. 키보드·마우스로 옮기면 바로 읽고, `chart.announce()` 로 원할 때 읽힐 수 있습니다. 차트 요약(`aria-label`)은 1초에 한 번 고칩니다.
- 숨긴 데이터 표와 `toCSV()` 는 지금 창의 데이터입니다(표는 1초에 한 번 고침).
- 매 프레임 SVG 를 새로 만들지 않고, 구조가 같으면 기존 요소의 속성과 글자만 바꿉니다. 그래서 요소 수가 늘지 않고 툴팁·강조가 끊기지 않습니다.
- 실시간 차트는 점진 렌더링을 하지 않고, 점이 그림 폭의 절반보다 많으면 LTTB 로 줄여 그립니다(CSV·표는 원본).
- "동작 줄이기"(`prefers-reduced-motion`)가 켜져 있으면 미끄러짐·전환 없이 바로 바꿉니다.
- 측정(노트북, Chrome, 개발 빌드): 4개 시리즈 × 2,000점 창에 초당 60번 붙이면 한 번 그리는 데 평균 약 7ms(p95 11ms), 시리즈 2개 × 120점 선은 약 1.5ms, 파이·게이지·트리맵 같은 바꾸기 차트는 1ms 안팎입니다.
