From 1b576708306eae715c7327ddfa442ccd226ad206 Mon Sep 17 00:00:00 2001 From: swcho Date: Wed, 24 Sep 2025 14:47:51 +0900 Subject: [PATCH] =?UTF-8?q?test:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=B0=A8=ED=8A=B8=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/(with-base-layout)/ii/test/page.tsx | 105 +++++++++++++ .../(with-base-layout)/ii/test/page_bar.tsx | 143 +++++++++++++++++ .../(with-base-layout)/ii/test/page_pie.tsx | 146 ++++++++++++++++++ 3 files changed, 394 insertions(+) create mode 100644 apps/sn/app/(with-base-layout)/ii/test/page.tsx create mode 100644 apps/sn/app/(with-base-layout)/ii/test/page_bar.tsx create mode 100644 apps/sn/app/(with-base-layout)/ii/test/page_pie.tsx diff --git a/apps/sn/app/(with-base-layout)/ii/test/page.tsx b/apps/sn/app/(with-base-layout)/ii/test/page.tsx new file mode 100644 index 000000000..9e11fccbb --- /dev/null +++ b/apps/sn/app/(with-base-layout)/ii/test/page.tsx @@ -0,0 +1,105 @@ +"use client"; + +import React from "react"; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, + type ChartOptions, +} from "chart.js"; +import { Bar } from "react-chartjs-2"; + +ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); + +export default function Page() { + const labels = ["한강", "낙동강", "영산강", "금강"]; + + // 데이터: [기타, 오수관로, 우수관로] + const datasets = [ + { + label: "기타", + data: [15481, 6959, 1938, 1967], + backgroundColor: "rgba(255, 178, 150, 0.9)", // 연한 주황(파스텔톤) + borderColor: "rgba(255,178,150,1)", + }, + { + label: "오수관로", + data: [15878, 16336, 5670, 8073], + backgroundColor: "rgba(180, 240, 190, 0.95)", // 연한 초록(파스텔톤) + borderColor: "rgba(180,240,190,1)", + }, + { + label: "우수관로", + data: [12892, 10491, 4280, 4838], + backgroundColor: "rgba(170, 210, 245, 0.95)", // 연한 파랑(파스텔톤) + borderColor: "rgba(170,210,245,1)", + }, + ]; + + // 총합(라벨당) 계산 + const totals = labels.map((_, i) => + datasets.reduce((s, ds) => s + (ds.data[i] as number), 0), + ); + + const options: ChartOptions<"bar"> = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: "top", + }, + tooltip: { + callbacks: { + label: (ctx) => `${ctx.dataset.label}: ${Number(ctx.parsed.y).toLocaleString()}km`, + }, + }, + }, + scales: { + x: { + stacked: true, + }, + y: { + stacked: true, + beginAtZero: true, + ticks: { + callback: (value) => `${Number(value).toLocaleString()}km`, + }, + }, + }, + // 각 스택 위에 총합 표시 + animation: { + onComplete: function (animation) { + const chart = animation.chart; + const ctx = chart.ctx; + const lastMeta = chart.getDatasetMeta(datasets.length - 1); + ctx.save(); + ctx.fillStyle = "#111"; + ctx.font = "gray bold 12px Arial"; + ctx.textAlign = "center"; + + totals.forEach((total, i) => { + const bar = lastMeta.data[i]; + if (!bar) return; + const x = bar.x; + // y는 마지막 스택의 y 좌표(스택의 최상단)에서 조금 위로 + const y = bar.y - 8; + ctx.fillText(`총 ${total.toLocaleString()}km`, x, y); + }); + + ctx.restore(); + }, + }, + }; + + const data = { labels, datasets }; + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/apps/sn/app/(with-base-layout)/ii/test/page_bar.tsx b/apps/sn/app/(with-base-layout)/ii/test/page_bar.tsx new file mode 100644 index 000000000..08b8755c8 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/ii/test/page_bar.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend, + type ChartOptions, +} from "chart.js"; +import { Bar } from "react-chartjs-2"; +import { Card, CardHeader, CardTitle, CardContent } from "@common/business_components/ui/chart/lib/card"; +import { Button } from "@common/components"; + +// ChartJS 컴포넌트 등록 +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Title, + Tooltip, + Legend +); + +export default function TestPage() { + // 차트 데이터 + const labels = ["한강", "낙동강", "영산강", "금강"]; + + const datasets = [ + { + label: '20-25년', + data: [3859, 1066, 1929, 2828], + backgroundColor: '#3B82F6', // 파란색 + borderRadius: 4, + }, + { + label: '25년-30년', + data: [3153, 1420, 1204, 2877], + backgroundColor: '#EF4444', // 빨간색 + borderRadius: 4, + }, + { + label: '30년이상', + data: [9404, 1835, 1821, 8115], + backgroundColor: '#F87171', // 연한 빨간색/분홍색 + borderRadius: 4, + }, + ]; + + // 각 지역별 총 길이 계산 + const totalLengths = [44251, 33786, 11888, 14878]; + + // 차트 옵션 설정 + const options: ChartOptions<"bar"> = { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + grid: { + display: true, + }, + }, + y: { + beginAtZero: true, + min: 0, + max: 10000, + ticks: { + stepSize: 2000, + callback: function(value) { + return value + 'km'; + } + }, + grid: { + color: 'rgba(200, 200, 200, 0.3)', + } + } + }, + plugins: { + legend: { + position: 'top', + align: 'start', + labels: { + boxWidth: 15, + padding: 15, + usePointStyle: false, + }, + }, + tooltip: { + callbacks: { + label: function(context) { + return `${context.dataset.label}: ${context.parsed.y}km`; + } + } + }, + }, + // 총 길이 데이터 표시를 위한 애니메이션 후 함수 설정 + animation: { + onComplete: function(animation) { + const chart = animation.chart; + const ctx = chart.ctx; + + // 각 데이터셋의 마지막 데이터셋의 인덱스 + const lastDatasetIndex = datasets.length - 1; + + // 텍스트 스타일 설정 + ctx.font = '11px Arial'; + ctx.fillStyle = '#333'; + ctx.textAlign = 'center'; + + // 각 지역별로 총 길이 표시 + labels.forEach((_, i) => { + const xPos = chart.scales.x ? chart.scales.x.getPixelForTick(i) : 0; + const yPos = chart.chartArea.top; // 차트 영역 최상단 위에 표시 + + // 막대 위에 총 길이 텍스트 표시 + if (totalLengths[i]) ctx.fillText(`총 ${totalLengths[i].toLocaleString()}km`, xPos, yPos); + }); + } + } + }; + + const data = { + labels, + datasets, + }; + + return ( + + + 노후하수관로현황 + + + + {/* 너비 400px, 높이 150px로 조정 */} +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/apps/sn/app/(with-base-layout)/ii/test/page_pie.tsx b/apps/sn/app/(with-base-layout)/ii/test/page_pie.tsx new file mode 100644 index 000000000..2cd6c1b95 --- /dev/null +++ b/apps/sn/app/(with-base-layout)/ii/test/page_pie.tsx @@ -0,0 +1,146 @@ +// ...existing code... +"use client"; + +import React from "react"; +import { Chart as ChartJS, ArcElement, Tooltip, Legend, type ChartOptions } from "chart.js"; +import { Pie } from "react-chartjs-2"; + +ChartJS.register(ArcElement, Tooltip, Legend); + +export default function Page() { + const labels = ["한강", "낙동강", "영산강", "금강"]; + const datasets = [ + { label: "20년 미만", data: [27835, 29465, 6934, 1058], color: "rgba(169, 169, 169, 0.8)" }, + { label: "20-25년", data: [3859, 1066, 1929, 2828], color: "rgba(255, 160, 122, 0.8)" }, + { label: "25년-30년", data: [3153, 1420, 1204, 2877], color: "rgba(152, 251, 152, 0.8)" }, + { label: "30년이상", data: [9404, 1835, 1821, 8115], color: "rgba(135, 206, 235, 0.8)" }, + ]; + // 각 지역별 총 길이 계산 + const totalLengths = [44251, 33786, 11888, 14878]; + // 외부 툴팁을 사용하여 컨테이너 넘침과 관계없이 전체 툴팁을 보여주고, + // 각 항목의 전체에서 차지하는 비율(%)을 표시합니다. + const pieOptions: ChartOptions<"pie"> = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + tooltip: { + enabled: false, // 기본 캔버스 툴팁 비활성화 (외부 툴팁 사용) + external: function (context) { + const { chart, tooltip } = context; + let tooltipEl = document.getElementById("chartjs-external-tooltip"); + + // tooltip DOM이 없으면 생성 + if (!tooltipEl) { + tooltipEl = document.createElement("div"); + tooltipEl.id = "chartjs-external-tooltip"; + tooltipEl.style.position = "absolute"; + tooltipEl.style.pointerEvents = "auto"; + tooltipEl.style.background = "rgba(0,0,0,0.75)"; + tooltipEl.style.color = "#fff"; + tooltipEl.style.padding = "6px 8px"; + tooltipEl.style.borderRadius = "6px"; + tooltipEl.style.fontSize = "12px"; + tooltipEl.style.zIndex = "9999"; + tooltipEl.style.transform = "translate(-50%, -120%)"; + document.body.appendChild(tooltipEl); + } + + // 사라짐 처리 + if (tooltip.opacity === 0) { + tooltipEl.style.opacity = "0"; + return; + } + + // 툴팁 내용 구성 (파이의 전체 합 대비 비율 표시) + const data = chart.data; + const dataset = data.datasets && data.datasets[0] && data.datasets[0].data + ? (data.datasets[0].data as number[]) + : []; + const total = dataset.reduce((s, n) => s + (Number(n) || 0), 0); + + const innerHtmlParts: string[] = []; + if (tooltip.dataPoints) { + tooltip.dataPoints.forEach((dp) => { + const idx = dp.dataIndex; + const label = (data.labels && data.labels[idx]) || dp.label || ""; + const value = Number(dp.raw ?? dp.parsed ?? 0); + const pct = total > 0 ? (value / total) * 100 : 0; + innerHtmlParts.push( + `
+ + ${label}: ${value.toLocaleString()}km + (${pct.toFixed(1)}%) +
` + ); + }); + } + + tooltipEl.innerHTML = innerHtmlParts.join(""); + tooltipEl.style.opacity = "1"; + + // 위치: 차트 캔버스 기준 툴팁 caret 위치에 맞춤 (윈도우 스크롤 보정 포함) + const canvasRect = chart.canvas.getBoundingClientRect(); + const caretX = tooltip.caretX ?? (canvasRect.left + canvasRect.width / 2); + const caretY = tooltip.caretY ?? (canvasRect.top + canvasRect.height / 2); + tooltipEl.style.left = `${canvasRect.left + window.pageXOffset + caretX}px`; + tooltipEl.style.top = `${canvasRect.top + window.pageYOffset + caretY}px`; + }, + }, + }, + }; + + + // 상단에 공통 범례(연도) 가로로 표시 + const SharedLegend = () => ( +
+ {datasets.map((d) => ( +
+ + {d.label} +
+ ))} +
+ ); + + // 레이블(강) 당 pie 데이터 생성 (레이블은 아래로 이동) + const Charts = () => + labels.map((river, i) => { + const data = { + labels: datasets.map((d) => d.label), + datasets: [ + { + data: datasets.map((d) => d.data[i]), + backgroundColor: datasets.map((d) => d.color), + borderColor: datasets.map(() => "rgba(255,255,255,0.8)"), + borderWidth: 1, + }, + ], + }; + + return ( +
+
+ +
+ {/* 레이블을 차트 하단으로 이동 */} +
{totalLengths[i]?.toLocaleString()} km
+
{river}
+
+ ); + }); + + return ( +
+ {/* 상단 공통 범례 */} +
+ +
+ +
+ +
+
+ ); +} +// ...existing code... \ No newline at end of file