Skip to content

Commit

Permalink
BandAxis 컴포넌트 추가 (#32)
Browse files Browse the repository at this point in the history
* 🚚 chore(workflow): create issue branch가 생성하는 브랜치 prefix 변경

* 🚚 chore(workflow): component plop에 vitest import 문 추가 및 jest 주석 삭제

* ♻️ refactor(component): bandAxis 분리 및 방향 추가

* ✨ feat(css): labelHide, lineHide 추가 및 관련 스토리 추가

* ✨ feat(util): getAxisOrientConfig 함수 추가

* ♻️ refactor(component): getAxisOrientConfig 함수 적용 및 direction을 orient로 props 이름 변경

* ♻️ refactor(component): barChart에 기존 BandAxis 제거 및 BandAxis 컴포넌트 적용

* 🚚 chore(type): axisOrient 타입 export

* ✅ test(util): getAxisOrientConfig 테스트 코드 추가

* 💄 style(css): bandAxisVariants 삭제

* ✅ test(component): bandAxis 테스트 코드 추가

* 💄 style(css): bandAxisVariants에 base만 있도록 수정

* ✅ test(component): bandAxis 테스트 코드 추가

* 🐛 fix(type): lineHide type 빠져있는 문제 수정

ISSUES CLOSED: #31

* 🐛 fix(component): lineHide 시 path와 line이 렌더링되는 문제 수정
  • Loading branch information
bh2980 committed May 26, 2024
1 parent 7570e48 commit 38dce2a
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 44 deletions.
8 changes: 4 additions & 4 deletions .github/issue-branch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ branchName: "chistockUI-#${issue.number}"
autoCloseIssue: true
branches:
- label: 🐛 Bug
prefix: bugfix/
prefix: fix/
- label: 🩹 Simple Fix
prefix: bugfix/
prefix: fix/
- label: 🚨 HOTFIX
prefix: hotfix/
- label: 👷 CI
Expand All @@ -16,8 +16,8 @@ branches:
- label: ✨ Feature
prefix: feature/
- label: ♻️ Refactor
prefix: feature/
prefix: refactor/
- label: ✅ Test
prefix: feature/
prefix: test/
- label: 📝 Document
prefix: docs/
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { render, screen } from "@testing-library/react";
import {{upperCamel name}} from "../{{upperCamel name}}";
import { describe, expect, it } from "vitest";

describe("{{name}}", () => {
// eslint-disable-next-line jest/expect-expect
it("에러 없이 렌더링되어야 합니다.", () => {
render(<{{upperCamel name}} />);
});
Expand Down
66 changes: 66 additions & 0 deletions src/components/charts/BandAxis/BandAxis.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Meta, StoryObj } from "@storybook/react";
import { scaleBand } from "d3";
import BandAxis from "./BandAxis";

const meta = {
title: "charts/BandAxis",
component: BandAxis,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
} satisfies Meta<typeof BandAxis>;

export default meta;
type Story = StoryObj<typeof BandAxis>;

const data = [
{ label: 2019, value: 151.8 },
{ label: 2020, value: 242.2 },
{ label: 2021, value: 121.3 },
{ label: 2022, value: 200.7 },
{ label: 2023, value: null }, // 값이 아직 없는 경우
];

const length = 256;

const xScale = scaleBand()
.domain(data.map((d) => d.label.toString()))
.range([0, length]);

export const Default: Story = {
render: () => (
<svg width={256} height={256}>
<BandAxis axisScale={xScale} transform="translate(0, 128)" />
</svg>
),
};

export const Direction: Story = {
render: () => (
<svg width={512} height={256}>
<g transform="translate(128, 0)">
<BandAxis axisScale={xScale} orient="DOWN" transform="translate(0, 5)" />
<BandAxis axisScale={xScale} orient="UP" transform="translate(0, 245)" />
<BandAxis axisScale={xScale} orient="RIGHT" transform="translate(280, 0)" />
<BandAxis axisScale={xScale} orient="LEFT" transform="translate(-24, 0)" />
</g>
</svg>
),
};

export const LabelHide: Story = {
render: () => (
<svg width={256} height={256}>
<BandAxis axisScale={xScale} transform="translate(0, 128)" labelHide />
</svg>
),
};

export const LineHide: Story = {
render: () => (
<svg width={256} height={256}>
<BandAxis axisScale={xScale} transform="translate(0, 128)" lineHide />
</svg>
),
};
5 changes: 5 additions & 0 deletions src/components/charts/BandAxis/BandAxis.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { tv } from "@utils/customTV";

export const BandAxisVariants = tv({
base: "stroke-black",
});
56 changes: 56 additions & 0 deletions src/components/charts/BandAxis/BandAxis.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { getAxisOrientConfig } from "@utils/getAxisOrientConfig";
import { isEven } from "@utils/isEven";
import { BandAxisVariants } from "./BandAxis.styles";
import { BandAxisProps } from "./BandAxis.types";

const BandAxis = ({
orient = "DOWN",
axisScale,
outerTickLength = 6,
innerTickLength = 6,
lineHide,
labelHide,
className,
...props
}: BandAxisProps) => {
const [startPoint, endPoint] = axisScale.range();
const tickCount = axisScale.domain().length;
const isVertical = orient === "LEFT" || orient === "RIGHT";

const tickStartPoint =
(endPoint - startPoint) / 2 -
axisScale.step() * (isEven(tickCount) ? tickCount / 2 - 0.5 : Math.floor(tickCount / 2));

const [textdX, textdY, path] = getAxisOrientConfig({ orient, startPoint, endPoint, outerTickLength });

return (
<g className={BandAxisVariants({ className })} textAnchor="middle" {...props}>
{!lineHide && <path fill="none" d={path} />}
{axisScale.domain().map((label, i) => (
<g
key={`tick-${i}`}
transform={
isVertical
? `translate(0, ${tickStartPoint + axisScale.step() * i})`
: `translate(${tickStartPoint + axisScale.step() * i}, 0)`
}
>
{!lineHide && (
<line
x2={isVertical ? innerTickLength : undefined}
y2={isVertical ? undefined : innerTickLength}
fill="none"
/>
)}
{!labelHide && (
<text dx={textdX} dy={textdY} stroke="none">
{label}
</text>
)}
</g>
))}
</g>
);
};

export default BandAxis;
19 changes: 19 additions & 0 deletions src/components/charts/BandAxis/BandAxis.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { type ScaleBand } from "d3";
import type { VariantProps } from "tailwind-variants";
import type { ComponentPropsWithInnerRef } from "@customTypes/utilType";
import { BandAxisVariants } from "./BandAxis.styles";

export type AxisOrient = "UP" | "DOWN" | "RIGHT" | "LEFT";

type BandAxisBaseProps = {
axisScale: ScaleBand<string>;
outerTickLength?: number;
innerTickLength?: number;
orient?: AxisOrient;
labelHide?: boolean;
lineHide?: boolean;
};

export type BandAxisProps = Omit<ComponentPropsWithInnerRef<"g">, "textAnchor"> &
VariantProps<typeof BandAxisVariants> &
BandAxisBaseProps;
83 changes: 83 additions & 0 deletions src/components/charts/BandAxis/__test__/BandAxis.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* eslint-disable testing-library/no-node-access */

/* eslint-disable testing-library/no-container */
import { render } from "@testing-library/react";
import { scaleBand } from "d3-scale";
import { describe, expect, it } from "vitest";
import BandAxis from "../BandAxis";
import type { AxisOrient } from "../BandAxis.types";

const createScale = () => scaleBand().domain(["A", "B", "C", "D"]).range([0, 100]);

describe("BandAxis", () => {
const defaultProps = {
axisScale: createScale(),
orient: "DOWN" as AxisOrient,
outerTickLength: 6,
innerTickLength: 6,
lineHide: false,
labelHide: false,
className: "",
};

it("에러 없이 렌더링", () => {
render(<BandAxis {...defaultProps} />);
});

it("data에 맞는 tick 개수", () => {
const { container } = render(<BandAxis {...defaultProps} />);
const ticks = container.querySelectorAll("g > g");
expect(ticks.length).toBe(4);
});

it("lineHide일 때 path, line 태그 없음", () => {
const { container } = render(<BandAxis {...defaultProps} lineHide />);
const path = container.querySelector("path");
const line = container.querySelector("line");

expect(path).toBe(null);
expect(line).toBe(null);
});

it("labelHide일 때 text 태그 없음", () => {
const { container } = render(<BandAxis {...defaultProps} labelHide />);
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(<BandAxis {...defaultProps} orient={orient as AxisOrient} />);
const tick = container.querySelector("g > g");
expect(tick).toHaveAttribute("transform", expect.stringMatching(regex));
});
});

it("orient가 UP | DOWN 일 경우, line의 y2 존재 및 x2 속성 없음", () => {
["UP", "DOWN"].forEach((orient) => {
const { container } = render(<BandAxis {...defaultProps} orient={orient as AxisOrient} />);
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(<BandAxis {...defaultProps} orient={orient as AxisOrient} />);
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(<BandAxis {...defaultProps} orient={orient as AxisOrient} />);
const line = container.querySelector("line");
expect(line).toHaveAttribute("x2");
expect(line).not.toHaveAttribute("y2");
});
});
});
9 changes: 9 additions & 0 deletions src/components/charts/BandAxis/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import BandAxis from "./BandAxis";
import { BandAxisVariants } from "./BandAxis.styles";
import type { AxisOrient, BandAxisProps } from "./BandAxis.types";

export type { BandAxisProps, AxisOrient };

export { BandAxisVariants };

export default BandAxis;
35 changes: 3 additions & 32 deletions src/components/charts/BarChart/BarChart.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,7 @@
import { max, scaleBand, scaleLinear } from "d3";
import { isEven } from "@utils/isEven";
import BandAxis from "@charts/BandAxis";
import { barChartVariants } from "./BarChart.styles";
import { BandAxisProps, BarChartProps, BarProps } from "./BarChart.types";

//TODO 추후 BandAxis를 별도의 컴포넌트로 분리
//useAxis가 필요할지도
const BandAxis = ({ xScale, outerTickLength = 6, innerTickLength = 6, ...props }: BandAxisProps) => {
const [startPoint, endPoint] = xScale.range();
const tickCount = xScale.domain().length;

const tickStartPoint =
(endPoint - startPoint) / 2 - xScale.step() * (isEven(tickCount) ? tickCount / 2 - 0.5 : Math.floor(tickCount / 2));

return (
<g {...props}>
<path fill="none" d={`M${startPoint},${outerTickLength}V0H${endPoint}V${outerTickLength}`}></path>
{xScale.domain().map((label, i) => (
<g key={`tick-${i}`} transform={`translate(${tickStartPoint + xScale.step() * i}, 0)`}>
<line y2={innerTickLength} fill="none" />
<text y="24" stroke="none">
{label}
</text>
</g>
))}
</g>
);
};
import type { BarChartProps, BarProps } from "./BarChart.types";

//TODO useBar를 통한 속성 제어가 필요할지도?
//useBar가 있으면 nullBarHeight를 별도로 제공해줄 필요가 없을수도?
Expand Down Expand Up @@ -113,12 +89,7 @@ const BarChart = ({ width, height, data, padding = 0.5 }: BarChartProps) => {
/>
);
})}
<BandAxis
xScale={xScale}
textAnchor="middle"
transform={`translate(0, ${height - margin.y})`}
className={xAxis()}
/>
<BandAxis axisScale={xScale} transform={`translate(0, ${height - margin.y})`} className={xAxis()} lineHide />
</svg>
);
};
Expand Down
8 changes: 1 addition & 7 deletions src/components/charts/BarChart/BarChart.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ScaleBand, ScaleLinear } from "d3";
import type { PropsWithChildren } from "react";
import type { PolymorphicPropsType, PolymorphicPropsWithInnerRefType } from "@customTypes/polymorphicType";
import type { PolymorphicPropsType } from "@customTypes/polymorphicType";

type BarChartDataType = { label: number; value: number | null };

Expand All @@ -11,12 +11,6 @@ export type BarChartProps = {
padding?: number;
};

export type BandAxisProps = PolymorphicPropsWithInnerRefType<"g"> & {
xScale: ScaleBand<string>;
outerTickLength?: number;
innerTickLength?: number;
};

export type BarProps = PolymorphicPropsType<"g"> &
PropsWithChildren & {
xScale: ScaleBand<string>;
Expand Down
28 changes: 28 additions & 0 deletions src/utils/__test__/getAxisOrientConfig.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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"]);
});
});
19 changes: 19 additions & 0 deletions src/utils/getAxisOrientConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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];
};

0 comments on commit 38dce2a

Please sign in to comment.