@ihabdevteam/core 0.160.0 → 0.162.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/dist/components.cjs.js +171 -171
- package/dist/components.esm.css +1 -1
- package/dist/hooks.cjs.js +1 -1
- package/dist/i18n.cjs.js +1 -1
- package/dist/ihabdevteam-core.cjs.js +19 -19
- package/dist/ihabdevteam-core.d.ts +9 -2
- package/dist/packages/core/src/components/BarChart.css +23 -1
- package/dist/packages/core/src/components/BarChart.d.ts +1 -1
- package/dist/packages/core/src/components/BarChart.js +58 -14
- package/dist/packages/core/src/components/ChartSection.css +4 -2
- package/dist/packages/core/src/components/barChartTypes.d.ts +7 -0
- package/dist/packages/core/src/version.d.ts +1 -1
- package/dist/packages/core/src/version.js +1 -1
- package/dist/utils.cjs.js +1 -1
- package/package.json +1 -1
|
@@ -309,7 +309,7 @@ export declare type BadgeType = 'bar' | 'dot';
|
|
|
309
309
|
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(월~일) 막대
|
|
310
310
|
* 원시 렌더링은 내부 BarChartCanvas가 담당한다.
|
|
311
311
|
*/
|
|
312
|
-
export declare function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, rateAgg, tightMax, avgMode, totalFormat, series, seriesMode, loading, onDetail, headExtra, detailSize, className, inGrid, }: BarChartProps): JSX.Element;
|
|
312
|
+
export declare function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, rateAgg, tightMax, avgMode, totalFormat, series, seriesMode, loading, onDetail, headExtra, detailSize, barGap, baseline, className, inGrid, }: BarChartProps): JSX.Element;
|
|
313
313
|
|
|
314
314
|
/** @public */
|
|
315
315
|
export declare interface BarChartControl {
|
|
@@ -410,6 +410,13 @@ export declare interface BarChartProps {
|
|
|
410
410
|
* 막대 수가 늘면 굵기는 `barWidth` 를 위 한계로 두고 줄어든다(계열 수만큼 센다).
|
|
411
411
|
*/
|
|
412
412
|
seriesMode?: 'stack' | 'group';
|
|
413
|
+
/** 0 선을 캔버스 가운데 두고 **음수는 아래로** 그린다(예: 변동률 %p). 주면 축은 위아래 대칭(±max|값|)이 되고,
|
|
414
|
+
* 눈금·격자선·이름표가 음수 구간까지 따라가며, 툴팁은 양수에 `+` 를 붙여 방향을 드러낸다.
|
|
415
|
+
* `series` + `seriesMode='group'` 과 함께 쓰면 갈래마다 0 선 위·아래로 자란다. 지금은 `0` 만 쓴다. */
|
|
416
|
+
baseline?: number;
|
|
417
|
+
/** 나란히 선 막대 사이 간격 px(기본 1). `seriesMode='group'` 에서만 쓰인다 — 한 칸에 서는 막대가 늘수록
|
|
418
|
+
* `barWidth` 와 함께 좁혀 묶음이 한 덩어리로 읽히게 한다(말귀: 5개면 굵기 6·간격 2 … 2개면 9·5). */
|
|
419
|
+
barGap?: number;
|
|
413
420
|
/** 로딩 중 — 차트 영역에 스피너 표시(헤더/레이아웃은 유지) */
|
|
414
421
|
loading?: boolean;
|
|
415
422
|
/** 지정 시 헤더 우측에 '상세보기' 버튼 표시 → 클릭 콜백 */
|
|
@@ -1493,7 +1500,7 @@ export declare function copyText(text: string): Promise<boolean>;
|
|
|
1493
1500
|
*
|
|
1494
1501
|
* 값은 손으로 적고 `test/coreVersion.test.ts` 가 package.json 과 어긋나면 운다.
|
|
1495
1502
|
*/
|
|
1496
|
-
export declare const CORE_VERSION = "0.
|
|
1503
|
+
export declare const CORE_VERSION = "0.162.0";
|
|
1497
1504
|
|
|
1498
1505
|
/** @public */
|
|
1499
1506
|
declare const coreDefault: Record<string, unknown>;
|
|
@@ -151,7 +151,29 @@
|
|
|
151
151
|
/* 나란히(seriesMode 'group') — 한 칸에 계열이 네다섯 선다. **칸 사이가 계열 사이보다 늘 넓어야**
|
|
152
152
|
어느 막대가 어느 주·날인지 읽힌다. 계열 사이는 1px 로 붙이고, 칸 양옆에 4px 를 둬 칸 사이가
|
|
153
153
|
적어도 8px 이 되게 한다(막대가 줄어 칸이 꽉 차도 무리가 붙지 않는다). */
|
|
154
|
+
/* 0 선 기준 막대 — 반쪽 둘(위·아래)을 쌓아 둔 자리. 나란히 선 칸의 폭 나눔은 그대로 두고, 0 선에서 위나 아래로 자란다 */
|
|
155
|
+
.manager-bar-chart-slot {
|
|
156
|
+
flex: 0 1 var(--bar-width);
|
|
157
|
+
height: 100%;
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
min-width: 0;
|
|
161
|
+
}
|
|
162
|
+
.manager-bar-chart-slot__half {
|
|
163
|
+
height: 50%;
|
|
164
|
+
display: flex;
|
|
165
|
+
justify-content: center;
|
|
166
|
+
}
|
|
167
|
+
.manager-bar-chart-slot__half--up { align-items: flex-end; }
|
|
168
|
+
.manager-bar-chart-slot__half--down { align-items: flex-start; }
|
|
169
|
+
.manager-bar-chart-bar--full { flex: 1 1 auto; width: 100%; }
|
|
170
|
+
/* 아래로 자라는 막대는 아래쪽 모서리를 둥글린다 */
|
|
171
|
+
.manager-bar-chart-bar--down { border-radius: 0 0 6px 6px; }
|
|
172
|
+
/* 0 선은 다른 격자선보다 또렷하게 */
|
|
173
|
+
.manager-bar-chart-gridline--baseline { opacity: 1; }
|
|
174
|
+
|
|
154
175
|
.manager-bar-chart-col--group {
|
|
155
|
-
|
|
176
|
+
/* 기본 1px — barGap prop 으로 칸마다 바꾼다 */
|
|
177
|
+
gap: var(--bar-group-gap, 1px);
|
|
156
178
|
padding: 0 4px;
|
|
157
179
|
}
|
|
@@ -13,4 +13,4 @@ import type { BarChartProps } from './barChartTypes';
|
|
|
13
13
|
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(월~일) 막대
|
|
14
14
|
* 원시 렌더링은 내부 BarChartCanvas가 담당한다.
|
|
15
15
|
*/
|
|
16
|
-
export default function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, rateAgg, tightMax, avgMode, totalFormat, series, seriesMode, loading, onDetail, headExtra, detailSize, className, inGrid, }: BarChartProps): import("react").JSX.Element;
|
|
16
|
+
export default function BarChart({ size, barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal, totalUnitKey, totalLabelKey, control: controlProp, hideControls, rate, rateAgg, tightMax, avgMode, totalFormat, series, seriesMode, loading, onDetail, headExtra, detailSize, barGap, baseline, className, inGrid, }: BarChartProps): import("react").JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import './BarChart.css';
|
|
4
4
|
import { useState, useMemo, useCallback, useEffect } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
@@ -10,12 +10,13 @@ import Loader from './Loader';
|
|
|
10
10
|
import { getTodayKstDate } from '../utils/date';
|
|
11
11
|
import DescriptionTooltip from './Tooltip/DescriptionTooltip';
|
|
12
12
|
import ChartSection, { ChartTotal, ChartLegend } from './ChartSection';
|
|
13
|
+
import { devWarnOnce } from '../utils/devWarn';
|
|
13
14
|
import { firstMondayOfMonth, KO_ORD } from './barChartData';
|
|
14
15
|
import { useBarChartControl } from './useBarChartControl';
|
|
15
16
|
// ─── 막대 렌더링 원시 컴포넌트 ───────────────────────────────────
|
|
16
17
|
/** 막대 값 — 수가 아니면(undefined·NaN·Infinity) 0 */
|
|
17
18
|
const 수 = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
18
|
-
function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, height = 140, maxOverride, tightMax, seriesColors, seriesLabels, rateLabels, grouped = false }) {
|
|
19
|
+
function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, barGap = 1, height = 140, maxOverride, tightMax, seriesColors, seriesLabels, rateLabels, grouped = false, baseline }) {
|
|
19
20
|
// colorB 미지정 시 단일 계열(A) 모드
|
|
20
21
|
const single = !colorB;
|
|
21
22
|
const stacked = !!(seriesColors && seriesColors.length > 0);
|
|
@@ -27,7 +28,18 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, h
|
|
|
27
28
|
? Math.max(0, ...(d.parts ?? []).map(수))
|
|
28
29
|
: (d.parts ?? []).reduce((s, v) => s + Math.max(0, 수(v)), 0)))
|
|
29
30
|
: Math.max(0, ...data.filter(d => d.a !== -1).flatMap(d => single ? [수(d.a)] : [수(d.a), 수(d.b)])), [data, single, stacked, grouped]);
|
|
30
|
-
|
|
31
|
+
/* 기준선 모드: 0 선에서 얼마나 떨어졌는지(|값 − 기준|)가 축을 정한다 — 위아래가 대칭이라야 같은 크기의 오름·내림이 같은 길이로 읽힌다 */
|
|
32
|
+
const 기준 = baseline;
|
|
33
|
+
const 대칭최대 = useMemo(() => {
|
|
34
|
+
if (기준 == null)
|
|
35
|
+
return 0;
|
|
36
|
+
const 값들 = data.filter(d => d.a !== -1).flatMap(d => stacked ? (d.parts ?? []).map(수) : (single ? [수(d.a)] : [수(d.a), 수(d.b)]));
|
|
37
|
+
return Math.max(0, ...값들.map(v => Math.abs(v - 기준)));
|
|
38
|
+
}, [data, 기준, stacked, single]);
|
|
39
|
+
const padded = useMemo(() => {
|
|
40
|
+
const m = 기준 != null ? 대칭최대 : maxVal;
|
|
41
|
+
return maxOverride ?? (tightMax ? (m > 0 ? m : 10) : (Math.ceil(m / 10) * 10 || 10));
|
|
42
|
+
}, [maxVal, 대칭최대, 기준, maxOverride, tightMax]);
|
|
31
43
|
const h = height;
|
|
32
44
|
// 막대 너비 — 여백과 무관하게 막대 '개수'(컬럼 × 계열)가 늘수록 점진 축소(최대 barWidth)
|
|
33
45
|
const barsPerCol = stacked ? (grouped ? Math.max(1, seriesColors.length) : 1) : (single ? 1 : 2);
|
|
@@ -39,12 +51,23 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, h
|
|
|
39
51
|
/* 굵기는 인라인 flex/maxWidth 가 아니라 CSS 변수로 박는다 — 소비자가 `!important`
|
|
40
52
|
없이 CSS 로 덮을 수 있게. 읽는 규칙은 BarChart.css 의 `.manager-bar-chart-bar`·`-stack`. */
|
|
41
53
|
const 막대굵기 = { '--bar-width': `${effBarWidth}px` };
|
|
54
|
+
/* 나란히 선 막대 사이 간격 — 칸(.manager-bar-chart-col--group)의 gap 으로 내려보낸다 */
|
|
55
|
+
const 묶음간격 = { '--bar-group-gap': `${Math.max(0, barGap)}px` };
|
|
42
56
|
const ticks = useMemo(() => {
|
|
43
57
|
const mid = Math.round(padded / 2);
|
|
58
|
+
if (기준 != null) {
|
|
59
|
+
/* 위 → 0 → 아래. 맨 아래 값은 x축 자리라 아래 렌더에서 뺀다(기존 규칙과 같다) */
|
|
60
|
+
const 위 = [기준 + padded, ...(mid !== padded ? [기준 + mid] : []), 기준];
|
|
61
|
+
return [...위, ...(mid !== padded ? [기준 - mid] : []), 기준 - padded];
|
|
62
|
+
}
|
|
44
63
|
return mid !== padded ? [padded, mid, 0] : [padded, 0];
|
|
45
|
-
}, [padded]);
|
|
64
|
+
}, [padded, 기준]);
|
|
65
|
+
/** 값 → 판 위에서의 높이 비율(0~1) 자리 — 기준선 모드는 0 선이 한가운데 */
|
|
66
|
+
const 눈금자리 = (t) => (기준 != null ? (1 - (t - (기준 - padded)) / (2 * padded)) : (1 - t / padded));
|
|
46
67
|
const [tooltip, setTooltip] = useState(null);
|
|
47
68
|
const handleBarLeave = useCallback(() => setTooltip(null), []);
|
|
69
|
+
/* 기준선 모드에서는 오름·내림이 뜻이라 양수에도 + 를 붙인다(부호가 없으면 툴팁만 보고 방향을 알 수 없다) */
|
|
70
|
+
const 부호붙이기 = (v) => (기준 != null && v > 기준 ? `+${v}` : `${v}`);
|
|
48
71
|
/* 떠 있는 동안 **스크롤하거나 막대 밖을 누르면** 닫는다 — 터치에서는 톡이 호환 mouseenter 로 띄우고 mouseleave 는 다른 곳을
|
|
49
72
|
눌러야 와서, 스크롤해도 제자리에 떠 있었고 누를 수 없는 곳을 톡해도 안 닫혔다(말귀 훈련 현황·매니저 화면, 태블릿). */
|
|
50
73
|
const 떠있음 = tooltip != null;
|
|
@@ -65,12 +88,16 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, h
|
|
|
65
88
|
document.removeEventListener('pointerdown', 바깥누름, true);
|
|
66
89
|
};
|
|
67
90
|
}, [떠있음]);
|
|
68
|
-
return (_jsxs("div", { className: "manager-bar-chart-wrap", children: [_jsx("div", { className: "manager-bar-chart-inner", children: _jsx("div", { className: "manager-bar-chart-scroll", children: _jsxs("div", { className: "manager-bar-chart-area", children: [_jsxs("div", { className: "manager-bar-chart-plot", style: { height: h }, children: [ticks.slice(0, -1).map((t, i) => (
|
|
91
|
+
return (_jsxs("div", { className: "manager-bar-chart-wrap", style: 묶음간격, children: [_jsx("div", { className: "manager-bar-chart-inner", children: _jsx("div", { className: "manager-bar-chart-scroll", children: _jsxs("div", { className: "manager-bar-chart-area", children: [_jsxs("div", { className: "manager-bar-chart-plot", style: { height: h }, children: [ticks.slice(0, -1).map((t, i) => (
|
|
69
92
|
/* 눈금선은 **적힌 값의 높이**에 둔다 — 가운데 값을 반올림(최대 3 → '2')하면서 자리는 절반(1.5)에 두어 선과 숫자가 어긋났다 */
|
|
70
|
-
_jsx("div", { className:
|
|
93
|
+
_jsx("div", { className: `manager-bar-chart-gridline${기준 != null && t === 기준 ? ' manager-bar-chart-gridline--baseline' : ''}`, style: { top: 눈금자리(t) * h }, children: _jsx("span", { className: "manager-bar-chart-gridline__label caption-1", children: t }) }, i))), data.map((d, i) => {
|
|
71
94
|
const isFuture = d.a === -1;
|
|
72
95
|
/* 판 높이를 넘지 않는다 — maxOverride(진행률 100)보다 큰 값이 와도 막대가 눈금 위로 삐져나가지 않게 */
|
|
73
|
-
|
|
96
|
+
/* 기준선 모드는 반쪽(h/2)을 쓴다 — 0 선에서 위나 아래로 자란다 */
|
|
97
|
+
const 반 = h / 2;
|
|
98
|
+
const 높이 = (v) => (기준 != null
|
|
99
|
+
? Math.min(반, (Math.abs(수(v) - 기준) / padded) * 반)
|
|
100
|
+
: Math.min(h, Math.max(0, (수(v) / padded) * h)));
|
|
74
101
|
const ha = isFuture ? 0 : 높이(d.a);
|
|
75
102
|
const hb = isFuture ? 0 : 높이(d.b);
|
|
76
103
|
const parts = (d.parts ?? []).map(수);
|
|
@@ -89,7 +116,7 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, h
|
|
|
89
116
|
}
|
|
90
117
|
else if (stacked && grouped) {
|
|
91
118
|
/* 자리가 고정이라 0 도 적는다 — 빈 자리가 무엇인지 알 수 있게 */
|
|
92
|
-
items = (seriesLabels ?? []).map((lb, k) => ({ text: `${lb}: ${parts[k] ?? 0}`, color: (seriesColors ?? [])[k] }));
|
|
119
|
+
items = (seriesLabels ?? []).map((lb, k) => ({ text: `${lb}: ${부호붙이기(parts[k] ?? 0)}`, color: (seriesColors ?? [])[k] }));
|
|
93
120
|
}
|
|
94
121
|
else if (stacked) {
|
|
95
122
|
const 전부 = (seriesLabels ?? []).map((lb, k) => ({ text: `${lb}: ${parts[k] ?? 0}`, color: (seriesColors ?? [])[k] }));
|
|
@@ -111,7 +138,14 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, h
|
|
|
111
138
|
setTooltip({ rect: { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height }, title, items });
|
|
112
139
|
}, onMouseLeave: handleBarLeave, children: !isFuture && (stacked && grouped ? (
|
|
113
140
|
/* 나란히 — 계열 k 는 늘 k 번째 자리. 0 이어도 자리를 두어 칸마다 같은 색이 같은 곳에 선다 */
|
|
114
|
-
(seriesColors ?? []).map((c, k) =>
|
|
141
|
+
(seriesColors ?? []).map((c, k) => {
|
|
142
|
+
const v = parts[k] ?? 0;
|
|
143
|
+
if (기준 == null)
|
|
144
|
+
return _jsx("div", { className: "manager-bar-chart-bar", style: { ...막대굵기, height: 높이(v), background: c } }, k);
|
|
145
|
+
/* 0 선 위·아래 — 반쪽 둘을 쌓아 둔 자리(slot)에 그려, 나란히 선 칸의 폭 나눔은 그대로 둔다 */
|
|
146
|
+
const 아래 = 수(v) < 기준;
|
|
147
|
+
return (_jsxs("div", { className: "manager-bar-chart-slot", style: 막대굵기, children: [_jsx("div", { className: "manager-bar-chart-slot__half manager-bar-chart-slot__half--up", children: !아래 && _jsx("div", { className: "manager-bar-chart-bar manager-bar-chart-bar--full", style: { height: 높이(v), background: c } }) }), _jsx("div", { className: "manager-bar-chart-slot__half manager-bar-chart-slot__half--down", children: 아래 && _jsx("div", { className: "manager-bar-chart-bar manager-bar-chart-bar--full manager-bar-chart-bar--down", style: { height: 높이(v), background: c } }) })] }, k));
|
|
148
|
+
})) : stacked ? (
|
|
115
149
|
// 계열별 stacked — 아래부터 위로 쌓고, 맨 위 segment만 상단 라운드
|
|
116
150
|
_jsx("div", { className: "manager-bar-chart-stack", style: 막대굵기, children: (() => {
|
|
117
151
|
const topK = parts.reduce((acc, v, k) => (v > 0 ? k : acc), -1); // 맨 위(최상단) 세그먼트
|
|
@@ -123,7 +157,7 @@ function BarChartCanvas({ data, colorA, colorB, labelA, labelB, barWidth = 10, h
|
|
|
123
157
|
남은 -= 조각;
|
|
124
158
|
return _jsx("div", { className: "manager-bar-chart-stack-segment", style: { height: 조각, background: (seriesColors ?? [])[k], borderRadius: k === topK ? '6px 6px 0 0' : undefined } }, k);
|
|
125
159
|
});
|
|
126
|
-
})() })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "manager-bar-chart-bar", style: { ...막대굵기, height: ha, background: colorA } }), !single && _jsx("div", { className: "manager-bar-chart-bar", style: { ...막대굵기, height: hb, background: colorB } })] }))) }, i));
|
|
160
|
+
})() })) : (기준 != null ? (_jsx(_Fragment, { children: [{ v: 수(d.a), c: colorA }, ...(single ? [] : [{ v: 수(d.b), c: colorB }])].map(({ v, c }, k) => (_jsxs("div", { className: "manager-bar-chart-slot", style: 막대굵기, children: [_jsx("div", { className: "manager-bar-chart-slot__half manager-bar-chart-slot__half--up", children: v >= 기준 && _jsx("div", { className: "manager-bar-chart-bar manager-bar-chart-bar--full", style: { height: 높이(v), background: c } }) }), _jsx("div", { className: "manager-bar-chart-slot__half manager-bar-chart-slot__half--down", children: v < 기준 && _jsx("div", { className: "manager-bar-chart-bar manager-bar-chart-bar--full manager-bar-chart-bar--down", style: { height: 높이(v), background: c } }) })] }, k))) })) : (_jsxs(_Fragment, { children: [_jsx("div", { className: "manager-bar-chart-bar", style: { ...막대굵기, height: ha, background: colorA } }), !single && _jsx("div", { className: "manager-bar-chart-bar", style: { ...막대굵기, height: hb, background: colorB } })] })))) }, i));
|
|
127
161
|
})] }), _jsx("div", { className: "manager-bar-chart-x-axis", children: data.map((d, i) => {
|
|
128
162
|
const isFuture = d.a === -1; // 아직 도래하지 않은 날 — 글씨 흐리게
|
|
129
163
|
return (_jsxs("span", { className: `manager-bar-chart-x-axis__label${isFuture ? ' manager-bar-chart-x-axis__label--future' : ''}`, children: [_jsxs("span", { className: "caption-1 manager-bar-chart-x-axis__main", children: [d.isToday && _jsx("span", { className: "manager-bar-chart-x-axis__today-dot", style: { background: colorA } }), d.label] }), d.sublabel && _jsx("span", { className: "caption-1", children: d.sublabel })] }, i));
|
|
@@ -219,7 +253,7 @@ const BAR_SIZE_PRESET = {
|
|
|
219
253
|
* - 주(week): 'YYYY년 M월 N째주' 드롭다운 + 그 주의 요일(월~일) 막대
|
|
220
254
|
* 원시 렌더링은 내부 BarChartCanvas가 담당한다.
|
|
221
255
|
*/
|
|
222
|
-
export default function BarChart({ size = 'medium', barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal = false, totalUnitKey, totalLabelKey, control: controlProp, hideControls = false, rate = false, rateAgg = 'dayMean', tightMax = false, avgMode = false, totalFormat, series, seriesMode = 'stack', loading = false, onDetail, headExtra, detailSize = 32, className, inGrid = false, }) {
|
|
256
|
+
export default function BarChart({ size = 'medium', barWidth, titleKey, data, colorA, colorB, labelAKey, labelBKey, showTotal = false, totalUnitKey, totalLabelKey, control: controlProp, hideControls = false, rate = false, rateAgg = 'dayMean', tightMax = false, avgMode = false, totalFormat, series, seriesMode = 'stack', loading = false, onDetail, headExtra, detailSize = 32, barGap, baseline, className, inGrid = false, }) {
|
|
223
257
|
const { t, i18n } = useTranslation();
|
|
224
258
|
const sizePreset = BAR_SIZE_PRESET[size];
|
|
225
259
|
// 외부 control 미지정 시 내부 컨트롤 사용 (단독 차트 하위 호환)
|
|
@@ -232,6 +266,10 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
|
|
|
232
266
|
// 다른 수로 선다(주 보기 25% · 월 보기 38%). 다음 달로 넘친 날도 그 주에 든다(주의날들).
|
|
233
267
|
// 툴팁의 전체/완료(rawA·rawB)는 합 그대로 — 둘 다 사실이다.
|
|
234
268
|
const 나란히 = seriesMode === 'group' && !!series && series.length > 0;
|
|
269
|
+
/* barGap 은 나란히 선 막대 **사이** 값이라 그 밖에서는 그릴 자리가 없다 — 조용히 먹으면 "간격을 줬는데 안 변한다" 가 된다 */
|
|
270
|
+
if (barGap != null && !나란히) {
|
|
271
|
+
devWarnOnce('bar-chart-bargap-without-group', "[BarChart] barGap 은 seriesMode='group' + series 에서만 쓰인다 — 그 밖에서는 아무 일도 하지 않는다.");
|
|
272
|
+
}
|
|
235
273
|
const bars = useMemo(() => {
|
|
236
274
|
if (!rate)
|
|
237
275
|
return rawBars;
|
|
@@ -332,8 +370,13 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
|
|
|
332
370
|
앞 달 막대). 막대 합으로 내면 8월 합계에 9/1~9/6 이 들고 8/1~8/2 가 빠져, 상세 목록(달력 달)의 건수와 어긋났다.
|
|
333
371
|
주 보기는 그 주 막대의 합 그대로. */
|
|
334
372
|
const 합칠것 = control.period === 'month' ? (data[control.monthRange]?.daily ?? []) : bars;
|
|
373
|
+
/* 기준선 모드는 부호를 살려 더한다 — 오르내림의 **순변화** 가 합계다(양수만 더하면 내린 주가 사라진다).
|
|
374
|
+
미래 표시(a === -1)는 여기서도 뺀다 — 기준선 모드에서도 -1 은 '아직 오지 않은 칸' 이다(실제 -1 이 나올 수 있으면 그 칸을 자료에서 뺀다). */
|
|
375
|
+
if (baseline != null) {
|
|
376
|
+
return 합칠것.reduce((s, d) => s + (d.a === -1 ? 0 : 수(d.a)) + (labelBKey ? 수(d.b) : 0), 0);
|
|
377
|
+
}
|
|
335
378
|
return 합칠것.reduce((s, d) => s + (d.a > 0 ? d.a : 0) + (labelBKey && d.b > 0 ? d.b : 0), 0);
|
|
336
|
-
}, [bars, rawBars, rate, rateAgg, avgMode, labelBKey, control.period, control.monthRange, data]);
|
|
379
|
+
}, [bars, rawBars, rate, rateAgg, avgMode, baseline, labelBKey, control.period, control.monthRange, data]);
|
|
337
380
|
const legendItems = series && series.length > 0
|
|
338
381
|
? series.map(s => ({ color: s.color, label: s.label }))
|
|
339
382
|
: [
|
|
@@ -350,7 +393,8 @@ export default function BarChart({ size = 'medium', barWidth, titleKey, data, co
|
|
|
350
393
|
// 진행률/평균 모드 또는 totalLabelKey+totalFormat 조합: 라벨(윗줄) + 값(아랫줄). size='small'은 라벨 생략
|
|
351
394
|
_jsx(ChartTotal, { label: isSmall ? undefined : `${t(control.period === 'week' ? 'manager.chart.weeklyPrefix' : 'manager.chart.monthlyPrefix')} ${t(totalLabelKey ?? labelAKey)}`, children: totalFormat ? (
|
|
352
395
|
// 숫자는 h0, 단위(시간/분/초 등)는 h5로 분리
|
|
353
|
-
|
|
396
|
+
/* 부호는 숫자 쪽에 붙인다 — `[^\d]+` 로 가르면 '-7%p' 의 '-' 가 단위로 넘어가 h5 로 작아졌다(변동률) */
|
|
397
|
+
(totalFormat(total).match(/([+-]?\d[\d.,]*|[^\d]+)/g) ?? []).map((part, i) => (/\d/.test(part)
|
|
354
398
|
? _jsx("span", { className: "barchart-total__num h0", children: part }, i)
|
|
355
|
-
: _jsx("span", { className: "barchart-total__unit h5", children: part }, i)))) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "barchart-total__num h0", children: total }), totalUnitKey && _jsx("span", { className: "barchart-total__unit h5", children: t(totalUnitKey) })] })) })) : (_jsxs(ChartTotal, { children: [_jsx("span", { className: "barchart-total__num h0", children: total }), _jsx("span", { className: "barchart-total__unit h5", children: t(labelAKey) }), totalUnitKey && _jsx("span", { className: "barchart-total__unit h5", children: t(totalUnitKey) })] }))) : _jsx("span", {}), _jsx(ChartLegend, { items: legendItems, className: "caption-1" })] }), children: loading ? (_jsx("div", { className: "manager-bar-chart-loading", children: _jsx(Loader, { size: 32 }) })) : (_jsx(BarChartCanvas, { data: bars, colorA: colorA, colorB: labelBKey ? colorB : undefined, labelA: t(labelAKey), labelB: labelBKey ? t(labelBKey) : undefined, barWidth: barWidth ?? sizePreset.barWidth, height: sizePreset.height, maxOverride: rate ? 100 : undefined, tightMax: tightMax, seriesColors: series?.map(s => s.color), seriesLabels: series?.map(s => s.label), rateLabels: rate ? { total: t('trainingStatus.today.tooltipTotal', '전체'), done: t('trainingStatus.today.tooltipDone', '완료') } : undefined, grouped: 나란히 })) }));
|
|
399
|
+
: _jsx("span", { className: "barchart-total__unit h5", children: part }, i)))) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "barchart-total__num h0", children: total }), totalUnitKey && _jsx("span", { className: "barchart-total__unit h5", children: t(totalUnitKey) })] })) })) : (_jsxs(ChartTotal, { children: [_jsx("span", { className: "barchart-total__num h0", children: total }), _jsx("span", { className: "barchart-total__unit h5", children: t(labelAKey) }), totalUnitKey && _jsx("span", { className: "barchart-total__unit h5", children: t(totalUnitKey) })] }))) : _jsx("span", {}), _jsx(ChartLegend, { items: legendItems, className: "caption-1" })] }), children: loading ? (_jsx("div", { className: "manager-bar-chart-loading", children: _jsx(Loader, { size: 32 }) })) : (_jsx(BarChartCanvas, { data: bars, colorA: colorA, colorB: labelBKey ? colorB : undefined, labelA: t(labelAKey), labelB: labelBKey ? t(labelBKey) : undefined, barWidth: barWidth ?? sizePreset.barWidth, barGap: barGap, baseline: baseline, height: sizePreset.height, maxOverride: rate ? 100 : undefined, tightMax: tightMax, seriesColors: series?.map(s => s.color), seriesLabels: series?.map(s => s.label), rateLabels: rate ? { total: t('trainingStatus.today.tooltipTotal', '전체'), done: t('trainingStatus.today.tooltipDone', '완료') } : undefined, grouped: 나란히 })) }));
|
|
356
400
|
}
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
/* 좁으면 줄을 바꾼다 — 안 바꾸면 오른쪽 묶음(고르개·상세보기)이 제목 위로 올라탄다 */
|
|
25
25
|
flex-wrap: wrap;
|
|
26
26
|
gap: var(--spacing-xs);
|
|
27
|
-
|
|
27
|
+
/* 머리 바로 아래가 합계 줄이라 12px 은 떠 보인다 — 한 눈금 줄인다 */
|
|
28
|
+
margin-bottom: var(--spacing-xs);
|
|
28
29
|
}
|
|
29
30
|
/* 제목은 줄어들 수 있어야 한다 — min-width 기본값(auto)이면 글자 길이만큼 버텨 넘친다 */
|
|
30
31
|
.manager-section__title {
|
|
@@ -104,7 +105,8 @@
|
|
|
104
105
|
.barchart-total--stacked {
|
|
105
106
|
flex-direction: column;
|
|
106
107
|
align-items: flex-start;
|
|
107
|
-
|
|
108
|
+
/* 이름표와 값이 딱 붙어 한 덩어리로 읽혔다 — 눈금보다 작은 2px 만 띄운다(8px 은 두 줄이 갈라져 보인다) */
|
|
109
|
+
gap: 2px;
|
|
108
110
|
}
|
|
109
111
|
.barchart-total--stacked .barchart-total__label,
|
|
110
112
|
.barchart-total--stacked .barchart-total__num,
|
|
@@ -95,6 +95,13 @@ export interface BarChartProps {
|
|
|
95
95
|
* 막대 수가 늘면 굵기는 `barWidth` 를 위 한계로 두고 줄어든다(계열 수만큼 센다).
|
|
96
96
|
*/
|
|
97
97
|
seriesMode?: 'stack' | 'group';
|
|
98
|
+
/** 0 선을 캔버스 가운데 두고 **음수는 아래로** 그린다(예: 변동률 %p). 주면 축은 위아래 대칭(±max|값|)이 되고,
|
|
99
|
+
* 눈금·격자선·이름표가 음수 구간까지 따라가며, 툴팁은 양수에 `+` 를 붙여 방향을 드러낸다.
|
|
100
|
+
* `series` + `seriesMode='group'` 과 함께 쓰면 갈래마다 0 선 위·아래로 자란다. 지금은 `0` 만 쓴다. */
|
|
101
|
+
baseline?: number;
|
|
102
|
+
/** 나란히 선 막대 사이 간격 px(기본 1). `seriesMode='group'` 에서만 쓰인다 — 한 칸에 서는 막대가 늘수록
|
|
103
|
+
* `barWidth` 와 함께 좁혀 묶음이 한 덩어리로 읽히게 한다(말귀: 5개면 굵기 6·간격 2 … 2개면 9·5). */
|
|
104
|
+
barGap?: number;
|
|
98
105
|
/** 로딩 중 — 차트 영역에 스피너 표시(헤더/레이아웃은 유지) */
|
|
99
106
|
loading?: boolean;
|
|
100
107
|
/** 지정 시 헤더 우측에 '상세보기' 버튼 표시 → 클릭 콜백 */
|