워드클라우드 · Sankey · Calendar Heatmap · Sunburst · Parallel Coords · Matrix · 박스플롯 · Gauge · Funnel · 코드 그대로 복사 → 데이터만 교체
<head>에 한 번만 (차트별로 다름)<canvas>(Chart.js) 또는 <div id=...>(Plotly·ApexCharts·D3)fetch()로 외부 JSON, 또는 사용자 입력(textarea)new Chart(canvas, config)Plotly.newPlot(divId, traces, layout)new ApexCharts(el, opts).render()// ── 외부 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 = 객체 배열 */ });
// ① 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); }
{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 } });
{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();
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 });
{ 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 });
{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 } } } } });
// ② <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 });
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);
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 });