📊 Chart.js — 인터랙티브 차트 종합 가이드

교통사고 데이터(TAAS)로 배우는 9가지 차트 + ⭐인터랙티브 필터 · 코드 그대로 복사 → 데이터만 교체

⓪ 시작하기
① Bar
② Line (이중축)
③ Pie / Doughnut
④ Radar
⑤ Bubble
⑥ Scatter
⑦ Polar Area
⑧ Mixed
⑨ Treemap
⑩ 인터랙티브 필터 ⭐
⓪ Chart.js 4단계 워크플로우 — 모든 차트의 공통 패턴
Chart.js로 차트를 그리는 과정은 항상 똑같은 4단계입니다. 차트 종류만 다르지 흐름은 동일하므로, 이 패턴을 한 번 익히면 다른 차트는 type과 data만 바꿔 쓰면 됩니다.
  1. ① CDN 스크립트 로드 — HTML <head>에 한 번만
  2. ② 캔버스 만들기 — <canvas id="..."> 하나가 차트 한 개
  3. ③ 데이터 준비 — 배열로 직접 작성하거나 fetch()로 외부 파일 로드
  4. ④ Chart 인스턴스 생성 — new Chart(canvas, { type, data, options })
💡 학생 미션: 아래 코드를 그대로 복사 → ③ 데이터 부분(노란 표시)만 자기 데이터로 교체.
<!-- ① Chart.js CDN 로드 (HTML head에 한 번만) -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>

<!-- ② 차트가 그려질 캔버스 (id는 자유) -->
<canvas id="myChart"></canvas>

<script>
// ③ 데이터 준비 — A안: 배열로 직접 작성
const labels = ['2020','2021','2022','2023'];
const values = [100, 120, 95, 140];

// ③ 데이터 준비 — B안: 외부 JSON/CSV에서 fetch (실무 패턴)
// fetch('data.json')
//     .then(res => res.json())          // JSON 파싱
//     .then(rows => {                    // rows = [{year:2020,count:100}, ...]
//         const labels = rows.map(r => r.year);
//         const values = rows.map(r => r.count);
//         drawChart(labels, values);
//     });

// ④ Chart 인스턴스 생성
new Chart(document.getElementById('myChart'), {
    type: 'bar',                        // 차트 종류만 바꾸면 다른 차트
    data: {
        labels,                          // x축 라벨
        datasets: [{
            label: '판매량',
            data: values,                // 값 배열
            backgroundColor: 'rgba(59,130,246,0.7)'
        }]
    },
    options: {
        responsive: true,              // 부모 너비에 자동 맞춤
        maintainAspectRatio: false     // 부모 div 높이 사용 → 캔버스를 div로 감싸기
    }
});
</script>
💡 차트 종류 바꾸기: 위 코드에서 type:'bar'를 'line', 'pie', 'radar', 'scatter', 'bubble', 'polarArea', 'doughnut'로 바꾸면 그게 바로 그 차트입니다. 데이터 형식만 차트별로 살짝 다릅니다(아래 탭에서 하나씩).
💡 데이터를 외부에서 불러오는 3가지 방법: ⓐ JSON 파일을 fetch().then(r=>r.json()), ⓑ CSV는 fetch().then(r=>r.text()) 후 직접 split 또는 PapaParse 라이브러리, ⓒ 구글 시트 발행 URL을 fetch. 어느 쪽이든 최종적으로 labels 배열과 values 배열을 만든다는 점은 동일합니다.
① 막대 그래프 (Bar) — 연도별 사고건수
📚 막대 그래프란?
🎯 목적 · 여러 항목의 양(量)을 비교. 막대 길이 = 값.
✨ 특징 · 시작점이 0이라 비례 인식이 직관적. 카테고리 간 차이가 한눈에 보임.
🚦 사례
  • 연도별 사고건수 — 같은 단위의 시간 비교
  • 시도별 사망자 — 지역 간 순위·격차
  • 차종별·연령대별 빈도 — 범주별 카운트
⚠️ 주의 · 항목이 20개 넘으면 가로 막대(indexAxis:'y')나 트리맵을 고려.
📝 자기 데이터로 바꾸려면: 아래 labels · values 두 배열만 자기 값으로 교체. 길이만 같으면 됩니다.
// ─── ① CDN: Chart.js 한 줄 (head에 한 번) ───
// <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
// ─── ② 캔버스: <canvas id="barChart"></canvas> ───

// ③ 데이터 준비 ─── 여기만 바꾸세요
const labels = [2016,2017,2018,2019,2020,2021,2022,2023];
const values = [220917,216335,217148,229600,209654,203130,196836,191418];

// ④ 차트 생성
new Chart(document.getElementById('barChart'), {
    type: 'bar',
    data: {
        labels,
        datasets: [{
            label: '교통사고 건수',
            data: values,
            backgroundColor: 'rgba(59,130,246,0.7)',
            borderColor: 'rgb(59,130,246)',
            borderWidth: 1
        }]
    },
    options: {
        responsive: true, maintainAspectRatio: false,
        plugins: { title: { display: true, text: '연도별 교통사고 건수' } },
        scales:  { y: { ticks: { callback: v => v.toLocaleString() } } }
    }
});
💡 maintainAspectRatio:false + 높이가 정해진 부모 div 조합이 핵심. 안 쓰면 차트가 무한히 커집니다.
② 선 그래프 (Line) — 사고건수 + 사망자 (이중축)
📚 선 그래프(이중축)란?
🎯 목적 · 시간에 따른 변화를 보여줌. 점들을 잇는 선이 "흐름"을 인식시킴.
✨ 특징 · 좌·우 두 축을 따로 두면(이중축) 스케일이 다른 두 시리즈를 한 화면에서 비교 가능.
🚦 사례
  • 사고건수(만 단위) ↔ 사망자(명 단위) — 스케일 1만:1
  • 통행량 ↔ 사고건수, 강수량 ↔ 야간사고 — 동행성 확인
  • 코로나 전·후 같은 시계열 변곡점 강조
⚠️ 주의 · 이중축은 인과를 시사하지 않음. "같이 움직인다"일 뿐. 양 축 시작점을 임의로 잡으면 시각적 왜곡.
📝 핵심 패턴: 시리즈마다 yAxisID를 다르게 주고, scales에서 두 축(y, y2)을 따로 정의.
// ② 캔버스: <canvas id="lineChart"></canvas>

// ③ 데이터 준비 ─── 여기만 바꾸세요
const years     = [2016,2017,2018,2019,2020,2021,2022,2023];
const accidents = [22.1,21.6,21.7,23.0,21.0,20.3,19.7,19.1];  // 만건
const deaths    = [4292,4185,3781,3349,3081,2916,2735,2551];       // 명

// ④ 차트 생성 ─── 단위 다른 두 시리즈는 yAxisID로 축 분리
new Chart(document.getElementById('lineChart'), {
    type: 'line',
    data: { labels: years, datasets: [
        { label: '사고건수(만건)', data: accidents, yAxisID: 'y',
          borderColor: '#3b82f6', backgroundColor: 'rgba(59,130,246,0.15)',
          tension: 0.3, fill: true, pointRadius: 4 },
        { label: '사망자(명)',    data: deaths,    yAxisID: 'y2',
          borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.15)',
          tension: 0.3, fill: true, pointRadius: 4 }
    ]},
    options: {
        responsive: true, maintainAspectRatio: false,
        interaction: { mode: 'index', intersect: false },  // 같은 x에서 두 점 동시 툴팁
        scales: {
            y:  { position: 'left',  title: { display: true, text: '사고건수(만)' } },
            y2: { position: 'right', title: { display: true, text: '사망자(명)' },
                  grid: { drawOnChartArea: false } }   // 오른쪽 축 격자 숨김
        }
    }
});
③ 파이 / 도넛 — 사고유형별 비율
📚 파이 / 도넛이란?
🎯 목적 · 전체 100% 중 부분 비율을 한눈에. "차대차가 전체의 74%"처럼.
✨ 특징 · type만 'pie' ↔ 'doughnut'으로 바꾸면 끝. 도넛은 가운데에 합계 텍스트 넣기 좋음.
🚦 사례
  • 사고유형별 비율(차대차·차대사람·차량단독)
  • 월별 사망 비율, 가해 차종 비율
⚠️ 주의 · 항목이 5개 넘으면 각도 차이가 안 보임 → 막대가 더 정확. 시계열 비교에도 부적합.
📝 자기 데이터로: labels와 values 두 배열만 길이 맞춰 교체. 색은 자동 처리하거나 직접 지정.
// ② 캔버스 2개: <canvas id="pieChart">, <canvas id="doughnutChart">

// ③ 데이터 준비
const labels = ['차대차','차대사람','차량단독','철길건널목'];
const values = [142350, 42580, 6380, 108];
const colors = ['#3b82f6','#ef4444','#f59e0b','#10b981'];
const total  = values.reduce((a,b) => a+b, 0);

// ④ 차트 생성 ─── pie/doughnut은 type만 다르므로 함수로 묶음
function drawPie(canvasId, type) {
    new Chart(document.getElementById(canvasId), {
        type,                                     // 'pie' 또는 'doughnut'
        data: { labels, datasets: [{ data: values,
            backgroundColor: colors, borderColor: 'white', borderWidth: 2 }] },
        options: {
            responsive: true, maintainAspectRatio: false,
            plugins: { tooltip: { callbacks: {
                label: c => ` ${c.label}: ${c.raw.toLocaleString()}건 ` +
                            `(${(c.raw/total*100).toFixed(1)}%)`
            }}}
        }
    });
}
drawPie('pieChart',      'pie');
drawPie('doughnutChart', 'doughnut');
④ 레이더 (Radar) — 시도별 다차원 비교
📚 레이더 차트란?
🎯 목적 · 여러 항목 점수를 동시에 보여주는 다각형 프로파일. 모양 자체가 "특성"의 시각 패턴이 됨.
✨ 특징 · 다각형을 겹쳐 그려 대상 간 패턴 차이를 비교. 꼭짓점 = 항목, 다각형 = 비교 대상.
🚦 사례
  • 시도별 (사고건수·사망·부상·사망률·10만명당) 5지표 동시 비교
  • 운전자 프로파일(반응속도·시야·집중력·경험·피로도)
⚠️ 주의 · 항목 단위가 다르면 큰 값이 지배 → 0~1 정규화 필수. 다각형 3개 넘으면 겹쳐 안 보임.
📝 구조: labels = 꼭짓점 항목, datasets = 비교할 대상들(다각형 N개).
// ② <canvas id="radarChart"></canvas>

// ③ 데이터 준비 ─── labels는 항목, 각 dataset은 비교 대상
const labels    = ['서울','경기','부산','대구','인천','광주','대전'];
const accidents = [342, 584, 156, 98, 145, 72, 81];   // 100건 단위
const deaths    = [252, 681, 178, 98, 141, 67, 83];   // 명

// ④ 차트 생성
new Chart(document.getElementById('radarChart'), {
    type: 'radar',
    data: { labels, datasets: [
        { label: '사고건수(백건)', data: accidents,
          borderColor: '#3b82f6', backgroundColor: 'rgba(59,130,246,0.25)',
          pointBackgroundColor: '#3b82f6' },
        { label: '사망자(명)',    data: deaths,
          borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.25)',
          pointBackgroundColor: '#ef4444' }
    ]},
    options: { responsive: true, maintainAspectRatio: false }
});
⑤ 버블 차트 — 사고건수(x) × 사망자(y) × 인구(원 크기)
📚 버블 차트란?
🎯 목적 · 산점도에 "세 번째 변수"를 원의 크기로 추가. 한 차트에 3변수 동시 표현.
✨ 특징 · 위치(x,y) + 크기(r). 색상까지 쓰면 4변수도 가능.
🚦 사례
  • x=사고건수, y=사망자, r=인구 → "인구 대비 위험 지역"
  • x=주행거리, y=사고율, r=차령
⚠️ 주의 · r은 면적이 값에 비례하도록 √값으로 변환 권장. 직접 r=값으로 하면 큰 값이 과장됨.
📝 데이터 형식: 각 점은 {x, y, r} 객체. 시도별로 색을 다르게 주려면 dataset을 시도별로 분리.
// ② <canvas id="bubbleChart"></canvas>

// ③ 데이터 준비 ─── 시도별 (x: 사고건수, y: 사망자, r: 인구만)
const sido       = ['서울','경기','부산','대구','인천','광주','대전'];
const accidents  = [34215,58432,15621,9843,14523,7234,8123];
const deaths     = [252,681,178,98,141,67,83];
const population = [950,1340,330,240,295,143,145];  // 만 명

// 시도별로 색을 분리하기 위해 dataset 7개 생성
const datasets = sido.map((name, i) => ({
    label: name,
    data: [{
        x: accidents[i],
        y: deaths[i],
        r: Math.sqrt(population[i]) * 0.8   // 면적이 인구에 비례
    }],
    backgroundColor: `hsla(${i*45},70%,55%,0.65)`,
    borderColor:    `hsla(${i*45},70%,40%,0.9)`,
    borderWidth: 1
}));

// ④ 차트 생성
new Chart(document.getElementById('bubbleChart'), {
    type: 'bubble',
    data: { datasets },
    options: {
        responsive: true, maintainAspectRatio: false,
        plugins: { tooltip: { callbacks: {
            label: c => `${c.dataset.label}: 사고 ${c.raw.x.toLocaleString()}건, ` +
                        `사망 ${c.raw.y}명`
        }}},
        scales: { x: { title: { display: true, text: '사고건수' } },
                  y: { title: { display: true, text: '사망자(명)' } } }
    }
});
⑥ 산점도 (Scatter) — 사고건수와 사망률의 상관
📚 산점도란?
🎯 목적 · 두 변수의 관계(상관·역상관·무관)를 점 분포로 확인. 통계 분석의 출발점.
✨ 특징 · 우상향=양의 상관, 우하향=음의 상관, 흩어짐=무관. 회귀선·이상치도 한눈에.
🚦 사례
  • x=사고건수, y=사망률 → "사고 많은 곳이 더 위험한가?"
  • x=평균속도, y=중상 비율 → 속도-피해의 비선형 관계
⚠️ 주의 · 산점도는 상관(correlation)일 뿐 인과(causation)가 아님.
📝 데이터 형식: {x, y} 객체 배열. label을 추가하면 툴팁에서 점 이름 표시 가능.
// ② <canvas id="scatterChart"></canvas>

// ③ 데이터 준비 ─── {x, y, label} 객체 배열
const points = [
    { x: 34215, y: 0.74, label: '서울' },
    { x: 58432, y: 1.17, label: '경기' },
    { x: 15621, y: 1.14, label: '부산' },
    { x:  9843, y: 1.00, label: '대구' },
    { x: 14523, y: 0.97, label: '인천' },
    { x:  7234, y: 0.93, label: '광주' },
    { x:  8123, y: 1.02, label: '대전' }
];

// ④ 차트 생성
new Chart(document.getElementById('scatterChart'), {
    type: 'scatter',
    data: { datasets: [{
        label: '시도', data: points,
        backgroundColor: 'rgba(99,102,241,0.7)',
        borderColor: '#4338ca',
        pointRadius: 8, pointHoverRadius: 11
    }]},
    options: {
        responsive: true, maintainAspectRatio: false,
        plugins: { tooltip: { callbacks: {
            label: c => `${c.raw.label}: ${c.raw.x.toLocaleString()}건, ${c.raw.y}%`
        }}},
        scales: { x: { title: { display: true, text: '사고건수' } },
                  y: { title: { display: true, text: '사망률(%)' } } }
    }
});
⑦ Polar Area — 시간대별 사고 분포
📚 Polar Area란?
🎯 목적 · 파이와 비슷하지만 각도는 모두 같고 반지름이 값. 카테고리 수와 양을 동시에 인식.
✨ 특징 · 시각적 임팩트가 강함. 큰 값일수록 멀리 뻗어나가는 형태로 "어디가 위험한지" 즉각 인식.
🚦 사례
  • 시간대별 사고(주중주간·야간·주말·심야): 5~7개 카테고리
  • 요일별 사고 분포(월~일 7개)
⚠️ 주의 · 면적은 r²에 비례해 시각적 과장 발생. 정밀 비교는 막대가 정확.
📝 핵심: pie/doughnut과 데이터 구조 동일, type만 'polarArea'.
// ② <canvas id="polarChart"></canvas>

// ③ 데이터 준비
const labels = ['주중 주간','주중 야간','주말 주간','주말 야간','심야(0–6시)'];
const values = [78532, 42180, 38420, 28910, 9376];
const colors = ['rgba(59,130,246,0.6)', 'rgba(239,68,68,0.6)',
                'rgba(245,158,11,0.6)', 'rgba(16,185,129,0.6)',
                'rgba(139,92,246,0.6)'];

// ④ 차트 생성 ─── pie와 거의 동일
new Chart(document.getElementById('polarChart'), {
    type: 'polarArea',
    data: { labels, datasets: [{ data: values, backgroundColor: colors }] },
    options: {
        responsive: true, maintainAspectRatio: false,
        plugins: { title: { display: true, text: '시간대별 사고 분포' } }
    }
});
⑧ 혼합 차트 (Mixed) — 막대(건수) + 선(사망률)
📚 혼합(Mixed) 차트란?
🎯 목적 · "양"과 "비율"처럼 성격이 다른 두 지표를 한 차트에. 막대=양, 선=추세.
✨ 특징 · dataset마다 type을 다르게 주면 자동으로 합쳐짐. 이중축과 결합하면 강력.
🚦 사례
  • 막대=사고건수, 선=사망률(%) → "건수는 줄지만 치사율은 그대로"
  • 막대=월별 단속 건수, 선=음주 사고 비율
⚠️ 주의 · 시리즈 3개 넘으면 헷갈림. 핵심 2개만.
📝 패턴: 최상위 type은 기본값(보통 'bar'), 각 dataset에서 type을 덮어써서 섞기.
// ② <canvas id="mixedChart"></canvas>

// ③ 데이터 준비
const years    = [2016,2017,2018,2019,2020,2021,2022,2023];
const counts   = [220917,216335,217148,229600,209654,203130,196836,191418];
const deaths   = [4292,4185,3781,3349,3081,2916,2735,2551];
const fatality = counts.map((c,i) => +(deaths[i]/c*100).toFixed(2));  // 사망률(%)

// ④ 차트 생성 ─── dataset마다 type을 따로 지정
new Chart(document.getElementById('mixedChart'), {
    type: 'bar',                   // 기본값(아래에서 덮어쓸 수 있음)
    data: { labels: years, datasets: [
        { type: 'bar',  label: '사고건수', data: counts,   yAxisID: 'y',
          backgroundColor: 'rgba(59,130,246,0.6)' },
        { type: 'line', label: '사망률(%)', data: fatality, yAxisID: 'y2',
          borderColor: '#ef4444', tension: 0.3, pointRadius: 5, fill: false }
    ]},
    options: {
        responsive: true, maintainAspectRatio: false,
        interaction: { mode: 'index', intersect: false },
        scales: {
            y:  { position: 'left',  title: { display: true, text: '건수' },
                  ticks: { callback: v => v.toLocaleString() } },
            y2: { position: 'right', title: { display: true, text: '사망률(%)' },
                  grid: { drawOnChartArea: false } }
        }
    }
});
⑨ 트리맵 (Treemap) — 시도 × 사고유형 계층
📚 트리맵이란?
🎯 목적 · "부분/전체 비율을 사각형 면적으로" + "계층 구조를 그룹 박스로". 한 화면에 수십~수백 항목 표시 가능.
✨ 특징 · 면적이 곧 양. 색으로 카테고리 구분. 항목이 많아도 일관된 표현.
🚦 사례
  • "권역 → 시도 → 사고유형" 3계층 비율
  • "차종 → 연식 → 빈도"
⚠️ 주의 · 사각형 면적은 길이보다 인식이 약함. 정확한 순위는 막대가 우수. 플러그인 별도 로드 필수.
📝 데이터 형식이 다른 차트들과 다름: 객체 배열(rows). key=면적 기준 컬럼, groups=계층 컬럼들.
// ① 트리맵은 별도 플러그인 CDN을 추가로 로드
// <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
// <script src="https://cdn.jsdelivr.net/npm/chartjs-chart-treemap@2.3.1/dist/chartjs-chart-treemap.min.js"></script>
// ② <canvas id="treemapChart"></canvas>

// ③ 데이터 준비 ─── 객체 배열(rows). 각 row가 한 박스
const tree = [
    { 시도:'서울', 유형:'차대차',   건수:25800 },
    { 시도:'서울', 유형:'차대사람', 건수: 6800 },
    { 시도:'경기', 유형:'차대차',   건수:44100 },
    { 시도:'경기', 유형:'차대사람', 건수:10800 },
    { 시도:'부산', 유형:'차대차',   건수:11800 },
    { 시도:'인천', 유형:'차대차',   건수:11000 }
    // ... 자기 데이터로 교체
];
const color = { 서울:'#3b82f6', 경기:'#10b981', 부산:'#f59e0b',
                대구:'#a855f7', 인천:'#ef4444' };

// ④ 차트 생성
new Chart(document.getElementById('treemapChart'), {
    type: 'treemap',
    data: { datasets: [{
        tree,                              // 위 객체 배열
        key: '건수',                       // 면적 기준 컬럼
        groups: ['시도', '유형'],            // 계층 (큰 그룹 → 작은 그룹)
        spacing: 1, borderWidth: 1,
        borderColor: 'rgba(255,255,255,0.6)',
        backgroundColor(c) {
            if (c.type !== 'data') return 'transparent';
            const sido = c.raw._data.시도 || c.raw.g;
            return (color[sido] || '#94a3b8') + (c.raw.l === 0 ? 'cc' : '88');
        },
        labels: { display: true, color: 'white',
            font: { size: 11, weight: '600' },
            formatter: c => c.raw ? [c.raw.g, c.raw.v.toLocaleString()] : ''
        }
    }]},
    options: { responsive: true, maintainAspectRatio: false,
        plugins: { legend: { display: false },
            tooltip: { callbacks: {
                title: items => items[0].raw.g,
                label: c => `건수: ${c.raw.v.toLocaleString()}`
            }} }
    }
});
⑩ ⭐ 인터랙티브 필터 — 드롭다운 → 차트 실시간 업데이트
📚 인터랙티브 차트의 핵심 패턴 2가지
🎯 목적 · 독자가 직접 지표·차트유형·기간을 바꾸며 데이터를 "탐색". 정적 차트와의 결정적 차이.
✨ 패턴
  • 패턴 A — 데이터만 갱신: chart.data.datasets[0].data = newData; chart.update(); (애니메이션 자연스러움)
  • 패턴 B — 차트 종류 변경: chart.destroy() → new Chart() 재생성 (Chart.js v4의 정석)
🚦 사례
  • "사망자 추이만 보고 싶다" → 지표 드롭다운으로 전환
  • "막대보다 선이 직관적" → 차트유형 토글
  • "최근 3년만" → 슬라이더로 범위 조정
⚠️ 주의 · UI 옵션 너무 많으면 길을 잃음. 핵심 변수 2~3개만.
📝 실전 팁: type이 바뀔 가능성이 있으면 그냥 항상 destroy() + 재생성이 안전. 두 패턴을 섞지 말고 통일.
// ② 캔버스 + 컨트롤 UI
// <select id="metric-select">...</select>
// <select id="type-select">...</select>
// <input  id="range-input" type="range" min="2" max="8" value="8">
// <canvas id="filterChart"></canvas>

// ③ 데이터 준비 ─── 지표를 객체로 묶어 두면 select 값으로 바로 꺼냄
const years = [2016,2017,2018,2019,2020,2021,2022,2023];
const dataMap = {
    사고건수: [220917,216335,217148,229600,209654,203130,196836,191418],
    사망자수: [4292,4185,3781,3349,3081,2916,2735,2551],
    부상자수: [331720,322780,323037,341712,304237,291608,273363,260296]
};
const color = { 사고건수: 'rgba(59,130,246,0.7)',
                사망자수: 'rgba(239,68,68,0.7)',
                부상자수: 'rgba(245,158,11,0.7)' };

// ④ 차트 생성 ─── render() 함수로 묶어 컨트롤 변경 시마다 호출
let chart = null;

function render() {
    // 4-1. 컨트롤에서 현재 값 읽기
    const metric = document.getElementById('metric-select').value;
    const type   = document.getElementById('type-select').value;
    const count  = +document.getElementById('range-input').value;

    // 4-2. 데이터를 잘라서 준비
    const labels = years.slice(-count);
    const values = dataMap[metric].slice(-count);

    // 4-3. type이 바뀔 수 있으니 항상 destroy → 재생성 (패턴 B)
    if (chart) chart.destroy();
    chart = new Chart(document.getElementById('filterChart'), {
        type,
        data: { labels, datasets: [{ label: metric, data: values,
            backgroundColor: color[metric], borderColor: color[metric],
            borderWidth: 2, fill: type=='line'||type=='radar', tension: 0.3 }] },
        options: { responsive: true, maintainAspectRatio: false,
            plugins: { title: { display: true, text: `최근 ${count}년 ${metric}` } } }
    });
}

// 4-4. 모든 컨트롤에 이벤트 연결
['metric-select', 'type-select', 'range-input'].forEach(id =>
    document.getElementById(id).addEventListener('change', render));
document.getElementById('range-input').addEventListener('input', render);
render();   // 첫 진입 시 1회 호출
최근 8년