✨ JS 기타 분석 시각화 종합

워드클라우드 · Sankey · Calendar Heatmap · Sunburst · Parallel Coords · Matrix · 박스플롯 · Gauge · Funnel · 코드 그대로 복사 → 데이터만 교체

⓪ 시작하기
① 워드클라우드 ⭐
② Sankey
③ Calendar Heatmap
④ Sunburst
⑤ Parallel Coords
⑥ 매트릭스 히트맵
⑦ 박스플롯
⑧ Gauge / KPI
⑨ Funnel
⓪ 다양한 시각화 라이브러리 — 어떤 차트에 어떤 도구?
이 페이지는 Chart.js 하나만으로 부족한 "특수 시각화"들을 모았습니다. 각 차트마다 가장 적합한 라이브러리가 다르므로, 탭마다 CDN과 호출 방식이 살짝 다릅니다. 패턴은 모두 동일합니다:
  1. ① CDN 스크립트 로드 — HTML <head>에 한 번만 (차트별로 다름)
  2. ② 컨테이너 만들기 — <canvas>(Chart.js) 또는 <div id=...>(Plotly·ApexCharts·D3)
  3. ③ 데이터 준비 — 배열로 직접, fetch()로 외부 JSON, 또는 사용자 입력(textarea)
  4. ④ 차트 생성 — 라이브러리별 API 호출
🧰 라이브러리별 역할 분담 (이 페이지 기준)
Chart.js + 플러그인 · ② Sankey, ⑥ Matrix  canvas  new Chart(canvas, config)
D3 + d3-cloud · ① 워드클라우드  SVG  가장 자유도 높음, 직접 그리기
Plotly · ④ Sunburst, ⑤ Parallel, ⑦ Box, ⑧ Gauge, ⑨ Funnel  div  Plotly.newPlot(divId, traces, layout)
ApexCharts · ③ Calendar Heatmap  div  new ApexCharts(el, opts).render()
💡 fetch로 데이터 불러오는 방법은 모든 라이브러리에서 동일합니다. 외부에서 데이터를 가져온 뒤 위 ③ 단계의 배열만 채우면 됩니다.
// ── 외부 JSON에서 데이터 가져오기 (모든 라이브러리 공통) ──
fetch('data.json')
    .then(res => res.json())
    .then(rows => {
        // rows 예: [{label:'서울', value:34215}, ...]
        const labels = rows.map(r => r.label);
        const values = rows.map(r => r.value);
        drawChart(labels, values);   // 어떤 라이브러리든 데이터만 넣어주면 됨
    });

// ── CSV 가져오기 (D3 사용 시 더 간단) ──
d3.csv('data.csv').then(rows => { /* rows = 객체 배열 */ });
💡 학생 미션: 각 탭의 코드 블록에서 노란 표시된 데이터 부분만 자기 데이터로 교체. 차트 구조(option, layout)는 그대로 둬도 동작합니다.
① ⭐ 워드클라우드 — 텍스트 단어 빈도 시각화 D3 + d3-cloud
📚 워드클라우드란?
🎯 목적 · 긴 텍스트(기사·인터뷰·SNS·민원)에서 "어떤 단어가 자주 나오는지"를 글자 크기로. 큰 글자 = 자주 등장.
✨ 특징 · 정량 분석은 아니지만 "한눈에 핵심 키워드"를 잡는 데 효과 만점.
🚦 사례
  • 경찰 사고 진술서 1년치 → "음주" "신호" "야간" "보행자"
  • 국민 민원 텍스트 → 정책 우선순위 단서
  • SNS·뉴스에서 "교통사고" 언급 맥락
⚠️ 주의 · 정확한 빈도 비교에는 부적합. 막대 차트와 함께 사용 권장. 불용어(은·는·이·가) 반드시 제거.
📝 흐름: 텍스트 → 토큰화(공백 분리 + 불용어 제거) → 빈도 계산 → d3-cloud로 좌표 계산 → SVG로 그리기. 텍스트만 자기 데이터로.
// ① CDN: D3 + d3-cloud
// <script src="https://d3js.org/d3.v7.min.js"></script>
// <script src="https://unpkg.com/d3-cloud@1.2.7/build/d3.layout.cloud.js"></script>
// ② <svg id="wc-svg" width="100%" height="100%"></svg>

// ③ 데이터 준비 ─── 텍스트만 바꾸면 됨
const text = `교통 사고 안전 보행자 차량 도로 신호 운전 사망 부상 음주 속도 ...`;

// 3-1. 토큰화 + 불용어 제거 + 빈도 계산
const stop = new Set(['은','는','이','가','을','를','의','에']);
const tokens = text.split(/[\s,.\n]+/).filter(t => t.length >= 2 && !stop.has(t));
const freq = {};
tokens.forEach(t => { freq[t] = (freq[t] || 0) + 1; });
const words = Object.entries(freq)
    .map(([word, count]) => ({ word, count }))
    .sort((a,b) => b.count - a.count).slice(0, 80);

// 3-2. 글자 크기 스케일 (빈도 → 픽셀)
const max = words[0].count, min = words.at(-1).count;
const scale = d3.scaleLinear().domain([min, max]).range([13, 56]);

// ④ d3-cloud가 좌표를 계산해 주면, draw에서 SVG로 그림
d3.layout.cloud()
    .size([800, 480])
    .words(words.map(d => ({ text: d.word, size: scale(d.count), count: d.count })))
    .padding(4)
    .rotate(() => ~~(Math.random()*2)*90)        // 0° 또는 90°
    .font('Pretendard, "Malgun Gothic"')
    .fontSize(d => d.size)
    .on('end', draw)
    .start();

function draw(words) {
    const svg = d3.select('#wc-svg').attr('viewBox', `0 0 800 480`);
    svg.selectAll('*').remove();
    svg.append('g').attr('transform', 'translate(400,240)')
        .selectAll('text').data(words).enter().append('text')
        .style('font-size', d => d.size + 'px')
        .style('fill', (_,i) => d3.schemeTableau10[i % 10])
        .attr('text-anchor', 'middle')
        .attr('transform', d => `translate(${d.x},${d.y})rotate(${d.rotate})`)
        .text(d => d.text);
}
② Sankey — 단계별 흐름 시각화 Chart.js + sankey 플러그인
📚 Sankey 다이어그램이란?
🎯 목적 · "A→B→C" 같은 단계별 흐름의 양을 두께로 시각화. 합계 보존(들어온 만큼 나감)이 핵심.
✨ 특징 · 좌→우 흐름. 분기·합류·손실이 자연스럽게 보임.
🚦 사례
  • "사고유형 → 시도 → 결과(경상/중상/사망)" 3단계
  • "가해 차종 → 피해 → 처분(불기소/벌금/실형)"
  • 에너지·예산·인구 이동 등 비교통 분야에도 광범위 사용
⚠️ 주의 · 단계 5개 넘으면 복잡. 사이클(A→B→A) 표현 불가.
📝 데이터 형식: {from, to, flow} 객체 배열 하나면 됨. 단계 구분은 별도로 안 해도 자동.
// ① CDN: Chart.js + sankey 플러그인
// <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-sankey@0.12.1/dist/chartjs-chart-sankey.min.js"></script>
// ② <canvas id="sankey"></canvas>

// ③ 데이터 준비 ─── from/to/flow 객체 배열
const flows = [
    { from:'차대차',   to:'서울', flow:25800 },
    { from:'차대차',   to:'경기', flow:44100 },
    { from:'차대사람', to:'서울', flow: 6800 },
    { from:'차대사람', to:'경기', flow:10800 },
    { from:'서울',     to:'경상', flow:18000 },
    { from:'서울',     to:'중상', flow: 9000 },
    { from:'서울',     to:'사망', flow:  252 },
    { from:'경기',     to:'경상', flow:32000 },
    { from:'경기',     to:'사망', flow:  681 }
    // ... 자기 흐름 데이터로
];
const nodeColors = { '차대차':'#3b82f6', '차대사람':'#ef4444',
                     '서울':'#10b981', '경기':'#10b981',
                     '경상':'#22c55e', '중상':'#f97316', '사망':'#dc2626' };

// ④ 차트 생성
new Chart(document.getElementById('sankey'), {
    type: 'sankey',
    data: { datasets: [{
        data: flows,
        colorFrom: c => nodeColors[c.dataset.data[c.dataIndex].from] || '#94a3b8',
        colorTo:   c => nodeColors[c.dataset.data[c.dataIndex].to]   || '#94a3b8',
        colorMode: 'gradient',
        size: 'max'
    }]},
    options: { responsive: true, maintainAspectRatio: false }
});
③ Calendar Heatmap — 일자별 강도 (GitHub 스타일) ApexCharts
📚 Calendar Heatmap이란?
🎯 목적 · 1년치 일별 데이터를 요일 × 주(week) 격자로 펼쳐 색 농도로 표현. GitHub의 contribution graph가 대표.
✨ 특징 · 주기성·계절성·요일효과·연휴효과가 한 화면에서 동시에 드러남.
🚦 사례
  • "금/토요일이 더 짙다" → 주말 음주 사고 효과
  • "여름·연말이 더 짙다" → 휴가철·연휴 집중
  • "특정 일자에만 폭증" → 폭우·폭설 같은 외부 사건
⚠️ 주의 · 색 단계(범위)를 잘 잡지 않으면 모든 칸이 비슷해 보임.
📝 데이터 형식: 요일별 series. 각 series는 {name, data: [{x, y}]} 형태.
// ① CDN: ApexCharts
// <script src="https://cdn.jsdelivr.net/npm/apexcharts@3.49.1"></script>
// ② <div id="cal-heat"></div>

// ③ 데이터 준비 ─── 요일을 series로, 주차를 x로
const days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
const series = days.map(d => ({
    name: d,
    data: Array.from({length: 52}, (_, w) => ({
        x: (w+1) + '째',                            // 주차 라벨
        y: Math.round(50 + Math.random()*60)         // ← 자기 데이터로 교체
    }))
}));

// ④ 차트 생성
const chart = new ApexCharts(document.getElementById('cal-heat'), {
    chart: { type: 'heatmap', height: 300, toolbar: { show: false } },
    series,
    plotOptions: { heatmap: {
        shadeIntensity: 0.5, radius: 2,
        colorScale: { ranges: [                       // 데이터 분포에 맞춰 구간 조정
            { from:0,   to:40,  color:'#fdf2f8', name:'한산' },
            { from:41,  to:70,  color:'#fbcfe8', name:'보통' },
            { from:71,  to:100, color:'#ec4899', name:'많음' },
            { from:101, to:200, color:'#9d174d', name:'매우 많음' }
        ]}
    }},
    dataLabels: { enabled: false },
    title: { text: '연간 일자별 × 요일별 사고 강도', align: 'left' },
    xaxis: { type: 'category', labels: { show: false } }
});
chart.render();
④ Sunburst — 계층 비율 (방사형) Plotly
📚 Sunburst란?
🎯 목적 · 트리맵과 비슷하지만 원형 방사 구조. 안쪽 = 상위, 바깥 = 하위. 클릭으로 줌인.
✨ 특징 · 계층 깊이를 깔끔히 표현. 면적 = 비율. 인터랙티브 탐색에 좋음.
🚦 사례
  • "권역(수도권·영남·호남·중부) → 시도 → 사고유형" 3계층
  • "가해자 연령대 → 면허 종류 → 위반항목"
⚠️ 주의 · 4계층 넘으면 바깥 원이 잘 안 보임.
📝 핵심: labels와 parents 두 배열의 같은 인덱스가 부모-자식 쌍. branchvalues:'total'이면 부모 값 = 자식 합.
// ① CDN: Plotly
// <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
// ② <div id="sunburst"></div>

// ③ 데이터 준비 ─── labels[i]의 부모는 parents[i]
const labels  = ['전국','수도권','영남','호남','서울','경기','부산','대구','광주','전남'];
const parents = ['',    '전국',  '전국','전국','수도권','수도권','영남','영남','호남','호남'];
const values  = [191418, 107170, 47932, 26591, 34215, 58432, 15621, 9843, 7234, 10234];

// ④ 차트 생성
Plotly.newPlot('sunburst', [{
    type: 'sunburst',
    labels, parents, values,
    branchvalues: 'total',                      // 부모값 = 자식 합 (정합성 필요)
    marker: { colorscale: 'Pinkyl', line: { width: 1, color: 'white' } },
    hovertemplate: '<b>%{label}</b><br>%{value:,} 건<extra></extra>',
    textinfo: 'label+percent parent'
}], {
    margin: { t: 10, l: 10, r: 10, b: 10 },
    height: 480,
    font: { family: 'Segoe UI, Malgun Gothic', size: 12 }
}, { responsive: true });
💡 조각을 클릭하면 그 그룹만 확대됩니다. 가운데 클릭으로 복귀.
⑤ Parallel Coordinates — 다변수 동시 비교 Plotly
📚 Parallel Coordinates란?
🎯 목적 · 4~10개의 변수(축)를 평행하게 늘어놓고, 각 데이터가 모든 축을 통과하는 선으로 표현. 다변수 분포·이상치·클러스터 탐색.
✨ 특징 · 축을 드래그하면 그 범위만 필터링. 색은 또 다른 변수로 매핑.
🚦 사례
  • 17개 시도의 (사고건수·사망·사망률·10만명당) 4축 동시 비교
  • 차종별 (속도·중상비율·연식·연료) 다변수 패턴
⚠️ 주의 · 축 순서에 따라 패턴이 달라 보임. 단위가 매우 다르면 정규화 권장.
📝 데이터 형식: 각 축은 { label, values }. 모든 축의 values 배열 길이가 같아야 함(=데이터 행 수).
// ② <div id="parallel"></div>

// ③ 데이터 준비 ─── 행(시도) 단위로 같은 인덱스에 정렬
const 시도   = ['서울','경기','부산','대구','인천','광주','대전'];
const 건수   = [34215, 58432, 15621, 9843, 14523, 7234, 8123];
const 사망   = [252, 681, 178, 98, 141, 67, 83];
const 사망률 = 사망.map((v,i) => +(v/건수[i]*100).toFixed(2));
const 십만명 = [35.2, 43.8, 47.2, 41.3, 48.1, 50.3, 55.7];

// ④ 차트 생성
Plotly.newPlot('parallel', [{
    type: 'parcoords',
    line: {
        color: 사망률,                              // 선 색상 = 사망률
        colorscale: 'Viridis', showscale: true,
        colorbar: { title: '사망률(%)' }
    },
    dimensions: [
        { label: '사고건수',  values: 건수 },
        { label: '사망자',    values: 사망 },
        { label: '사망률(%)', values: 사망률 },
        { label: '10만명당',  values: 십만명 }
    ]
}], {
    height: 480,
    margin: { t: 70, l: 70, r: 90, b: 40 },        // top 70 → 라벨 잘림 방지
    font: { family: 'Segoe UI, Malgun Gothic', size: 13 }
}, { responsive: true });
⑥ 매트릭스 히트맵 — 시간대 × 요일 Chart.js + matrix 플러그인
📚 매트릭스 히트맵이란?
🎯 목적 · 두 범주형 변수(요일 × 시간대 등) 교차 격자에 색 농도로 값을 표현. 두 차원의 패턴 동시 인식.
✨ 특징 · 표보다 패턴이 잘 보임. 흰색~짙은색 그라디언트로 가독성↑.
🚦 사례
  • "금요일 18–24시"가 가장 짙음 → 퇴근+불금 효과
  • 월별 × 시도별 사고 → 특정 지역의 계절성
⚠️ 주의 · 숫자 정확도는 떨어짐. 칸 안 텍스트나 툴팁으로 보완.
📝 데이터 형식: {x, y, v} 객체 배열. x·y는 카테고리 라벨, v는 색 농도 결정.
// ① CDN: Chart.js + matrix 플러그인
// <script src="https://cdn.jsdelivr.net/npm/chartjs-chart-matrix@2.0.1/dist/chartjs-chart-matrix.min.js"></script>
// ② <canvas id="matrix"></canvas>

// ③ 데이터 준비 ─── x축 라벨, y축 라벨, 그리고 {x,y,v} 셀 배열
const dayLabels  = ['월','화','수','목','금','토','일'];
const hourLabels = ['0–6시','6–12시','12–18시','18–24시'];
const data = [];
dayLabels.forEach(d => hourLabels.forEach(h => {
    data.push({ x: d, y: h, v: Math.round(80 + Math.random()*60) });
}));
const maxV = Math.max(...data.map(c => c.v));

// ④ 차트 생성
new Chart(document.getElementById('matrix'), {
    type: 'matrix',
    data: { datasets: [{
        label: '사고 강도', data,
        backgroundColor: c => `rgba(236,72,153,${0.15 + 0.75*c.raw.v/maxV})`,
        borderColor: 'white', borderWidth: 1,
        width:  ({chart}) => (chart.chartArea?.width  || 0) / dayLabels.length  - 4,
        height: ({chart}) => (chart.chartArea?.height || 0) / hourLabels.length - 4
    }]},
    options: {
        responsive: true, maintainAspectRatio: false,
        plugins: { legend: { display: false },
            tooltip: { callbacks: {
                title: items => `${items[0].raw.x} · ${items[0].raw.y}`,
                label: c => `사고 ${c.raw.v}건`
            }} },
        scales: {
            x: { type: 'category', labels: dayLabels,  grid: { display: false } },
            y: { type: 'category', labels: hourLabels, reverse: true, grid: { display: false } }
        }
    }
});
⑦ 박스플롯 — 분포(중앙·사분위·이상치) 비교 Plotly
📚 박스플롯이란?
🎯 목적 · 평균 한 숫자가 아닌 분포 전체(중앙값·25%·75%·이상치)를 한 박스로. 통계 분석의 기본.
✨ 특징 · 박스 = 25~75% 구간(IQR), 가운데 선 = 중앙값, 점 = 이상치.
🚦 사례
  • 시도별 월별 사고건수 분포 → "경기는 평균도 높고 변동도 큼"
  • 차종별 부상 정도 분포
⚠️ 주의 · 일반 독자에겐 낯설 수 있음. 의미를 캡션으로 풀어주는 게 좋음.
📝 데이터 형식: 그룹마다 관측값 배열(예: 12개월치)을 그대로 넘김. Plotly가 자동으로 통계 계산.
// ② <div id="box"></div>

// ③ 데이터 준비 ─── 그룹별 관측값 배열 (서울 12개월, 경기 12개월 ...)
const 서울월별 = [2900,2750,2650,2880,2810,2950,2700,3050,2820,2900,2780,2850];
const 경기월별 = [4900,4700,4500,4870,4820,5100,4760,5230,4880,4930,4810,4970];
const 부산월별 = [1300,1250,1180,1320,1270,1410,1190,1450,1300,1340,1240,1290];

// ④ 차트 생성 ─── 각 trace가 한 그룹
Plotly.newPlot('box', [
    { type:'box', y: 서울월별, name:'서울', boxpoints:'outliers' },
    { type:'box', y: 경기월별, name:'경기' },
    { type:'box', y: 부산월별, name:'부산' }
], {
    yaxis: { title: '월별 사고건수' },
    margin: { t:30, l:60, r:30, b:50 },
    height: 480
}, { responsive: true });
⑧ Gauge / KPI — 핵심 지표 강조 Plotly indicator
📚 Gauge / KPI 카드란?
🎯 목적 · "올해 사망자 2,551명, 작년 대비 -184명" 같은 핵심 단일 지표를 부각. 대시보드 상단에 흔히 배치.
✨ 특징 · 숫자 + 변화량(델타) + 게이지. 구간 색칠(녹·황·빨)로 상태 즉시 인식.
🚦 사례
  • 실시간 도로 안전 대시보드: 사고·사망·부상·사망률
  • 경찰서 종합 KPI: 단속·검거율·민원처리
  • "사망자 2,000명 이하 목표" 게이지
⚠️ 주의 · 숫자 한 개의 임팩트가 크므로 맥락(전년·평균·목표)도 함께.
📝 핵심 4값: 현재값(value), 비교 기준(delta.reference), 게이지 최댓값(axis.range), 임계선(threshold).
// ② <div id="kpi1" style="height:240px"></div> (KPI 카드 4개라면 4개 div)

// ③ 데이터 준비 ─── 4개 KPI 카드
const kpis = [
    { id:'kpi1', value:191418, ref:196836, max:250000, title:'사고건수 (전년 대비)', color:'#3b82f6' },
    { id:'kpi2', value:  2551, ref:  2735, max:  5000, title:'사망자 (전년 대비)',   color:'#ef4444' },
    { id:'kpi3', value:260296, ref:273363, max:350000, title:'부상자 (전년 대비)',   color:'#f59e0b' },
    { id:'kpi4', value:  1.33, ref:  1.39, max:    3.0,title:'사망률(%) (전년 대비)',color:'#a855f7' }
];

// ④ 함수로 묶어 4번 호출
function drawKPI({ id, value, ref, max, title, color }) {
    Plotly.newPlot(id, [{
        type: 'indicator',
        mode: 'gauge+number+delta',
        value,
        number: { font: { size: 32 } },
        delta:  { reference: ref,
                  increasing: { color: '#ef4444' },
                  decreasing: { color: '#10b981' },
                  valueformat: ',' },
        gauge: {
            axis: { range: [0, max] },
            bar:  { color },
            steps: [
                { range: [0,        max*0.6 ], color: '#dcfce7' },     // 안전
                { range: [max*0.6,  max*0.85], color: '#fef3c7' },     // 경계
                { range: [max*0.85, max     ], color: '#fee2e2' }      // 위험
            ],
            threshold: { line: { color: '#1e293b', width: 3 }, value: ref }
        },
        title: { text: title, font: { size: 14 } }
    }], { margin: { t:40, l:20, r:20, b:10 }, height: 240 }, { responsive: true });
}
kpis.forEach(drawKPI);
⑨ Funnel — 단계별 감소 시각화 Plotly
📚 Funnel(깔때기) 차트란?
🎯 목적 · 단계가 진행될수록 대상 수가 줄어드는 과정을 깔때기 모양으로. 단계별 통과율·이탈률 직관적.
✨ 특징 · 마케팅 전환 깔때기에서 유래. 절대값 + 첫 단계 대비 % 동시 표시.
🚦 사례
  • "신고 → 출동 → 부상 → 중상 → 사망" 단계별 감소
  • "적발 → 입건 → 기소 → 유죄" 형사 처리
⚠️ 주의 · 단계 순서가 명확하고 단조 감소일 때만. 다시 늘면 막대가 더 정직.
📝 데이터 형식: 단계 라벨 배열(y) + 값 배열(x). 두 배열 길이만 같으면 됨.
// ② <div id="funnel"></div>

// ③ 데이터 준비 ─── 단계 라벨 + 단계별 값
const stages = ['신고된 사고','경찰 출동','부상 발생','중상 이상','사망'];
const counts = [191418, 178520, 134128, 28430, 2551];

// ④ 차트 생성
Plotly.newPlot('funnel', [{
    type: 'funnel',
    y: stages,
    x: counts,
    textinfo: 'value+percent initial',           // 절대값 + 첫 단계 대비 %
    marker: { color: ['#3b82f6','#6366f1','#a855f7','#ec4899','#ef4444'] }
}], {
    margin: { t:30, l:120, r:30, b:30 },
    height: 400
}, { responsive: true });