차트 컴포넌트 수정

This commit is contained in:
dohyeons 2025-10-20 15:01:32 +09:00
parent 597b9b9a51
commit 84994a30e8
3 changed files with 133 additions and 64 deletions

View File

@ -32,11 +32,12 @@ export function BarChart({ data, config, width = 600, height = 400 }: BarChartPr
// X축 스케일 (카테고리) // X축 스케일 (카테고리)
const xScale = d3.scaleBand().domain(data.labels).range([0, chartWidth]).padding(0.2); const xScale = d3.scaleBand().domain(data.labels).range([0, chartWidth]).padding(0.2);
// Y축 스케일 (값) // Y축 스케일 (값) - 절대값 기준
const maxValue = d3.max(data.datasets.flatMap((ds) => ds.data)) || 0; const allValues = data.datasets.flatMap((ds) => ds.data);
const maxAbsValue = d3.max(allValues.map((v) => Math.abs(v))) || 0;
const yScale = d3 const yScale = d3
.scaleLinear() .scaleLinear()
.domain([0, maxValue * 1.1]) .domain([0, maxAbsValue * 1.1])
.range([chartHeight, 0]) .range([chartHeight, 0])
.nice(); .nice();
@ -49,23 +50,12 @@ export function BarChart({ data, config, width = 600, height = 400 }: BarChartPr
.style("text-anchor", "end") .style("text-anchor", "end")
.style("font-size", "12px"); .style("font-size", "12px");
// Y축 그리기 // Y축 그리기 (값 표시 제거)
g.append("g").call(d3.axisLeft(yScale)).style("font-size", "12px"); g.append("g")
.call(d3.axisLeft(yScale).tickFormat(() => ""))
.style("font-size", "12px");
// 그리드 라인 // 그리드 라인 제거됨
if (config.showGrid !== false) {
g.append("g")
.attr("class", "grid")
.call(
d3
.axisLeft(yScale)
.tickSize(-chartWidth)
.tickFormat(() => ""),
)
.style("stroke-dasharray", "3,3")
.style("stroke", "#e0e0e0")
.style("opacity", 0.5);
}
// 색상 팔레트 // 색상 팔레트
const colors = config.colors || ["#3B82F6", "#EF4444", "#10B981", "#F59E0B"]; const colors = config.colors || ["#3B82F6", "#EF4444", "#10B981", "#F59E0B"];
@ -84,18 +74,48 @@ export function BarChart({ data, config, width = 600, height = 400 }: BarChartPr
.attr("y", chartHeight) .attr("y", chartHeight)
.attr("width", barWidth) .attr("width", barWidth)
.attr("height", 0) .attr("height", 0)
.attr("fill", dataset.color || colors[i % colors.length]) .attr("fill", (d) => {
// 음수면 빨간색 계열, 양수면 원래 색상
if (d < 0) {
return "#EF4444";
}
return dataset.color || colors[i % colors.length];
})
.attr("rx", 4); .attr("rx", 4);
// 애니메이션 // 애니메이션 - 절대값 기준으로 위쪽으로만 렌더링
if (config.enableAnimation !== false) { if (config.enableAnimation !== false) {
bars bars
.transition() .transition()
.duration(config.animationDuration || 750) .duration(config.animationDuration || 750)
.attr("y", (d) => yScale(d)) .attr("y", (d) => yScale(Math.abs(d)))
.attr("height", (d) => chartHeight - yScale(d)); .attr("height", (d) => chartHeight - yScale(Math.abs(d)));
} else { } else {
bars.attr("y", (d) => yScale(d)).attr("height", (d) => chartHeight - yScale(d)); bars.attr("y", (d) => yScale(Math.abs(d))).attr("height", (d) => chartHeight - yScale(Math.abs(d)));
}
// 막대 위에 값 표시 (음수는 - 부호 포함)
const labels = g
.selectAll(`.label-${i}`)
.data(dataset.data)
.enter()
.append("text")
.attr("class", `label-${i}`)
.attr("x", (_, j) => (xScale(data.labels[j]) || 0) + barWidth * i + barWidth / 2)
.attr("y", (d) => yScale(Math.abs(d)) - 5)
.attr("text-anchor", "middle")
.style("font-size", "11px")
.style("font-weight", "500")
.style("fill", (d) => (d < 0 ? "#EF4444" : "#333"))
.text((d) => (d < 0 ? "-" : "") + Math.abs(d).toLocaleString());
// 애니메이션 (라벨)
if (config.enableAnimation !== false) {
labels
.style("opacity", 0)
.transition()
.duration(config.animationDuration || 750)
.style("opacity", 1);
} }
// 툴팁 // 툴팁

View File

@ -32,37 +32,25 @@ export function HorizontalBarChart({ data, config, width = 600, height = 400 }:
// Y축 스케일 (카테고리) - 수평이므로 Y축이 카테고리 // Y축 스케일 (카테고리) - 수평이므로 Y축이 카테고리
const yScale = d3.scaleBand().domain(data.labels).range([0, chartHeight]).padding(0.2); const yScale = d3.scaleBand().domain(data.labels).range([0, chartHeight]).padding(0.2);
// X축 스케일 (값) - 수평이므로 X축이 값 // X축 스케일 (값) - 수평이므로 X축이 값, 절대값 기준
const maxValue = d3.max(data.datasets.flatMap((ds) => ds.data)) || 0; const allValues = data.datasets.flatMap((ds) => ds.data);
const maxAbsValue = d3.max(allValues.map((v) => Math.abs(v))) || 0;
const xScale = d3 const xScale = d3
.scaleLinear() .scaleLinear()
.domain([0, maxValue * 1.1]) .domain([0, maxAbsValue * 1.1])
.range([0, chartWidth]) .range([0, chartWidth])
.nice(); .nice();
// Y축 그리기 (카테고리) // Y축 그리기 (카테고리)
g.append("g").call(d3.axisLeft(yScale)).style("font-size", "12px").selectAll("text").style("text-anchor", "end"); g.append("g").call(d3.axisLeft(yScale)).style("font-size", "12px").selectAll("text").style("text-anchor", "end");
// X축 그리기 (값) // X축 그리기 (값 표시 제거)
g.append("g") g.append("g")
.attr("transform", `translate(0,${chartHeight})`) .attr("transform", `translate(0,${chartHeight})`)
.call(d3.axisBottom(xScale)) .call(d3.axisBottom(xScale).tickFormat(() => ""))
.style("font-size", "12px"); .style("font-size", "12px");
// 그리드 라인 // 그리드 라인 제거됨
if (config.showGrid !== false) {
g.append("g")
.attr("class", "grid")
.call(
d3
.axisBottom(xScale)
.tickSize(chartHeight)
.tickFormat(() => ""),
)
.style("stroke-dasharray", "3,3")
.style("stroke", "#e0e0e0")
.style("opacity", 0.5);
}
// 색상 팔레트 // 색상 팔레트
const colors = config.colors || ["#3B82F6", "#EF4444", "#10B981", "#F59E0B"]; const colors = config.colors || ["#3B82F6", "#EF4444", "#10B981", "#F59E0B"];
@ -81,17 +69,49 @@ export function HorizontalBarChart({ data, config, width = 600, height = 400 }:
.attr("y", (_, j) => (yScale(data.labels[j]) || 0) + barHeight * i) .attr("y", (_, j) => (yScale(data.labels[j]) || 0) + barHeight * i)
.attr("width", 0) .attr("width", 0)
.attr("height", barHeight) .attr("height", barHeight)
.attr("fill", dataset.color || colors[i % colors.length]) .attr("fill", (d) => {
// 음수면 빨간색 계열, 양수면 원래 색상
if (d < 0) {
return "#EF4444";
}
return dataset.color || colors[i % colors.length];
})
.attr("ry", 4); .attr("ry", 4);
// 애니메이션 // 애니메이션 - 절대값 기준으로 오른쪽으로만 렌더링
if (config.enableAnimation !== false) { if (config.enableAnimation !== false) {
bars bars
.transition() .transition()
.duration(config.animationDuration || 750) .duration(config.animationDuration || 750)
.attr("width", (d) => xScale(d)); .attr("x", 0)
.attr("width", (d) => xScale(Math.abs(d)));
} else { } else {
bars.attr("width", (d) => xScale(d)); bars.attr("x", 0).attr("width", (d) => xScale(Math.abs(d)));
}
// 막대 끝에 값 표시 (음수는 - 부호 포함)
const labels = g
.selectAll(`.label-${i}`)
.data(dataset.data)
.enter()
.append("text")
.attr("class", `label-${i}`)
.attr("x", (d) => xScale(Math.abs(d)) + 5)
.attr("y", (_, j) => (yScale(data.labels[j]) || 0) + barHeight * i + barHeight / 2)
.attr("text-anchor", "start")
.attr("dominant-baseline", "middle")
.style("font-size", "11px")
.style("font-weight", "500")
.style("fill", (d) => (d < 0 ? "#EF4444" : "#333"))
.text((d) => (d < 0 ? "-" : "") + Math.abs(d).toLocaleString());
// 애니메이션 (라벨)
if (config.enableAnimation !== false) {
labels
.style("opacity", 0)
.transition()
.duration(config.animationDuration || 750)
.style("opacity", 1);
} }
// 툴팁 // 툴팁

View File

@ -66,24 +66,12 @@ export function StackedBarChart({ data, config, width = 600, height = 400 }: Sta
.style("text-anchor", "end") .style("text-anchor", "end")
.style("font-size", "12px"); .style("font-size", "12px");
// Y축 그리기 // Y축 그리기 (값 표시 제거)
const yAxis = config.stackMode === "percent" ? d3.axisLeft(yScale).tickFormat((d) => `${d}%`) : d3.axisLeft(yScale); g.append("g")
g.append("g").call(yAxis).style("font-size", "12px"); .call(d3.axisLeft(yScale).tickFormat(() => ""))
.style("font-size", "12px");
// 그리드 라인 // 그리드 라인 제거됨
if (config.showGrid !== false) {
g.append("g")
.attr("class", "grid")
.call(
d3
.axisLeft(yScale)
.tickSize(-chartWidth)
.tickFormat(() => ""),
)
.style("stroke-dasharray", "3,3")
.style("stroke", "#e0e0e0")
.style("opacity", 0.5);
}
// 색상 팔레트 // 색상 팔레트
const colors = config.colors || ["#3B82F6", "#EF4444", "#10B981", "#F59E0B"]; const colors = config.colors || ["#3B82F6", "#EF4444", "#10B981", "#F59E0B"];
@ -131,6 +119,47 @@ export function StackedBarChart({ data, config, width = 600, height = 400 }: Sta
.attr("height", (d) => yScale(d[0] as number) - yScale(d[1] as number)); .attr("height", (d) => yScale(d[0] as number) - yScale(d[1] as number));
} }
// 각 세그먼트에 값 표시
layers.each(function (layerData, layerIndex) {
d3.select(this)
.selectAll("text")
.data(layerData)
.enter()
.append("text")
.attr("x", (d) => (xScale((d.data as any).label) || 0) + xScale.bandwidth() / 2)
.attr("y", (d) => {
const segmentHeight = yScale(d[0] as number) - yScale(d[1] as number);
const segmentMiddle = yScale(d[1] as number) + segmentHeight / 2;
return segmentMiddle;
})
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.style("font-size", "11px")
.style("font-weight", "500")
.style("fill", "white")
.style("pointer-events", "none")
.text((d) => {
const value = (d[1] as number) - (d[0] as number);
if (config.stackMode === "percent") {
return value > 5 ? `${value.toFixed(0)}%` : "";
}
return value > 0 ? value.toLocaleString() : "";
})
.style("opacity", 0);
// 애니메이션 (라벨)
if (config.enableAnimation !== false) {
d3.select(this)
.selectAll("text")
.transition()
.delay(config.animationDuration || 750)
.duration(300)
.style("opacity", 1);
} else {
d3.select(this).selectAll("text").style("opacity", 1);
}
});
// 툴팁 // 툴팁
if (config.showTooltip !== false) { if (config.showTooltip !== false) {
bars bars