// 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 (
);
}
// 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 (
);
}
// horizontal stacked composition bar + legend
function StackBar({ segments, total, unit = 'tỷ' }) {
return (
{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 (
);
}
// 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 (
);
}
// 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) =>
{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 (
);
}
Object.assign(window, { Gauge, TrendChart, StackBar, MeterBar, MultiTrend, Donut, HeatMap, HBars, Spark });