/* ============================================================================ * 서명전에 — TDS BarChart 컴포넌트 (Task #160) * * · TDS Mobile 2026-03 BarChart 스펙. 데이터 값을 막대 높이로 시각화. * 색상 테마로 특정 막대/전체/우측 N 개를 강조할 수 있어요. * * · props: * data* BarChartData[] — value / maxValue / label / theme / barAnnotation * fill* AllBar | SingleBar | Auto — 3 타입 중 하나 (type 필드로 구분) * height number(px, 기본 205) — 차트 전체 세로 길이 (막대 개수 무관) * className / style — 패스스루 * * · data item (BarChartData): * value* number — 실제 값 (막대 높이 = value/maxValue 비례) * maxValue number — 생략 시 data 내 최대 value 자동 사용 * label string — X축 레이블 * theme 7 테마 — 항목별 지정이 있으면 fill 과 병합 시 우선 * barAnnotation string|number — 막대 위에 표시할 텍스트/숫자 * * · fill 3 타입: * { type: "all-bar", theme } — 모든 막대 같은 색 * { type: "single-bar", theme, barIndex } — 특정 인덱스 1개만 * { type: "auto", count } — 오른쪽부터 count 개, * blue → green → yellow → orange → red → grey 순 * * · 7 테마: blue / green / yellow / orange / red / grey / default * default = 회색 중립 (적극 강조 아님). 미지정 = default. * * · data.length > 12 시 첫 번째와 마지막 항목만 label + barAnnotation 을 * 표시해요 (텍스트 겹침 방지). 중간 막대는 색만 보여요. * * · 모든 색/폰트/간격은 var(--tds-…) 토큰. 하드코딩 없음. * · prefers-reduced-motion 시 막대 transition OFF. * * · role="img" + aria-label 자동 (데이터 요약) — 스크린 리더 대응. * ========================================================================== */ // ─── 상수 ───────────────────────────────────────────────────────────── const __BCHART_THEMES = ["blue", "green", "yellow", "orange", "red", "grey", "default"]; // auto 타입 오른쪽부터 적용 순서 (스펙 고정). const __BCHART_AUTO_ORDER = ["blue", "green", "yellow", "orange", "red", "grey"]; const __BCHART_LABEL_CAP = 12; // data 길이가 이 값을 초과하면 첫/마지막만 표시. function __bchartWarn(msg) { if (typeof console !== "undefined" && console.warn) console.warn("[BarChart] " + msg); } function __bchartTheme(raw) { if (raw == null) return "default"; if (__BCHART_THEMES.indexOf(raw) === -1) { __bchartWarn('theme "' + raw + '" 은 지원하지 않아요. 가능한 값: ' + __BCHART_THEMES.join(", ") + ". \"default\" 로 폴백."); return "default"; } return raw; } // ─── fill 결정 로직 ───────────────────────────────────────────────── // 각 막대(index 기준)의 최종 테마를 계산. // 항목 개별 theme > fill 타입별 테마 > "default" function __bchartResolveThemes(data, fill) { const n = data.length; const out = new Array(n).fill("default"); if (!fill || typeof fill !== "object") { // fill 미지정 — data 자체의 theme 만 사용, 나머지는 default. for (let i = 0; i < n; i++) { out[i] = __bchartTheme(data[i] && data[i].theme); } return out; } const type = fill.type; if (type === "all-bar") { const t = __bchartTheme(fill.theme); for (let i = 0; i < n; i++) { // 항목별 theme 가 명시되어 있으면 그걸 우선. out[i] = data[i] && data[i].theme ? __bchartTheme(data[i].theme) : t; } return out; } if (type === "single-bar") { const t = __bchartTheme(fill.theme); let idx = fill.barIndex; if (typeof idx !== "number" || !isFinite(idx)) { __bchartWarn("single-bar 의 barIndex 는 number 여야 해요. 0 으로 폴백."); idx = 0; } idx = Math.floor(idx); if (idx < 0 || idx >= n) { __bchartWarn("barIndex=" + idx + " 는 data 범위(0~" + (n - 1) + ") 밖이에요."); } for (let i = 0; i < n; i++) { if (data[i] && data[i].theme) out[i] = __bchartTheme(data[i].theme); else if (i === idx) out[i] = t; else out[i] = "default"; } return out; } if (type === "auto") { let count = fill.count; if (typeof count !== "number" || !isFinite(count)) { __bchartWarn("auto 의 count 는 number 여야 해요. data.length 로 폴백."); count = n; } count = Math.max(0, Math.min(n, Math.floor(count))); // 오른쪽에서 count 개 — 가장 오른쪽이 blue, 왼쪽으로 가며 auto 순서. // count 가 6 초과면 6번째 이후 (더 왼쪽) 는 마지막 색(grey) 반복 — 보수적 해석. for (let i = 0; i < n; i++) { if (data[i] && data[i].theme) { out[i] = __bchartTheme(data[i].theme); continue; } const distFromRight = n - 1 - i; // 0 이 가장 오른쪽 if (distFromRight >= count) { out[i] = "default"; } else { const orderIdx = distFromRight < __BCHART_AUTO_ORDER.length ? distFromRight : __BCHART_AUTO_ORDER.length - 1; // grey 에 고정 out[i] = __BCHART_AUTO_ORDER[orderIdx]; } } return out; } __bchartWarn('fill.type "' + type + '" 은 지원하지 않아요. "all-bar"/"single-bar"/"auto" 중 하나여야 해요.'); // 알 수 없는 타입 — data 자체의 theme 만 사용. for (let i = 0; i < n; i++) { out[i] = __bchartTheme(data[i] && data[i].theme); } return out; } // ─── maxValue 결정 ──────────────────────────────────────────────── // 항목마다 maxValue 가 다를 수 있지만, 차트 전체 높이 정규화를 위해 // max 값을 각 막대별 개별 처리 (스펙 상 "value/maxValue 비율"). // 항목의 maxValue 생략 시 data 전체 최대 value 로 폴백. function __bchartResolveMax(data) { let globalMax = 0; for (let i = 0; i < data.length; i++) { const v = data[i] && typeof data[i].value === "number" ? data[i].value : 0; if (v > globalMax) globalMax = v; } if (globalMax === 0) globalMax = 1; // 0 분모 방지 return globalMax; } function __bchartRatio(value, maxV) { if (typeof value !== "number" || !isFinite(value)) return 0; if (typeof maxV !== "number" || !isFinite(maxV) || maxV <= 0) return 0; const r = value / maxV; if (r < 0) return 0; if (r > 1) return 1; return r; } // ─── BarChart 본체 ─────────────────────────────────────────────── function BarChart({ data, fill, height = 205, className = "", style, ...rest }) { if (!Array.isArray(data) || data.length === 0) { __bchartWarn("data 는 길이 > 0 인 배열이어야 해요."); return null; } const safeHeight = typeof height === "number" && isFinite(height) && height > 0 ? height : 205; const themes = __bchartResolveThemes(data, fill); const globalMax = __bchartResolveMax(data); // 데이터가 많으면 첫/마지막만 label + annotation 노출. const manyData = data.length > __BCHART_LABEL_CAP; // 접근성: "1월 6, 2월 5, …" 같은 요약 라벨. const aria = data .map((d) => (d && d.label ? d.label + " " : "") + (d && d.value != null ? d.value : "")) .join(", "); const mergedStyle = Object.assign( { "--tds-bchart-h": safeHeight + "px" }, style || {} ); const cls = ["tds-bchart", manyData && "tds-bchart--many", className] .filter(Boolean) .join(" "); return (
); } // ─── 글로벌 등록 ───────────────────────────────────────────────── window.BarChart = BarChart; Object.assign(window, { BarChart });