From c6fca137839595e025a8b863ab5ab2a3c7ee8ba8 Mon Sep 17 00:00:00 2001
From: Janghun Lee <74360958+bh2980@users.noreply.github.com>
Date: Mon, 27 May 2024 14:32:10 +0900
Subject: [PATCH] =?UTF-8?q?BarChart=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?=
=?UTF-8?q?=20(#36)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* ♻️ refactor(component): width, height 속성 필수로 설정 및 각 변에 축을 그리도록 설정
* 💄 style(css): label을 height 중앙에 오도록 수정, textAlign 삭제 및 tickAlign 추가
* 📝 docs(story): bandAxis Story 수정
* 🚚 chore(component): 주석 추가
* 🚚 chore(component): barChart에서 Axis 제거 및 animate 제거
* ♻️ refactor(component): bar 제거 및 방향 추가
* 💄 style(constant): orient를 BarChart와 동일하게 수정 및 불필요한 속성 css로 이동
* ✨ feat(component): label 및 showLabel props 추가
* 💄 style(css): labelOffset 및 style 조정
* 🚚 chore(type): barProps 삭제
* 📝 docs(story): padding Story 추가
---
.../charts/BandAxis/BandAxis.stories.tsx | 34 ++----
.../charts/BandAxis/BandAxis.styles.ts | 22 +++-
src/components/charts/BandAxis/BandAxis.tsx | 55 ++++++---
.../charts/BandAxis/BandAxis.types.ts | 4 +-
.../BandAxis/__test__/BandAxis.test.tsx | 46 +++----
.../charts/BarChart/BarChart.stories.tsx | 27 +++-
.../charts/BarChart/BarChart.styles.ts | 17 ++-
src/components/charts/BarChart/BarChart.tsx | 115 ++++++++----------
.../charts/BarChart/BarChart.types.ts | 17 +--
.../__test__/getAxisOrientConfig.test.ts | 28 -----
src/utils/getAxisOrientConfig.ts | 19 ---
11 files changed, 180 insertions(+), 204 deletions(-)
delete mode 100644 src/utils/__test__/getAxisOrientConfig.test.ts
delete mode 100644 src/utils/getAxisOrientConfig.ts
diff --git a/src/components/charts/BandAxis/BandAxis.stories.tsx b/src/components/charts/BandAxis/BandAxis.stories.tsx
index 9f12518..21d6961 100644
--- a/src/components/charts/BandAxis/BandAxis.stories.tsx
+++ b/src/components/charts/BandAxis/BandAxis.stories.tsx
@@ -29,38 +29,24 @@ const xScale = scaleBand()
.range([0, length]);
export const Default: Story = {
- render: () => (
-
- ),
+ render: () => ,
};
-export const Direction: Story = {
+export const Orient: Story = {
render: () => (
-
+
+
+
+
+
+
),
};
export const LabelHide: Story = {
- render: () => (
-
- ),
+ render: () => ,
};
export const LineHide: Story = {
- render: () => (
-
- ),
+ render: () => ,
};
diff --git a/src/components/charts/BandAxis/BandAxis.styles.ts b/src/components/charts/BandAxis/BandAxis.styles.ts
index aed8f3b..6d2e7c2 100644
--- a/src/components/charts/BandAxis/BandAxis.styles.ts
+++ b/src/components/charts/BandAxis/BandAxis.styles.ts
@@ -1,5 +1,25 @@
import { tv } from "@utils/customTV";
export const BandAxisVariants = tv({
- base: "stroke-black",
+ slots: {
+ root: "stroke-surface-on",
+ axisLine: "fill-none",
+ labelText: "stroke-none",
+ },
+ variants: {
+ lineHide: {
+ true: {
+ axisLine: "hidden",
+ },
+ },
+ labelHide: {
+ true: {
+ labelText: "hidden",
+ },
+ },
+ },
+ defaultVariants: {
+ lineHide: false,
+ labelHide: false,
+ },
});
diff --git a/src/components/charts/BandAxis/BandAxis.tsx b/src/components/charts/BandAxis/BandAxis.tsx
index 671d045..34f6aba 100644
--- a/src/components/charts/BandAxis/BandAxis.tsx
+++ b/src/components/charts/BandAxis/BandAxis.tsx
@@ -1,10 +1,13 @@
-import { getAxisOrientConfig } from "@utils/getAxisOrientConfig";
import { isEven } from "@utils/isEven";
import { BandAxisVariants } from "./BandAxis.styles";
import { BandAxisProps } from "./BandAxis.types";
+// TODO width, height가 없는 반응형 대응이 필요
+// TODO 조립형으로 하면 각 컴포넌트 별로 props 분리 및 label 회전, 포맷팅도 가능할 듯 -> 꼭 필요한가? 싶긴 함
const BandAxis = ({
- orient = "DOWN",
+ width,
+ height,
+ orient = "UP",
axisScale,
outerTickLength = 6,
innerTickLength = 6,
@@ -13,6 +16,8 @@ const BandAxis = ({
className,
...props
}: BandAxisProps) => {
+ const { root, axisLine, labelText } = BandAxisVariants();
+
const [startPoint, endPoint] = axisScale.range();
const tickCount = axisScale.domain().length;
const isVertical = orient === "LEFT" || orient === "RIGHT";
@@ -21,11 +26,23 @@ const BandAxis = ({
(endPoint - startPoint) / 2 -
axisScale.step() * (isEven(tickCount) ? tickCount / 2 - 0.5 : Math.floor(tickCount / 2));
- const [textdX, textdY, path] = getAxisOrientConfig({ orient, startPoint, endPoint, outerTickLength });
+ const pathConfig: { [key: string]: string } = {
+ UP: `M${startPoint + 0.5},${outerTickLength}V0H${endPoint - 0.5}V${outerTickLength}`,
+ DOWN: `M${startPoint + 0.5},${height - outerTickLength}V${height}H${endPoint - 0.5}V${height - outerTickLength}`,
+ RIGHT: `M${outerTickLength},${startPoint + 0.5}H0V${endPoint - 0.5}H${outerTickLength}`,
+ LEFT: `M${width - outerTickLength},${startPoint + 0.5}H${width}V${endPoint - 0.5}H${width - outerTickLength}`,
+ };
+
+ const tickAlign = {
+ UP: `translate(0, 0)`,
+ DOWN: `translate(0, ${height - innerTickLength})`,
+ RIGHT: `translate(0, 0)`,
+ LEFT: `translate(${width - innerTickLength}, 0)`,
+ };
return (
-
- {!lineHide && }
+
+
);
};
diff --git a/src/components/charts/BandAxis/BandAxis.types.ts b/src/components/charts/BandAxis/BandAxis.types.ts
index c0ca66b..aa76585 100644
--- a/src/components/charts/BandAxis/BandAxis.types.ts
+++ b/src/components/charts/BandAxis/BandAxis.types.ts
@@ -7,6 +7,8 @@ export type AxisOrient = "UP" | "DOWN" | "RIGHT" | "LEFT";
type BandAxisBaseProps = {
axisScale: ScaleBand;
+ width: number;
+ height: number;
outerTickLength?: number;
innerTickLength?: number;
orient?: AxisOrient;
@@ -14,6 +16,6 @@ type BandAxisBaseProps = {
lineHide?: boolean;
};
-export type BandAxisProps = Omit, "textAnchor"> &
+export type BandAxisProps = Omit, "width" | "height" | "textAnchor"> &
VariantProps &
BandAxisBaseProps;
diff --git a/src/components/charts/BandAxis/__test__/BandAxis.test.tsx b/src/components/charts/BandAxis/__test__/BandAxis.test.tsx
index d97011c..06d367f 100644
--- a/src/components/charts/BandAxis/__test__/BandAxis.test.tsx
+++ b/src/components/charts/BandAxis/__test__/BandAxis.test.tsx
@@ -21,60 +21,46 @@ describe("BandAxis", () => {
};
it("에러 없이 렌더링", () => {
- render();
+ render();
});
it("data에 맞는 tick 개수", () => {
- const { container } = render();
- const ticks = container.querySelectorAll("g > g");
+ const { container } = render();
+ const ticks = container.querySelectorAll("svg > g");
expect(ticks.length).toBe(4);
});
- it("lineHide일 때 path, line 태그 없음", () => {
- const { container } = render();
+ it("lineHide일 때 path, line 태그 hidden", () => {
+ const { container } = render();
const path = container.querySelector("path");
const line = container.querySelector("line");
- expect(path).toBe(null);
- expect(line).toBe(null);
+ expect(path).toHaveClass("hidden");
+ expect(line).toHaveClass("hidden");
});
it("labelHide일 때 text 태그 없음", () => {
- const { container } = render();
- const texts = container.querySelectorAll("text");
- expect(texts.length).toBe(0);
- });
-
- it("orient가 UP | DOWN 일 경우, transform 속성", () => {
- const regex = /^translate\([\d.]+,\s*0\)$/;
- ["UP", "DOWN"].forEach((orient) => {
- const { container } = render();
- const tick = container.querySelector("g > g");
- expect(tick).toHaveAttribute("transform", expect.stringMatching(regex));
- });
+ const { container } = render();
+ const text = container.querySelector("text");
+ expect(text).toHaveClass("hidden");
});
it("orient가 UP | DOWN 일 경우, line의 y2 존재 및 x2 속성 없음", () => {
["UP", "DOWN"].forEach((orient) => {
- const { container } = render();
+ const { container } = render(
+ ,
+ );
const line = container.querySelector("line");
expect(line).toHaveAttribute("y2");
expect(line).not.toHaveAttribute("x2");
});
});
- it("orient가 LEFT | RIGHT 일 경우, transform 속성", () => {
- const regex = /^translate\(0,\s*[\d.]+\)$/;
- ["LEFT", "RIGHT"].forEach((orient) => {
- const { container } = render();
- const tick = container.querySelector("g > g");
- expect(tick).toHaveAttribute("transform", expect.stringMatching(regex));
- });
- });
-
it("orient가 LEFT | RIGHT 일 경우, line의 x2 존재 및 y2 속성 없음", () => {
["LEFT", "RIGHT"].forEach((orient) => {
- const { container } = render();
+ const { container } = render(
+ ,
+ );
const line = container.querySelector("line");
expect(line).toHaveAttribute("x2");
expect(line).not.toHaveAttribute("y2");
diff --git a/src/components/charts/BarChart/BarChart.stories.tsx b/src/components/charts/BarChart/BarChart.stories.tsx
index d141c01..e3abd07 100644
--- a/src/components/charts/BarChart/BarChart.stories.tsx
+++ b/src/components/charts/BarChart/BarChart.stories.tsx
@@ -22,5 +22,30 @@ const data = [
];
export const Default: Story = {
- render: () => ,
+ render: () => ,
+};
+
+export const Orient: Story = {
+ render: () => (
+
+
+
+
+
+
+ ),
+};
+
+export const ShowLabel: Story = {
+ render: () => ,
+};
+
+export const Padding: Story = {
+ render: () => (
+
+
+
+
+
+ ),
};
diff --git a/src/components/charts/BarChart/BarChart.styles.ts b/src/components/charts/BarChart/BarChart.styles.ts
index f4ffd33..b3986c7 100644
--- a/src/components/charts/BarChart/BarChart.styles.ts
+++ b/src/components/charts/BarChart/BarChart.styles.ts
@@ -2,7 +2,20 @@ import { tv } from "@utils/customTV";
export const barChartVariants = tv({
slots: {
- bar: "stroke-secondary fill-secondary font-bold text-sm",
- xAxis: "text-sm fill-surface-on-variant",
+ bar: "",
+ labelText: "",
+ },
+ variants: {
+ value: {
+ true: { bar: "fill-secondary" },
+ false: { bar: "stroke-secondary fill-none" },
+ },
+ showLabel: {
+ false: { labelText: "hidden" },
+ },
+ },
+ defaultVariants: {
+ value: true,
+ showLabel: false,
},
});
diff --git a/src/components/charts/BarChart/BarChart.tsx b/src/components/charts/BarChart/BarChart.tsx
index b79ba1c..d651039 100644
--- a/src/components/charts/BarChart/BarChart.tsx
+++ b/src/components/charts/BarChart/BarChart.tsx
@@ -1,69 +1,33 @@
import { max, scaleBand, scaleLinear } from "d3";
-import BandAxis from "@charts/BandAxis";
import { barChartVariants } from "./BarChart.styles";
-import type { BarChartProps, BarProps } from "./BarChart.types";
+import type { BarChartProps } from "./BarChart.types";
-//TODO useBar를 통한 속성 제어가 필요할지도?
-//useBar가 있으면 nullBarHeight를 별도로 제공해줄 필요가 없을수도?
-const Bar = ({
- xScale,
- yScale,
+// TODO 반응형 대응
+// TODO 애니메이션 설정
+// TODO label을 잘리지 않게 하기 위한 labelOffset 설정, label transform, formatting 등 커스텀 설정 필요
+const BarChart = ({
+ width,
+ height,
data,
- nullBarHeight = 0,
- animationDuration = "0.3s",
- rx,
- labelPostfix = "",
- ...props
-}: BarProps) => {
- const rectWidth = xScale.bandwidth();
- const rectHeight = yScale(0) - yScale(data.value || nullBarHeight);
- const rectX = xScale(data.label.toString())!;
- const rectY = yScale(data.value || nullBarHeight);
+ orient = "UP",
+ padding = 0.5,
+ showLabel,
+ labelOffset = 24,
+}: BarChartProps) => {
+ const isVertical = orient === "UP" || orient === "DOWN";
- const labelOffset = 12;
+ const labelRange = isVertical ? width : height;
+ const valueRange = isVertical ? height - labelOffset : width - labelOffset;
- return (
-
-
-
-
-
-
- {data.value ? `${data.value}${labelPostfix}` : "?"}
-
-
-
- );
-};
-
-// TODO useBar와 useAxis를 이용하면 줄일 수 있을 지도?
-const BarChart = ({ width, height, data, padding = 0.5 }: BarChartProps) => {
- const margin = { x: 0, y: 32 };
-
- const xScale = scaleBand()
+ const labelScale = scaleBand()
.domain(data.map((d) => d.label.toString()))
- .range([margin.x, width - margin.x])
+ .range([0, labelRange])
.padding(padding);
- const yScale = scaleLinear()
+ const valueScale = scaleLinear()
.domain([0, max(data, (d) => (d.value ? d.value : 0))!])
.nice()
- .range([height - margin.y, margin.y]);
+ .range([0, valueRange]);
const nullBarHeight =
data.reduce((acc, cur) => {
@@ -71,25 +35,42 @@ const BarChart = ({ width, height, data, padding = 0.5 }: BarChartProps) => {
return acc;
}, 0) / data.length;
- const { bar, xAxis } = barChartVariants();
+ const { bar, labelText } = barChartVariants();
return (
);
};
diff --git a/src/components/charts/BarChart/BarChart.types.ts b/src/components/charts/BarChart/BarChart.types.ts
index 951f783..dfa163c 100644
--- a/src/components/charts/BarChart/BarChart.types.ts
+++ b/src/components/charts/BarChart/BarChart.types.ts
@@ -1,22 +1,13 @@
-import type { ScaleBand, ScaleLinear } from "d3";
-import type { PropsWithChildren } from "react";
-import type { PolymorphicPropsType } from "@customTypes/polymorphicType";
+import type { AxisOrient } from "@charts/BandAxis";
type BarChartDataType = { label: number; value: number | null };
export type BarChartProps = {
+ orient?: AxisOrient;
width: number;
height: number;
data: BarChartDataType[];
padding?: number;
+ showLabel?: boolean;
+ labelOffset?: number;
};
-
-export type BarProps = PolymorphicPropsType<"g"> &
- PropsWithChildren & {
- xScale: ScaleBand;
- yScale: ScaleLinear;
- data: BarChartDataType;
- nullBarHeight?: number;
- animationDuration?: string;
- labelPostfix?: string;
- };
diff --git a/src/utils/__test__/getAxisOrientConfig.test.ts b/src/utils/__test__/getAxisOrientConfig.test.ts
deleted file mode 100644
index 4fdc527..0000000
--- a/src/utils/__test__/getAxisOrientConfig.test.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { describe, expect, test } from "vitest";
-import { getAxisOrientConfig } from "@utils/getAxisOrientConfig";
-
-describe("getAxisOrientConfig", () => {
- const startPoint = 0;
- const endPoint = 100;
- const outerTickLength = 10;
-
- test("returns correct config for UP orientation", () => {
- const result = getAxisOrientConfig({ orient: "UP", startPoint, endPoint, outerTickLength });
- expect(result).toEqual([0, -6, "M0.5,0V10H99.5V0"]);
- });
-
- test("returns correct config for DOWN orientation", () => {
- const result = getAxisOrientConfig({ orient: "DOWN", startPoint, endPoint, outerTickLength });
- expect(result).toEqual([0, 24, "M0.5,10V0H99.5V10"]);
- });
-
- test("returns correct config for LEFT orientation", () => {
- const result = getAxisOrientConfig({ orient: "LEFT", startPoint, endPoint, outerTickLength });
- expect(result).toEqual([-24, 6, "M0,0.5H10V99.5H0"]);
- });
-
- test("returns correct config for RIGHT orientation", () => {
- const result = getAxisOrientConfig({ orient: "RIGHT", startPoint, endPoint, outerTickLength });
- expect(result).toEqual([28, 6, "M10,0.5H0V99.5H10"]);
- });
-});
diff --git a/src/utils/getAxisOrientConfig.ts b/src/utils/getAxisOrientConfig.ts
deleted file mode 100644
index d299e79..0000000
--- a/src/utils/getAxisOrientConfig.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { AxisOrient } from "@charts/BandAxis/BandAxis.types";
-
-type getAxisOrientConfigParams = {
- orient: AxisOrient;
- startPoint: number;
- endPoint: number;
- outerTickLength: number;
-};
-
-export const getAxisOrientConfig = ({ orient, startPoint, endPoint, outerTickLength }: getAxisOrientConfigParams) => {
- const pathConfig: { [key: string]: [number, number, string] } = {
- UP: [0, -6, `M${startPoint + 0.5},0V${outerTickLength}H${endPoint - 0.5}V0`],
- DOWN: [0, 24, `M${startPoint + 0.5},${outerTickLength}V0H${endPoint - 0.5}V${outerTickLength}`],
- LEFT: [-24, 6, `M0,${startPoint + 0.5}H${outerTickLength}V${endPoint - 0.5}H0`],
- RIGHT: [28, 6, `M${outerTickLength},${startPoint + 0.5}H0V${endPoint - 0.5}H${outerTickLength}`],
- };
-
- return pathConfig[orient];
-};