// charts.jsx — Data visualization primitives const { useState: useStateC, useMemo: useMemoC } = React; // --- Horizontal Score Bar (with benchmark marker for 일반 기준) --- const ScoreBar = ({ label, value, base, max = 100, color, tone = "default", animate = true }) => { const pct = Math.max(0, Math.min(100, (value / max) * 100)); const basePct = base != null ? (base / max) * 100 : null; const fillColor = tone === "alert" ? "var(--alert)" : tone === "watch" ? "var(--watch)" : tone === "good" ? "var(--good)" : color || undefined; return (
{label}
{basePct != null &&
}
{value}
); }; // --- Radial Donut (overall skin score / package coverage) --- const Donut = ({ value, max = 100, size = 140, stroke = 12, color = "var(--ink-1)", track = "var(--bg-subtle)", label, subtitle }) => { const r = (size - stroke) / 2; const c = 2 * Math.PI * r; const pct = Math.max(0, Math.min(100, (value / max) * 100)); const dash = (pct / 100) * c; return (
{label &&
{label}
}
{value}
{subtitle &&
{subtitle}
}
); }; // --- Sparkline --- const Sparkline = ({ data, w = 200, h = 56, color = "var(--phase-tint)", fillOpacity = 0.12, showDots = false }) => { const min = Math.min(...data), max = Math.max(...data); const pad = 4; const px = (i) => pad + (i / (data.length - 1)) * (w - pad * 2); const py = (v) => pad + (1 - (v - min) / (max - min || 1)) * (h - pad * 2); const path = data.map((v, i) => `${i ? "L" : "M"} ${px(i).toFixed(1)} ${py(v).toFixed(1)}`).join(" "); const area = `${path} L ${px(data.length - 1)} ${h - pad} L ${px(0)} ${h - pad} Z`; return ( {showDots && data.map((v, i) => ( ))} ); }; // --- Mini bar chart (last N days) --- const MiniBars = ({ data, w = 200, h = 56, color = "var(--phase-tint)", gap = 3 }) => { const max = Math.max(...data); const bw = (w - gap * (data.length - 1)) / data.length; return ( {data.map((v, i) => { const bh = Math.max(2, (v / max) * (h - 4)); return ; })} ); }; // --- Radar chart (skin scores all-in-one) --- const Radar = ({ axes, values, baseline, size = 320, max = 100, color = "var(--data-blue)" }) => { const cx = size / 2, cy = size / 2; const r = size * 0.38; const n = axes.length; const point = (i, v) => { const angle = -Math.PI / 2 + (i / n) * Math.PI * 2; const rad = (v / max) * r; return [cx + Math.cos(angle) * rad, cy + Math.sin(angle) * rad]; }; const polyData = values.map((v, i) => point(i, v)); const polyBase = baseline ? baseline.map((v, i) => point(i, v)) : null; const polyStr = (arr) => arr.map((p, i) => `${i ? "L" : "M"} ${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(" ") + " Z"; return ( {/* Grid rings */} {[0.25, 0.5, 0.75, 1].map((f, i) => ( ))} {/* Axis lines */} {axes.map((_, i) => { const angle = -Math.PI / 2 + (i / n) * Math.PI * 2; return ; })} {/* Baseline polygon */} {polyBase && ( )} {/* Actual polygon */} {/* Dots */} {polyData.map((p, i) => ( ))} {/* Labels */} {axes.map((a, i) => { const angle = -Math.PI / 2 + (i / n) * Math.PI * 2; const lx = cx + Math.cos(angle) * (r + 18); const ly = cy + Math.sin(angle) * (r + 18); const anchor = Math.abs(Math.cos(angle)) < 0.2 ? "middle" : Math.cos(angle) > 0 ? "start" : "end"; return ( {a} ); })} ); }; // --- Bar chart (horizontal, treatment fit) --- const HBarChart = ({ items, max = 100, w = "100%", showLabels = true }) => (
{items.map((it, i) => (
{it.label} {it.sub &&
{it.sub}
}
{it.value}
))}
); // --- Line chart with grid (BI) --- const LineChart = ({ data, w = 520, h = 180, color = "var(--phase-tint)", label, yUnit = "", baseline }) => { const min = 0; const max = Math.ceil(Math.max(...data, baseline ? Math.max(...baseline) : 0) * 1.1); const padL = 32, padR = 8, padT = 8, padB = 22; const px = (i) => padL + (i / (data.length - 1)) * (w - padL - padR); const py = (v) => padT + (1 - (v - min) / (max - min)) * (h - padT - padB); const path = data.map((v, i) => `${i ? "L" : "M"} ${px(i).toFixed(1)} ${py(v).toFixed(1)}`).join(" "); const area = `${path} L ${px(data.length - 1)} ${h - padB} L ${px(0)} ${h - padB} Z`; const yTicks = [0, 0.25, 0.5, 0.75, 1].map(f => Math.round(max * f)); return ( {/* Grid */} {yTicks.map((t, i) => { const y = padT + (1 - i / 4) * (h - padT - padB); return ( {t}{yUnit} ); })} {/* Baseline */} {baseline && ( `${i ? "L" : "M"} ${px(i).toFixed(1)} ${py(v).toFixed(1)}`).join(" ")} fill="none" stroke="var(--ink-3)" strokeWidth="1.2" strokeDasharray="3 3" /> )} {/* Area */} {/* End dot */} ); }; // --- Heatmap overlay SVG (for face analysis viz) --- const HeatmapOverlay = ({ kind = "pores" }) => { // Approximate face regions const specs = { pores: [ // nose + cheeks density { cx: 50, cy: 52, r: 7, fill: "rgba(255, 80, 60, 0.45)" }, { cx: 38, cy: 56, r: 9, fill: "rgba(255, 120, 60, 0.32)" }, { cx: 62, cy: 56, r: 9, fill: "rgba(255, 120, 60, 0.32)" }, ], sebum: [ { cx: 50, cy: 32, r: 12, fill: "rgba(255, 200, 60, 0.45)" }, { cx: 50, cy: 52, r: 8, fill: "rgba(255, 200, 60, 0.55)" }, ], redness: [ { cx: 38, cy: 60, r: 10, fill: "rgba(220, 60, 70, 0.38)" }, { cx: 62, cy: 60, r: 10, fill: "rgba(220, 60, 70, 0.38)" }, { cx: 50, cy: 56, r: 6, fill: "rgba(220, 60, 70, 0.42)" }, ], pigment: [ { cx: 40, cy: 48, r: 3, fill: "rgba(140, 80, 50, 0.6)" }, { cx: 60, cy: 50, r: 2.5, fill: "rgba(140, 80, 50, 0.6)" }, { cx: 35, cy: 64, r: 2, fill: "rgba(140, 80, 50, 0.6)" }, { cx: 65, cy: 65, r: 2.5, fill: "rgba(140, 80, 50, 0.6)" }, { cx: 50, cy: 70, r: 2, fill: "rgba(140, 80, 50, 0.6)" }, ], wrinkle: [ { cx: 50, cy: 30, r: 14, fill: "rgba(120, 100, 180, 0.20)" }, { cx: 38, cy: 42, r: 5, fill: "rgba(120, 100, 180, 0.32)" }, { cx: 62, cy: 42, r: 5, fill: "rgba(120, 100, 180, 0.32)" }, ], }[kind] || []; return ( {specs.map((s, i) => )} ); }; // --- Coverage gauge (linear) --- const CoverageGauge = ({ value = 0.6, label, color }) => (
{label} {Math.round(value * 100)}%
); Object.assign(window, { ScoreBar, Donut, Sparkline, MiniBars, Radar, HBarChart, LineChart, HeatmapOverlay, CoverageGauge });