// charts.jsx — lightweight SVG charts for UROSYS. Exports to window. // Semicircle gauge for CAR (%). thresholds: alert < warnMin, warn < okMin, ok >=. function Gauge({ value, min = 0, max = 300, threshold = 180, okMin = 180, warnMin = 150, unit = '%', size = 230 }) { const w = size,h = size * 0.62,cx = w / 2,cy = h - 6,r = w / 2 - 18; const a0 = Math.PI,a1 = 0; // 180° -> 0° const pol = (frac) => {const a = a0 + (a1 - a0) * frac;return [cx + r * Math.cos(a), cy + r * Math.sin(a)];}; const arc = (f0, f1) => {const [x0, y0] = pol(f0),[x1, y1] = pol(f1);return `M ${x0} ${y0} A ${r} ${r} 0 0 1 ${x1} ${y1}`;}; const clamp = (v) => Math.max(0, Math.min(1, (v - min) / (max - min))); const vf = clamp(value),tf = clamp(threshold); const color = value >= okMin ? T.ok : value >= warnMin ? T.warn : T.alert; const [nx, ny] = pol(vf); return ( {/* zones */} {/* value arc */} {/* threshold tick */} {/* center value */} {value.toFixed(1)}{unit} Threshold {threshold}{unit} {min} {max} ); } // CAR 12-month trend line with threshold reference function TrendChart({ data, threshold = 180, w = 640, h = 200, unit = '%' }) { const padL = 40,padR = 14,padT = 16,padB = 26; const iw = w - padL - padR,ih = h - padT - padB; const vals = data.map((d) => d.v); const min = Math.min(...vals, threshold) - 10,max = Math.max(...vals, threshold) + 10; const x = (i) => padL + i / (data.length - 1) * iw; const y = (v) => padT + ih - (v - min) / (max - min) * ih; const line = data.map((d, i) => `${x(i)},${y(d.v)}`).join(' '); const area = `${padL},${padT + ih} ${line} ${padL + iw},${padT + ih}`; const yTicks = [min, (min + max) / 2, max].map(Math.round); return ( {yTicks.map((t, i) => {t})} {/* threshold */} Regulatory minimum {threshold}{unit} {data.map((d, i) => {i === data.length - 1 && } {d.m} )} ); } // horizontal stacked composition bar + legend function StackBar({ segments, total, unit = 'tỷ' }) { return (
{segments.map((s) =>
)}
{segments.map((s) =>
{s.label} {(s.value / total * 100).toFixed(1)}% {s.value.toLocaleString()}
)}
Total Value-at-Risk {total.toLocaleString()} {unit}
); } // utilization meter (current vs limit) with threshold ticks function MeterBar({ pct, ticks = [], color }) { const c = color || (pct >= 100 ? T.alert : pct >= 80 ? T.warn : T.ok); return (
{ticks.map((t) => )}
); } // Multi-series trend — mode 'line' (overlaid) or 'stack' (stacked area). data: {days:[], series:{name:[]}, colors:{name}} function MultiTrend({ days, series, colors, unit = '억', h = 240, mode = 'line', yTickN = 4, zero = true, marker = null, markers = [] }) { const w = 720,padL = 46,padR = 16,padT = 14,padB = 26; const iw = w - padL - padR,ih = h - padT - padB; const names = Object.keys(series),N = days.length; let max = 0, min = 0; if (mode === 'stack') {for (let i = 0; i < N; i++) {let s = 0;names.forEach((n) => s += series[n][i]);if (s > max) max = s;}max = max * 1.08;} else { names.forEach((n) => series[n].forEach((v) => {if (v > max) max = v;})); if (zero) {max = max * 1.08;} else { min = Infinity;names.forEach((n) => series[n].forEach((v) => {if (v < min) min = v;})); const pad = (max - min) * 0.15 || max * 0.02;min = Math.max(0, min - pad);max = max + pad; } } const span = max - min || 1; const x = (i) => padL + i / (N - 1) * iw,y = (v) => padT + ih - (v - min) / span * ih; const ticks = Array.from({ length: yTickN + 1 }, (_, i) => Math.round(min + span * i / yTickN)); const fmt = (v) => v >= 10000 ? (v / 10000).toFixed(1) + '만' : v.toLocaleString(); const cum = names.map(() => new Array(N).fill(0)); if (mode === 'stack') {for (let i = 0; i < N; i++) {let acc = 0;names.forEach((n, ni) => {acc += series[n][i];cum[ni][i] = acc;});}} // helpers: interpolate value at frac position on first series const valAt = (frac) => { const nm = names[0]; const pos = frac * (N - 1); const i0 = Math.floor(pos), i1 = Math.min(N - 1, i0 + 1), f = pos - i0; return series[nm][i0] * (1 - f) + series[nm][i1] * f; }; return ( {names.map((n) => )} {ticks.map((t, i) => {fmt(t)})} {mode === 'stack' ? names.slice().reverse().map((n) => {const ni = names.indexOf(n); const top = cum[ni].map((v, i) => `${x(i)},${y(v)}`).join(' '); const bot = (ni > 0 ? cum[ni - 1] : new Array(N).fill(0)).map((v, i) => `${x(i)},${y(v)}`).reverse().join(' '); return ;}) : names.map((n) => {const pts = series[n].map((v, i) => `${x(i)},${y(v)}`).join(' '); return ;})} {/* vline markers (gray remark lines) — drawn before dots so dots appear on top */} {markers.filter((m) => m.type === 'vline').map((m, idx) => {const mx = padL + m.frac * iw; return {m.label && {m.label}};})} {/* legacy single marker (danger date: dashed line + dot + top label) */} {marker && (() => {const mx = padL + marker.frac * iw;return {marker.label};})()} {/* dot markers (sell signal red dots) — drawn last so always on top */} {markers.filter((m) => m.type === 'dot').map((m, idx) => {const mx = padL + m.frac * iw; return ;})} {days.map((d, i) => i % 5 === 0 || i === N - 1 ? {d} : null)} ); } // Donut composition. data:[{label,value,color}], center label/value function Donut({ data, size = 170, thick = 26, centerTop, centerBig, unit }) { const total = data.reduce((s, d) => s + d.value, 0); const r = (size - thick) / 2,cx = size / 2,cy = size / 2,C = 2 * Math.PI * r; let off = 0; return (
{data.map((d, i) => {const frac = d.value / total;const dash = frac * C;const el = ;off += dash;return el;})} {centerTop && {centerTop}} {centerBig && {centerBig}{unit}}
{data.map((d, i) =>
{d.label} {(d.value / total * 100).toFixed(1)}%
)}
); } // Treemap-style heat cells. data:[{label,value,intensity 0..1}] function HeatMap({ data, h = 180, max }) { const total = data.reduce((s, d) => s + d.value, 0); const mx = max || Math.max(...data.map((d) => d.intensity)); const heat = (t) => {const f = Math.min(1, t / mx); // light->deep red const r = Math.round(248 - (248 - 176) * f),g = Math.round(232 - (232 - 30) * f),b = Math.round(232 - (232 - 36) * f);return `rgb(${r},${g},${b})`;}; // simple row layout: widths balance value (sqrt-compressed) with a readable floor return (
{data.map((d, i) => {const f = Math.min(1, d.intensity / mx);const dark = f > 0.45; return (
{d.label}
{d.value.toLocaleString()}
강도 {(d.intensity * 100).toFixed(2)}%
); })}
); } // Ranked horizontal bars. data:[{label,value,color,sub}] function HBars({ data, unit = '억', h }) { const max = Math.max(...data.map((d) => d.value)); return (
{data.map((d, i) =>
{d.label} {d.sub && {d.sub}} {d.value.toLocaleString()} {unit}
)}
); } // tiny inline sparkline function Spark({ data, color = T.blue, w = 84, h = 26, fillArea = true, full = false }) { const min = Math.min(...data),max = Math.max(...data),rng = max - min || 1; const x = (i) => i / (data.length - 1) * w,y = (v) => h - 2 - (v - min) / rng * (h - 4); const pts = data.map((v, i) => `${x(i)},${y(v)}`).join(' '); return ( {fillArea && } ); } Object.assign(window, { Gauge, TrendChart, StackBar, MeterBar, MultiTrend, Donut, HeatMap, HBars, Spark });