Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ed120b8
chore: 도넛 차트 오타 삭제
lee0jae330 Feb 15, 2026
61b8e01
fix: line chart 그리기 애니메이션이 적용되지 않는 문제 해결
lee0jae330 Feb 15, 2026
e418293
refactor: lineChart x축, y축, x, y 가이드라인 chart 도메인으로 이동
lee0jae330 Feb 15, 2026
820138f
feat: x좌표 계산 유틸 함수 chart 도메인으로 이동
lee0jae330 Feb 15, 2026
15eaf1b
feat: y최댓값 계산 유틸 함수 추가
lee0jae330 Feb 15, 2026
9228e10
chore: line chart series 네이밍 구체적으로 변경
lee0jae330 Feb 15, 2026
9374e20
feat: barline chart data type 정의
lee0jae330 Feb 15, 2026
b345c6b
feat: line chart gradient 중 background에 조건부 렌더링 추가
lee0jae330 Feb 15, 2026
b2f4bae
feat: bar line chart 관련 좌표 계산, 너비, 높이 설정에 대한 커스텀 훅 추가
lee0jae330 Feb 15, 2026
c181812
refactor: lineChart Dots에서 Dot 컴포너트 분리
lee0jae330 Feb 15, 2026
aecab5e
refactor: bar chart 각 막대 너비, 높이 관련 유틸 함수 분리
lee0jae330 Feb 15, 2026
ba26b39
feat: bar line chart 컴포넌트 추가
lee0jae330 Feb 15, 2026
db5b207
feat: dot, line 컴포넌트 classname props 추가
lee0jae330 Feb 16, 2026
b42ff3f
feat: bar line 관련 툴팁 및 bar 관련 위치 계산 훅 추가
lee0jae330 Feb 16, 2026
99c5ad0
feat: bar line chart 혼합형 차트 추가
lee0jae330 Feb 16, 2026
c2c5613
refactor: 차트 좌표 계산 함수 y data를 mainY가 아닌 subY로도 계산할 수 있도록 변경
lee0jae330 Feb 16, 2026
8d550d8
chore: bar line chart 주석 추가
lee0jae330 Feb 16, 2026
d5c740b
chore: bar line chart 툴팁 트리거 path에 pointer-event 추가
lee0jae330 Feb 16, 2026
a548f63
feat: bar line chart에 bar-line series 컴포넌트 추가
lee0jae330 Feb 16, 2026
b8932ce
chore: 불필요한 args 스토리에서 제거
lee0jae330 Feb 16, 2026
931d9b4
Merge branch 'feature/#194-fe-dashboard-sales-ui' into feature/#275-f…
lee0jae330 Feb 16, 2026
2de96e0
feat: y좌표가 null인 경우 방어 로직 추가
lee0jae330 Feb 16, 2026
8ef12a8
feat: 실시간 값 데이트 story 추가
lee0jae330 Feb 16, 2026
2244868
refactor: bar line chart mock data 분리
lee0jae330 Feb 16, 2026
bbcbace
feat: bar chart bar 너비 계산 시 x좌표 리스트를 넘기는 것이 아닌 x좌표 개수만 넘기도록 수정
lee0jae330 Feb 16, 2026
80455e3
feat: 툴팁 활성화 여부 props 추가 및 조건부 렌더링 추가
lee0jae330 Feb 16, 2026
9ccc854
chore: props명 변경 tooltipTriggerPathD -> interactionPathD
lee0jae330 Feb 16, 2026
dd6806e
feat: y좌표가 null일 때만 렌더링하지 않도록 변경
lee0jae330 Feb 16, 2026
6b93fe8
feat: data.amount가 null일 때 방어 로직 추가
lee0jae330 Feb 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/src/components/shared/bar-chart/Bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface BarProps {
isActive?: boolean; //포커스거나 강조해야 하는 상태인지
bgColor?: string;
barColorChangeOnHover?: boolean; // 바에 호버 시 색상 변화 줄지 말지
className?: string;
}

export const Bar = ({
Expand All @@ -42,6 +43,7 @@ export const Bar = ({
bgColor = BAR_CHART.DEFAULT_BAR_COLOR,
isActive = false,
barColorChangeOnHover = true,
className,
}: BarProps) => {
const pathRef = useRef<SVGPathElement>(null);
const barRef = useRef<SVGGElement>(null); // g 태그 조작(막대 위치 이동시 애니메이션)을 위한 ref
Expand Down Expand Up @@ -83,6 +85,7 @@ export const Bar = ({
style={{
transformOrigin: `${barMiddleX}px ${barTopY + height}px`,
}}
className={className}
>
{activeTooltip ? (
<Tooltip>
Expand Down
8 changes: 2 additions & 6 deletions frontend/src/components/shared/bar-chart/BarChart.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import {
XAxis,
XAxisLabel,
XGuideLine,
YGuideLine,
} from '@/components/shared/line-chart';
import { useBarChart } from '@/hooks/shared';
import { useBarChartId } from '@/hooks/shared';
import type { XAxisType } from '@/types/shared';
import type { AllBarChartSeries } from '@/types/shared';

import { XAxis, XAxisLabel, XGuideLine, YGuideLine } from '../chart';

import { BarSeries } from './BarSeries';
/**
* @description 막대 차트 컴포넌트 (자세한 사용법은 스토리북 문서 참고)
Expand Down
69 changes: 12 additions & 57 deletions frontend/src/components/shared/bar-chart/BarSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import type {
StackBarDatum,
} from '@/types/shared';
import type { AllBarChartSeries } from '@/types/shared';
import { checkIsStackBarChart } from '@/utils/shared';
import {
checkIsStackBarChart,
getBarHeight,
getBarWidth,
getLabelContentText,
} from '@/utils/shared';

import { Bar } from './Bar';
import { BarLabel } from './BarLabel';
Expand Down Expand Up @@ -44,70 +49,20 @@ export const BarSeries = ({
activeDataIndex,
barColorChangeOnHover,
}: BarSeriesProps) => {
const { XAXIS_Y_OFFSET, XAXIS_STROKE_WIDTH, BAR_RADIUS } = BAR_CHART; // X축이 있을 때 X축의 Y좌표 오프셋 값
const { BAR_RADIUS } = BAR_CHART; // X축이 있을 때 X축의 Y좌표 오프셋 값

// 스택바 그래프인지 일반 바 그래프인지 -> mainY의 값이 배열이면 스택바
const isStackBar = checkIsStackBarChart({ series });

// 바 전체 높이 계산 (바의 상단 y좌표 부터 x축 또는 svg 하단까지의 거리)
const getBarHeight = ({
y,
hasXAxis,
viewBoxHeight,
}: {
y: number;
hasXAxis: boolean;
viewBoxHeight: number;
}) => {
if (hasXAxis) {
// x축이 있을 때는 x축의 y위치 만큼을 빼고 축 높이의 0.5배 만큼 더 빼줘야 함
return viewBoxHeight - XAXIS_Y_OFFSET - y - XAXIS_STROKE_WIDTH / 2; // x축이 있을 때 바 높이는 y좌표 ~ x 축까지 거리
}
return viewBoxHeight - y; // x축이 없을 떄 바 높이는 y좌표 ~ svg 최하단 까지 거리
};

// 바 너비는 막대 간격의 50%로 설정 (막대 간격은 viewBoxWidth / x축의 지점 개수)
const getBarWidth = ({
viewBoxWidth,
xCoordinate,
}: {
viewBoxWidth: number;
xCoordinate: Coordinate[];
}) => {
return (viewBoxWidth / xCoordinate.length) * 0.5;
};
// 바 위에 표시될 라벨 내용 텍스트 생성
const getLabelContentText = ({
index,
series,
}: {
index: number;
series: AllBarChartSeries;
}) => {
if (isStackBar) {
// 스택바 그래프일 때는 mainY의 각 항목이 배열이므로 각 스택의 합계를 계산하여 라벨에 표시
const stackValues = series.data.mainY[index] as StackBarDatum;
const total = stackValues.reduce((sum, item) => {
if (typeof item.amount === 'number') {
return sum + item.amount;
}
return sum;
}, 0);
const unit = stackValues[0]?.unit || ''; // 단위는 첫 번째 항목의 단위를 사용
return `${total} ${unit}`;
} else {
// 일반 바 그래프일 때는 mainY의 단일 값을 라벨에 표시
const value = series.data.mainY[index] as BarChartDatum;
return `${value.amount} ${value.unit}`;
}
};

return (
<>
{coordinate.map(({ x, y }, index) => {
if (x !== null && y !== null) {
const barHeight = getBarHeight({ y, hasXAxis, viewBoxHeight });
const barWidth = getBarWidth({ viewBoxWidth, xCoordinate }); // 막대 너비는 막대 간격의 50%
const barWidth = getBarWidth({
viewBoxWidth,
xDataLength: xCoordinate.length,
}); // 막대 너비는 막대 간격의 50%
// 막대 그래프 툴팁에 넣을 내용
const tooltipContentText = tooltipContent
? tooltipContent(
Expand All @@ -126,7 +81,7 @@ export const BarSeries = ({
<BarLabel
x={x}
y={y}
label={getLabelContentText({ index, series })}
label={getLabelContentText({ isStackBar, index, series })}
textColor={BAR_CHART.DEFAULT_BAR_COLOR}
/>
)}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/shared/bar-chart/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { BarChart } from './BarChart';
export { Bar } from './Bar';
231 changes: 231 additions & 0 deletions frontend/src/components/shared/bar-line-chart/BarLineChart.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import { useState } from 'react';

import type { Meta, StoryObj } from '@storybook/react-vite';

import { Button, TooltipProvider } from '@/components/shared/shadcn-ui';
import {
BAR_LINE_MONTHLY_MOCK,
BAR_LINE_REALTIME_MOCK,
BAR_LINE_WEEKLY_MOCK,
} from '@/mocks/data';
import type { BarLineChartSeries } from '@/types/shared';

import { BarLineChart } from './BarLineChart';

const meta = {
title: 'components/shared/bar-line-chart/BarLineChart',
component: BarLineChart,
tags: ['autodocs'],
parameters: {
layout: 'centered',
},
argTypes: {},
} satisfies Meta<typeof BarLineChart>;

export default meta;

type Story = StoryObj<typeof meta>;

const RealtimeBarLineChart = (args: Story['args']) => {
const [barLineChartSeries, setBarLineChartSeries] =
useState<BarLineChartSeries>(args.barLineChartSeries as BarLineChartSeries);

const handleUpdateCurrentSeries = () => {
let currentIndex =
barLineChartSeries.data.mainY.filter((datum) => datum.amount !== null)
.length - 1;

if (currentIndex < 0) {
currentIndex = 0;
}

setBarLineChartSeries((prev) => {
const newMainY = [...prev.data.mainY];
const newSubY = [...prev.data.subY];

const currentMainYAmount = Number(newMainY[currentIndex]?.amount ?? 0);
const currentSubYAmount = Number(newSubY[currentIndex]?.amount ?? 0);

newMainY[currentIndex] = {
...newMainY[currentIndex],
amount: +(currentMainYAmount + Math.random() * 2).toFixed(1),
unit: '만',
};

newSubY[currentIndex] = {
...newSubY[currentIndex],
amount: currentSubYAmount + Math.floor(Math.random() * 3 + 1),
unit: '건',
};

return {
...prev,
data: {
...prev.data,
mainY: newMainY,
subY: newSubY,
},
};
});
};

const handleUpdateNextSeries = () => {
const nextIndex = barLineChartSeries.data.mainY.filter(
(datum) => datum.amount !== null,
).length;

if (nextIndex >= barLineChartSeries.data.mainY.length) {
return;
}

setBarLineChartSeries((prev) => ({
...prev,
data: {
...prev.data,
mainY: [
...prev.data.mainY.slice(0, nextIndex),
{ amount: 0, unit: '만' },
...prev.data.mainY.slice(nextIndex + 1),
],
subY: [
...prev.data.subY.slice(0, nextIndex),
{ amount: 0, unit: '건' },
...prev.data.subY.slice(nextIndex + 1),
],
},
}));
};

const handleReset = () => {
setBarLineChartSeries({
...BAR_LINE_REALTIME_MOCK,
data: {
...BAR_LINE_REALTIME_MOCK.data,
mainY: BAR_LINE_REALTIME_MOCK.data.mainY.map((datum) => ({
...datum,
amount: null,
})),
subY: BAR_LINE_REALTIME_MOCK.data.subY.map((datum) => ({
...datum,
amount: null,
})),
},
});
};

return (
<div className="flex flex-col gap-5">
<div
style={{
width: `${args.viewBoxWidth}px`,
height: `${args.viewBoxHeight}px`,
}}
>
<BarLineChart {...args} barLineChartSeries={barLineChartSeries} />
</div>
<Button
onClick={handleUpdateCurrentSeries}
variant="outline"
size="sm"
className="w-fit"
>
실시간 업데이트
</Button>
<Button
onClick={handleUpdateNextSeries}
variant="outline"
size="sm"
className="w-fit"
>
다음 시간대 업데이트
</Button>
<Button
onClick={handleReset}
variant="outline"
size="sm"
className="w-fit"
>
초기화
</Button>
</div>
);
};

export const Default: Story = {
args: {
viewBoxWidth: 1000,
viewBoxHeight: 300,
yGuideLineCount: 5,
xAxisType: 'default',
chartTitle: 'BarLineChart',
chartDescription: 'BarLineChart',
hasXAxis: true,
showXGuideLine: true,
showYGuideLine: true,
tooltipContent: (mainY, subY) => `${mainY}/${subY}`,
barLineChartSeries: BAR_LINE_WEEKLY_MOCK,
},
render: (args) => (
<TooltipProvider>
<div
style={{
width: `${args.viewBoxWidth}px`,
height: `${args.viewBoxHeight}px`,
}}
>
<BarLineChart {...args} />
</div>
</TooltipProvider>
),
};

export const Monthly30Days: Story = {
args: {
viewBoxWidth: 2000,
viewBoxHeight: 400,
yGuideLineCount: 4,
xAxisType: 'right-arrow',
chartTitle: '일별 매출 추이',
chartDescription: '최근 30일 실매출과 주문건수 데이터',
hasXAxis: true,
showXGuideLine: false,
showYGuideLine: true,
activeTooltip: true,
tooltipContent: (mainY, subY) => `${mainY}/${subY}`,
barLineChartSeries: BAR_LINE_MONTHLY_MOCK,
},
render: (args) => (
<TooltipProvider>
<div
style={{
width: `${args.viewBoxWidth}px`,
height: `${args.viewBoxHeight}px`,
}}
>
<BarLineChart {...args} />
</div>
</TooltipProvider>
),
};

export const Realtime: Story = {
args: {
viewBoxWidth: 1020,
viewBoxHeight: 260,
yGuideLineCount: 4,
xAxisType: 'tick',
chartTitle: '시간대별 실시간 매출/주문',
chartDescription: '실시간으로 업데이트되는 바-라인 차트',
hasXAxis: true,
showXGuideLine: true,
showYGuideLine: true,
activeTooltip: true,
tooltipContent: (mainY, subY) => `${mainY}/${subY}`,
barLineChartSeries: BAR_LINE_REALTIME_MOCK,
},
render: (args) => (
<TooltipProvider>
<RealtimeBarLineChart {...args} />
</TooltipProvider>
),
};
Loading
Loading