// 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 (
);
};
// --- 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 (
);
};
// --- 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 (
);
};
// --- 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 (
);
};
// --- 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 (
);
};
// --- 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 (
);
};
// --- 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 });