차트 컴포넌트 수정
This commit is contained in:
parent
597b9b9a51
commit
84994a30e8
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 툴팁
|
// 툴팁
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 툴팁
|
// 툴팁
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue