@uniai-fe/chart-legacy 0.1.1
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/LICENSE +27 -0
- package/README.md +3 -0
- package/package.json +74 -0
- package/src/chart.scss +13 -0
- package/src/components/Contents.tsx +17 -0
- package/src/components/control/Button.tsx +39 -0
- package/src/components/control/Container.tsx +27 -0
- package/src/components/control/Provider.tsx +117 -0
- package/src/components/control/index.tsx +12 -0
- package/src/components/custom/ActivePieSector.tsx +29 -0
- package/src/components/graphs/ArcGauge.tsx +58 -0
- package/src/components/graphs/ArcMeter.tsx +112 -0
- package/src/components/graphs/Area.tsx +132 -0
- package/src/components/graphs/Bar.tsx +179 -0
- package/src/components/graphs/Doughnut.tsx +90 -0
- package/src/components/graphs/Line.tsx +123 -0
- package/src/components/graphs/index.tsx +17 -0
- package/src/components/ticks/XAxis.tsx +39 -0
- package/src/components/ticks/index.tsx +7 -0
- package/src/components.tsx +17 -0
- package/src/icon/Control.tsx +40 -0
- package/src/icon/index.tsx +7 -0
- package/src/index.tsx +3 -0
- package/src/styled.tsx +7 -0
- package/src/styles/scss/arc-gauge.scss +11 -0
- package/src/styles/scss/arc-meter.scss +6 -0
- package/src/styles/scss/control.scss +34 -0
- package/src/styles/scss/filter.scss +0 -0
- package/src/styles/scss/graph.scss +3 -0
- package/src/styles/scss/icon.scss +3 -0
- package/src/styles/scss/legend.scss +35 -0
- package/src/styles/scss/level.color.scss +7 -0
- package/src/styles/scss/tick.scss +48 -0
- package/src/styles/scss/tooltip.scss +70 -0
- package/src/styles/styled/arc-gauge.ts +65 -0
- package/src/styles/styled/arc-meter.ts +42 -0
- package/src/styles/styled/common.ts +14 -0
- package/src/styles/styled/control.ts +31 -0
- package/src/styles/styled/icon.ts +17 -0
- package/src/styles/styled/legend.ts +11 -0
- package/src/svg/control/pan-left.svg +4 -0
- package/src/svg/control/pan-right.svg +4 -0
- package/src/svg/control/zoom-in.svg +4 -0
- package/src/svg/control/zoom-out.svg +4 -0
- package/src/svg/gauge-track.min.svg +1 -0
- package/src/types/axis.d.ts +15 -0
- package/src/types/control.d.ts +109 -0
- package/src/types/data.d.ts +41 -0
- package/src/types/dot.d.ts +3 -0
- package/src/types/index.d.ts +7 -0
- package/src/types/legend.d.ts +124 -0
- package/src/types/props.d.ts +271 -0
- package/src/types/tick.d.ts +30 -0
- package/src/utils/getLevel.ts +23 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 UNIAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This project includes third-party software governed by additional licenses,
|
|
26
|
+
including Apache License 2.0. Refer to `THIRD_PARTY_NOTICES.md` for the full
|
|
27
|
+
text of those notices and any required attributions.
|
package/README.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uniai-fe/chart-legacy",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Legacy Chart Toolkit for UNIAI FE Projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"homepage": "https://www.uniai.co.kr/",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"packageManager": "pnpm@10.23.0",
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=24",
|
|
16
|
+
"pnpm": ">=10"
|
|
17
|
+
},
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "GraffitoRyu",
|
|
20
|
+
"email": "yth4135@naver.com",
|
|
21
|
+
"url": "https://github.com/GraffitoRyu"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"lint": "eslint . --max-warnings=0",
|
|
28
|
+
"typecheck": "tsc --project tsconfig.build.json --noEmit",
|
|
29
|
+
"build": "pnpm typecheck",
|
|
30
|
+
"dev": "tsc --project tsconfig.build.json --watch --noEmit",
|
|
31
|
+
"module:lint": "pnpm lint",
|
|
32
|
+
"module:typecheck": "pnpm typecheck",
|
|
33
|
+
"module:build": "pnpm build",
|
|
34
|
+
"chart-legacy:build": "pnpm run build",
|
|
35
|
+
"chart-legacy:dev": "pnpm run dev"
|
|
36
|
+
},
|
|
37
|
+
"module": "./src/index.tsx",
|
|
38
|
+
"main": "./src/index.tsx",
|
|
39
|
+
"types": "./src/types/index.d.ts",
|
|
40
|
+
"exports": {
|
|
41
|
+
".": "./src/index.tsx",
|
|
42
|
+
"./types": "./src/types/index.d.ts",
|
|
43
|
+
"./scss": "./src/index.scss"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"sass": ">= 15",
|
|
47
|
+
"react": ">= 19",
|
|
48
|
+
"react-dom": ">= 19",
|
|
49
|
+
"styled-components": ">= 6"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^24.10.1",
|
|
53
|
+
"@types/react": "^19.2.7",
|
|
54
|
+
"@types/react-dom": "^19.2.3",
|
|
55
|
+
"@uniai-fe/eslint-config": "workspace:*",
|
|
56
|
+
"@uniai-fe/next-devkit": "workspace:*",
|
|
57
|
+
"@uniai-fe/tsconfig": "workspace:*",
|
|
58
|
+
"@uniai-fe/util-functions": "workspace:*",
|
|
59
|
+
"@uniai-fe/util-react": "workspace:*",
|
|
60
|
+
"@uniai-fe/react-hooks": "workspace:*",
|
|
61
|
+
"@uniai-fe/ui-legacy": "workspace:*",
|
|
62
|
+
"autoprefixer": "^10.4.22",
|
|
63
|
+
"clsx": "^2.1.1",
|
|
64
|
+
"eslint": "^9.39.1",
|
|
65
|
+
"jotai": "^2.15.1",
|
|
66
|
+
"prettier": "^3.6.2",
|
|
67
|
+
"react": "^19.2.0",
|
|
68
|
+
"react-dom": "^19.2.0",
|
|
69
|
+
"recharts": "^3.5.0",
|
|
70
|
+
"sass": "^1.94.2",
|
|
71
|
+
"styled-components": "^6.1.19",
|
|
72
|
+
"typescript": "~5.9.3"
|
|
73
|
+
}
|
|
74
|
+
}
|
package/src/chart.scss
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
@use "./styles/scss/level.color.scss";
|
|
2
|
+
|
|
3
|
+
@use "./styles/scss/tick.scss";
|
|
4
|
+
@use "./styles/scss/graph.scss";
|
|
5
|
+
@use "./styles/scss/legend.scss";
|
|
6
|
+
@use "./styles/scss/tooltip.scss";
|
|
7
|
+
@use "./styles/scss/control.scss";
|
|
8
|
+
@use "./styles/scss/filter.scss";
|
|
9
|
+
|
|
10
|
+
@use "./styles/scss/arc-gauge.scss";
|
|
11
|
+
@use "./styles/scss/arc-meter.scss";
|
|
12
|
+
|
|
13
|
+
@use "./styles/scss/icon.scss";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import StyledChartCommon from "../styles/styled/common";
|
|
4
|
+
|
|
5
|
+
export default function ChartContents({
|
|
6
|
+
className,
|
|
7
|
+
children,
|
|
8
|
+
}: {
|
|
9
|
+
className?: string;
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
}) {
|
|
12
|
+
return (
|
|
13
|
+
<StyledChartCommon.container className={className}>
|
|
14
|
+
{children}
|
|
15
|
+
</StyledChartCommon.container>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { clsx } from "clsx";
|
|
4
|
+
import StyledChartControl from "../../styles/styled/control";
|
|
5
|
+
import IconChartControl from "../../icon/Control";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 차트 인터렉션 컨트롤 버튼
|
|
9
|
+
* @component
|
|
10
|
+
* @property {"zoom-in" | "zoom-out" | "pan-left" | "pan-right"} role - 버튼 역할
|
|
11
|
+
* @property {Function} clickEvent - 클릭 이벤트 핸들러
|
|
12
|
+
* @property {string} [className] - 추가 클래스 이름
|
|
13
|
+
* @property {React.ReactNode} [children] - 버튼 내부에 렌더링
|
|
14
|
+
*/
|
|
15
|
+
export default function ChartControlButton({
|
|
16
|
+
className,
|
|
17
|
+
role,
|
|
18
|
+
clickEvent,
|
|
19
|
+
children,
|
|
20
|
+
disabled,
|
|
21
|
+
}: {
|
|
22
|
+
role: "zoom-in" | "zoom-out" | "pan-left" | "pan-right";
|
|
23
|
+
clickEvent: () => void;
|
|
24
|
+
} & Partial<{
|
|
25
|
+
className: string;
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
disabled: boolean;
|
|
28
|
+
}>) {
|
|
29
|
+
return (
|
|
30
|
+
<StyledChartControl.button
|
|
31
|
+
type="button"
|
|
32
|
+
className={clsx("chart-control-button", className)}
|
|
33
|
+
disabled={disabled}
|
|
34
|
+
onClick={!disabled ? clickEvent : undefined}
|
|
35
|
+
>
|
|
36
|
+
{children || <IconChartControl {...{ role, width: 20, height: 20 }} />}
|
|
37
|
+
</StyledChartControl.button>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { clsx } from "clsx";
|
|
4
|
+
import StyledChartControl from "../../styles/styled/control";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 차트 인터렉션 컨트롤 컨테이너
|
|
8
|
+
* @component
|
|
9
|
+
* @property {string} [className] - 추가 클래스 이름
|
|
10
|
+
* @property {React.ReactNode} children - 자식 컴포넌트
|
|
11
|
+
*/
|
|
12
|
+
export default function ChartControlContainer({
|
|
13
|
+
className,
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
children: React.ReactNode;
|
|
17
|
+
} & Partial<{
|
|
18
|
+
className: string;
|
|
19
|
+
}>) {
|
|
20
|
+
return (
|
|
21
|
+
<StyledChartControl.container
|
|
22
|
+
className={clsx("chart-control-container", className)}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</StyledChartControl.container>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from "react";
|
|
10
|
+
import type { ChartControlContext, ChartControlRange } from "../../types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 차트 컨트롤을 위한 데이터 컨텍스트
|
|
14
|
+
* @context
|
|
15
|
+
* @desc
|
|
16
|
+
* - {object[]} originData 원본 차트 데이터
|
|
17
|
+
* - {{ start: number; end: number; }} indexRange 현재 X축 인덱스 범위
|
|
18
|
+
* - {function} setRange X축 범위 업데이트 함수
|
|
19
|
+
*/
|
|
20
|
+
const ChartControlContext = createContext<ChartControlContext>({
|
|
21
|
+
originData: [],
|
|
22
|
+
indexRange: { start: 0, end: 0 },
|
|
23
|
+
setIndexRange: () => {},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 차트 컨트롤을 위한 데이터 컨텍스트 공급자
|
|
28
|
+
* @component
|
|
29
|
+
* @param {object} props
|
|
30
|
+
* @param {object[]} props.originData 원본 차트 데이터
|
|
31
|
+
* @param {React.ReactNode} props.children
|
|
32
|
+
*/
|
|
33
|
+
export default function ChartControlProvider<
|
|
34
|
+
ChartData extends object = object,
|
|
35
|
+
>({
|
|
36
|
+
originData,
|
|
37
|
+
children,
|
|
38
|
+
}: {
|
|
39
|
+
/**
|
|
40
|
+
* 원본 차트 데이터
|
|
41
|
+
* @type {object[]}
|
|
42
|
+
*/
|
|
43
|
+
originData: ChartData[];
|
|
44
|
+
children: React.ReactNode;
|
|
45
|
+
}) {
|
|
46
|
+
const [indexRange, setIndexRange] = useState<ChartControlRange>({
|
|
47
|
+
start: 0,
|
|
48
|
+
end: originData.length > 1 ? originData.length - 1 : 0,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 초기 데이터 로딩 시(0 -> N) 전체 범위로 동기 업데이트하여 깜빡임 방지
|
|
52
|
+
const prevLenRef = useRef<number>(originData.length);
|
|
53
|
+
useLayoutEffect(() => {
|
|
54
|
+
const prev = prevLenRef.current;
|
|
55
|
+
const next = originData.length;
|
|
56
|
+
if (prev === 0 && next > 0) {
|
|
57
|
+
setIndexRange({ start: 0, end: Math.max(0, next - 1) });
|
|
58
|
+
}
|
|
59
|
+
prevLenRef.current = next;
|
|
60
|
+
}, [originData.length]);
|
|
61
|
+
|
|
62
|
+
// 데이터 길이가 변할 때 현재 indexRange를 데이터 범위에 맞춰 정돈
|
|
63
|
+
useLayoutEffect(() => {
|
|
64
|
+
const total = originData.length;
|
|
65
|
+
if (total < 0) return;
|
|
66
|
+
const maxEnd = Math.max(0, total - 1);
|
|
67
|
+
|
|
68
|
+
// 비어있는 경우 0,0 고정
|
|
69
|
+
if (total === 0) {
|
|
70
|
+
if (indexRange.start !== 0 || indexRange.end !== 0)
|
|
71
|
+
setIndexRange({ start: 0, end: 0 });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 현재 윈도우 길이 보존 시도
|
|
76
|
+
const length = Math.max(1, indexRange.end - indexRange.start + 1);
|
|
77
|
+
|
|
78
|
+
// 범위를 넘어가면 오른쪽 끝에 맞춰 재정렬
|
|
79
|
+
if (indexRange.end > maxEnd || indexRange.start > maxEnd) {
|
|
80
|
+
const newEnd = maxEnd;
|
|
81
|
+
const newStart = Math.max(0, newEnd - Math.min(length, total) + 1);
|
|
82
|
+
if (newStart !== indexRange.start || newEnd !== indexRange.end)
|
|
83
|
+
setIndexRange({ start: newStart, end: newEnd });
|
|
84
|
+
}
|
|
85
|
+
}, [originData.length, indexRange.start, indexRange.end]);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<ChartControlContext.Provider
|
|
89
|
+
value={{
|
|
90
|
+
originData,
|
|
91
|
+
indexRange,
|
|
92
|
+
setIndexRange,
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
{children}
|
|
96
|
+
</ChartControlContext.Provider>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 차트 컨트롤 데이터 컨텍스트를 사용하기 위한 커스텀 훅
|
|
102
|
+
* @hook
|
|
103
|
+
* @desc
|
|
104
|
+
* - {object[]} originData 원본 차트 데이터
|
|
105
|
+
* - {{ start: number; end: number; }} indexRange 현재 X축 인덱스 범위
|
|
106
|
+
* - {function} setIndexRange X축 범위 업데이트 함수
|
|
107
|
+
* @return {ChartControlContext} { originData, indexRange, setIndexRange }
|
|
108
|
+
*/
|
|
109
|
+
export function useChartContext<
|
|
110
|
+
ChartData extends object,
|
|
111
|
+
>(): ChartControlContext<ChartData> {
|
|
112
|
+
const context = useContext(ChartControlContext);
|
|
113
|
+
if (!context) {
|
|
114
|
+
throw new Error("차트 데이터에 대한 컨텍스트를 찾을 수 없습니다.");
|
|
115
|
+
}
|
|
116
|
+
return context as ChartControlContext<ChartData>;
|
|
117
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import ChartControlButton from "./Button";
|
|
2
|
+
import ChartControlContainer from "./Container";
|
|
3
|
+
import ChartControlProvider, { useChartContext } from "./Provider";
|
|
4
|
+
|
|
5
|
+
const ChartControl = {
|
|
6
|
+
Provider: ChartControlProvider,
|
|
7
|
+
useContext: useChartContext,
|
|
8
|
+
Container: ChartControlContainer,
|
|
9
|
+
Button: ChartControlButton,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default ChartControl;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Sector } from "recharts";
|
|
4
|
+
import type { Props } from "recharts/types/shape/Sector";
|
|
5
|
+
|
|
6
|
+
export default function ChartActivePieSector({
|
|
7
|
+
cx,
|
|
8
|
+
cy,
|
|
9
|
+
innerRadius,
|
|
10
|
+
outerRadius,
|
|
11
|
+
startAngle,
|
|
12
|
+
endAngle,
|
|
13
|
+
fill,
|
|
14
|
+
}: Props) {
|
|
15
|
+
return (
|
|
16
|
+
<Sector
|
|
17
|
+
cx={cx}
|
|
18
|
+
cy={cy}
|
|
19
|
+
innerRadius={innerRadius}
|
|
20
|
+
outerRadius={
|
|
21
|
+
typeof outerRadius === "number" ? outerRadius + 2 : outerRadius
|
|
22
|
+
}
|
|
23
|
+
startAngle={startAngle}
|
|
24
|
+
endAngle={endAngle}
|
|
25
|
+
fill={fill}
|
|
26
|
+
cornerRadius={0}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import { lengthFormat } from "@uniai-fe/util-functions";
|
|
5
|
+
import StyledArcGauge from "../../styles/styled/arc-gauge";
|
|
6
|
+
import ArcGaugeTrack from "../../svg/gauge-track.min.svg";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 차트 템플릿; 반원 게이지
|
|
10
|
+
* - 체중균일도 차트 등에 사용
|
|
11
|
+
*/
|
|
12
|
+
export default function ChartArcGauge({
|
|
13
|
+
className,
|
|
14
|
+
value,
|
|
15
|
+
unit,
|
|
16
|
+
}: {
|
|
17
|
+
className?: string;
|
|
18
|
+
value: number | "";
|
|
19
|
+
unit: string;
|
|
20
|
+
}) {
|
|
21
|
+
const v = useMemo((): string => {
|
|
22
|
+
if (typeof value !== "number") return "-";
|
|
23
|
+
return lengthFormat(value, 0);
|
|
24
|
+
}, [value]);
|
|
25
|
+
|
|
26
|
+
const level = useMemo((): number => {
|
|
27
|
+
if (typeof value !== "number") return 0;
|
|
28
|
+
|
|
29
|
+
if (value <= 25) return 1;
|
|
30
|
+
else if (25 < value && value <= 50) return 2;
|
|
31
|
+
else if (50 < value && value <= 75) return 3;
|
|
32
|
+
else if (75 < value) return 4;
|
|
33
|
+
return 0;
|
|
34
|
+
}, [value]);
|
|
35
|
+
|
|
36
|
+
const degree = useMemo((): number => {
|
|
37
|
+
if (typeof value !== "number") return 0;
|
|
38
|
+
|
|
39
|
+
if (value <= 0) return 0;
|
|
40
|
+
else if (value > 100) return 180;
|
|
41
|
+
return ((value * Math.PI) / 180) * 100;
|
|
42
|
+
}, [value]);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<StyledArcGauge.container className={className}>
|
|
46
|
+
<StyledArcGauge.track>
|
|
47
|
+
<ArcGaugeTrack />
|
|
48
|
+
{value !== "" && (
|
|
49
|
+
<StyledArcGauge.point $level={level} $degree={degree} />
|
|
50
|
+
)}
|
|
51
|
+
</StyledArcGauge.track>
|
|
52
|
+
<StyledArcGauge.value>
|
|
53
|
+
<strong>{v}</strong>
|
|
54
|
+
<span>{unit}</span>
|
|
55
|
+
</StyledArcGauge.value>
|
|
56
|
+
</StyledArcGauge.container>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
|
|
5
|
+
import ChartDoughnut from "./Doughnut";
|
|
6
|
+
import getChartLevel from "../../utils/getLevel";
|
|
7
|
+
|
|
8
|
+
import type { ChartDoughnutProps } from "../../types";
|
|
9
|
+
import StyledArcMeter from "../../styles/styled/arc-meter";
|
|
10
|
+
import { lengthFormat } from "@uniai-fe/util-functions";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 차트 템플릿; 반원 미터
|
|
14
|
+
* - AI 건강지수 차트 등에 사용
|
|
15
|
+
* @param {ChartDoughnutProps} props
|
|
16
|
+
* @param {string} [props.className]
|
|
17
|
+
* @param {object} props.data 데이터
|
|
18
|
+
* @param {string} props.data.name 데이터 이름
|
|
19
|
+
* @param {number | ""} props.data.value 값 (스코어)
|
|
20
|
+
* @param {string} [props.data.unit] 데이터 단위
|
|
21
|
+
* @param {number} [props.max] 최대값 (기본값 100)
|
|
22
|
+
* @param {ChartDoughnutProps} [props.doughnutOptions] <ChartDoughnut /> Props 옵션
|
|
23
|
+
*/
|
|
24
|
+
export default function ChartArcMeter({
|
|
25
|
+
className,
|
|
26
|
+
data: d,
|
|
27
|
+
max = 100,
|
|
28
|
+
doughnutOptions,
|
|
29
|
+
}: {
|
|
30
|
+
className?: string;
|
|
31
|
+
data: { name: string; value: number | ""; unit?: string };
|
|
32
|
+
max?: number;
|
|
33
|
+
doughnutOptions?: ChartDoughnutProps;
|
|
34
|
+
}) {
|
|
35
|
+
const START_ANGLE = 180;
|
|
36
|
+
const WIDTH = 180;
|
|
37
|
+
const THICKNESS = 14;
|
|
38
|
+
|
|
39
|
+
const POINT_BORDER = 5;
|
|
40
|
+
const POINT_RADIUS = THICKNESS / 2 + POINT_BORDER / 2;
|
|
41
|
+
|
|
42
|
+
const point = useMemo((): { x: number; y: number } => {
|
|
43
|
+
const pos = { x: 0, y: 0 };
|
|
44
|
+
if (typeof d.value !== "number") return pos;
|
|
45
|
+
|
|
46
|
+
// -90 ~ 90
|
|
47
|
+
const startDegree = -90;
|
|
48
|
+
const rangeDegree = 180;
|
|
49
|
+
const angle = (d.value / max) * rangeDegree + startDegree;
|
|
50
|
+
const distance = WIDTH / 2 - THICKNESS / 2;
|
|
51
|
+
const radian = (angle * Math.PI) / rangeDegree;
|
|
52
|
+
|
|
53
|
+
pos.x = WIDTH / 2 + distance * Math.sin(radian);
|
|
54
|
+
pos.y = WIDTH / 2 - distance * Math.cos(radian);
|
|
55
|
+
|
|
56
|
+
return pos;
|
|
57
|
+
}, [max, d.value]);
|
|
58
|
+
|
|
59
|
+
const color = useMemo((): string => {
|
|
60
|
+
const level = getChartLevel(d.value, max);
|
|
61
|
+
return `var(--chart-level-color-${level})`;
|
|
62
|
+
}, [max, d.value]);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<StyledArcMeter.container className={className} $height={WIDTH / 2}>
|
|
66
|
+
<ChartDoughnut
|
|
67
|
+
width={WIDTH}
|
|
68
|
+
height={WIDTH / 2}
|
|
69
|
+
thickness={THICKNESS}
|
|
70
|
+
startAngle={START_ANGLE}
|
|
71
|
+
endAngle={START_ANGLE - 180}
|
|
72
|
+
pieOptions={{
|
|
73
|
+
cy: WIDTH / 2,
|
|
74
|
+
cornerRadius: THICKNESS / 2,
|
|
75
|
+
dataKey: "value",
|
|
76
|
+
}}
|
|
77
|
+
chartData={[
|
|
78
|
+
{
|
|
79
|
+
name: d.name,
|
|
80
|
+
value: typeof d.value === "number" ? d.value : 0,
|
|
81
|
+
track: max,
|
|
82
|
+
fill: color,
|
|
83
|
+
unit: d.unit || "",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "",
|
|
87
|
+
value: typeof d.value === "number" ? max - d.value : max,
|
|
88
|
+
track: max,
|
|
89
|
+
fill: "transparent",
|
|
90
|
+
unit: "",
|
|
91
|
+
},
|
|
92
|
+
]}
|
|
93
|
+
{...doughnutOptions}
|
|
94
|
+
>
|
|
95
|
+
{typeof d.value === "number" && (
|
|
96
|
+
<circle
|
|
97
|
+
cx={point.x}
|
|
98
|
+
cy={point.y}
|
|
99
|
+
r={POINT_RADIUS}
|
|
100
|
+
fill="var(--color_0)"
|
|
101
|
+
stroke={color}
|
|
102
|
+
strokeWidth={POINT_BORDER}
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
105
|
+
</ChartDoughnut>
|
|
106
|
+
<StyledArcMeter.text $color={color}>
|
|
107
|
+
<strong>{lengthFormat(d.value)}</strong>
|
|
108
|
+
{d.unit && <span>{d.unit}</span>}
|
|
109
|
+
</StyledArcMeter.text>
|
|
110
|
+
</StyledArcMeter.container>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Area,
|
|
5
|
+
AreaChart,
|
|
6
|
+
CartesianGrid,
|
|
7
|
+
ResponsiveContainer,
|
|
8
|
+
Tooltip,
|
|
9
|
+
XAxis,
|
|
10
|
+
YAxis,
|
|
11
|
+
type XAxisProps,
|
|
12
|
+
type YAxisProps,
|
|
13
|
+
} from "recharts";
|
|
14
|
+
import type { ChartAreaDataLegendProps, ChartAreaProps } from "../../types";
|
|
15
|
+
import { useChartSize } from "@uniai-fe/react-hooks";
|
|
16
|
+
|
|
17
|
+
export default function ChartArea({
|
|
18
|
+
chartData,
|
|
19
|
+
legendData,
|
|
20
|
+
yAxisData,
|
|
21
|
+
chartLayoutOptions,
|
|
22
|
+
xAxisOptions,
|
|
23
|
+
yAxisOptions,
|
|
24
|
+
isHideGrid,
|
|
25
|
+
tooltipOptions,
|
|
26
|
+
containerOptions,
|
|
27
|
+
children: extraComponents,
|
|
28
|
+
}: ChartAreaProps) {
|
|
29
|
+
const { sizeOptions } = useChartSize();
|
|
30
|
+
|
|
31
|
+
const iaValidGradient = ({ areaGradient }: ChartAreaDataLegendProps) =>
|
|
32
|
+
areaGradient && areaGradient.selectorId && areaGradient.gradient.length > 1;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<ResponsiveContainer
|
|
36
|
+
width="100%"
|
|
37
|
+
height="100%"
|
|
38
|
+
{...sizeOptions(containerOptions)}
|
|
39
|
+
>
|
|
40
|
+
<AreaChart
|
|
41
|
+
// margin={{ top: 0, left: 0, right: 0, bottom: 0 }}
|
|
42
|
+
{...sizeOptions({
|
|
43
|
+
margin: { top: 0, left: 0, right: 0, bottom: 0 },
|
|
44
|
+
...chartLayoutOptions,
|
|
45
|
+
})}
|
|
46
|
+
data={chartData}
|
|
47
|
+
>
|
|
48
|
+
{!isHideGrid && yAxisData?.[0] && (
|
|
49
|
+
<CartesianGrid vertical={false} yAxisId={yAxisData[0].axisCategory} />
|
|
50
|
+
)}
|
|
51
|
+
{tooltipOptions && <Tooltip {...tooltipOptions} />}
|
|
52
|
+
<XAxis
|
|
53
|
+
axisLine={false}
|
|
54
|
+
{...sizeOptions<XAxisProps>({
|
|
55
|
+
padding: { left: 30, right: 30 },
|
|
56
|
+
tickSize: 0,
|
|
57
|
+
tickMargin: 10,
|
|
58
|
+
...xAxisOptions,
|
|
59
|
+
})}
|
|
60
|
+
/>
|
|
61
|
+
{yAxisData.map(({ key, axisCategory }) => (
|
|
62
|
+
<YAxis
|
|
63
|
+
key={key}
|
|
64
|
+
yAxisId={axisCategory}
|
|
65
|
+
axisLine={false}
|
|
66
|
+
hide={true}
|
|
67
|
+
{...sizeOptions<YAxisProps>({ tickSize: 0, ...yAxisOptions })}
|
|
68
|
+
/>
|
|
69
|
+
))}
|
|
70
|
+
{legendData.filter(iaValidGradient).length > 0 ? (
|
|
71
|
+
<defs>
|
|
72
|
+
{legendData.filter(iaValidGradient).map(
|
|
73
|
+
({ areaGradient }) =>
|
|
74
|
+
areaGradient && (
|
|
75
|
+
<linearGradient
|
|
76
|
+
key={areaGradient.selectorId}
|
|
77
|
+
id={areaGradient.selectorId}
|
|
78
|
+
x1="0"
|
|
79
|
+
y1="0"
|
|
80
|
+
x2="0"
|
|
81
|
+
y2="1"
|
|
82
|
+
>
|
|
83
|
+
{areaGradient.gradient.map(({ key, offset, color }) => (
|
|
84
|
+
<stop key={key} offset={offset} stopColor={color} />
|
|
85
|
+
))}
|
|
86
|
+
</linearGradient>
|
|
87
|
+
),
|
|
88
|
+
)}
|
|
89
|
+
</defs>
|
|
90
|
+
) : null}
|
|
91
|
+
{legendData
|
|
92
|
+
.filter(({ active }) => active)
|
|
93
|
+
.map(
|
|
94
|
+
({
|
|
95
|
+
key,
|
|
96
|
+
code,
|
|
97
|
+
axisCategory,
|
|
98
|
+
name,
|
|
99
|
+
unit,
|
|
100
|
+
color,
|
|
101
|
+
areaColor,
|
|
102
|
+
areaGradient,
|
|
103
|
+
highlight,
|
|
104
|
+
}) => (
|
|
105
|
+
<Area
|
|
106
|
+
key={key}
|
|
107
|
+
type="monotone"
|
|
108
|
+
dataKey={code}
|
|
109
|
+
yAxisId={axisCategory}
|
|
110
|
+
name={name}
|
|
111
|
+
unit={unit}
|
|
112
|
+
stroke={color}
|
|
113
|
+
strokeWidth="var(--chart-data-line-width)"
|
|
114
|
+
fill={
|
|
115
|
+
areaGradient?.selectorId
|
|
116
|
+
? `url(#${areaGradient.selectorId})`
|
|
117
|
+
: areaColor || undefined
|
|
118
|
+
}
|
|
119
|
+
{...(typeof highlight === "boolean" && highlight === false
|
|
120
|
+
? { strokeOpacity: 0.3 }
|
|
121
|
+
: {})}
|
|
122
|
+
dot={false}
|
|
123
|
+
isAnimationActive={false}
|
|
124
|
+
/>
|
|
125
|
+
),
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
{extraComponents}
|
|
129
|
+
</AreaChart>
|
|
130
|
+
</ResponsiveContainer>
|
|
131
|
+
);
|
|
132
|
+
}
|