[st] feat: 통계 검증 상세 차트 구현
This commit is contained in:
@@ -5,6 +5,7 @@ import { ValueType } from "realgrid";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeApi } from "@/entities/code/api/code-service";
|
||||
import { useStAnlsRsltList } from "@/entities/statistics/api/queries";
|
||||
import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api";
|
||||
import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface";
|
||||
import {
|
||||
@@ -80,22 +81,10 @@ const RegionalGrid: React.FC<RegionalGridProps> = ({
|
||||
);
|
||||
}, [regionCodes, selectedSido]);
|
||||
|
||||
const { data: resultData, isLoading } = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT,
|
||||
"list",
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
],
|
||||
queryFn: () =>
|
||||
stAnlsRsltApi.getResultList({
|
||||
anlsSn,
|
||||
vrfcTrgtSn: Number(vrfcTrgtSn),
|
||||
page: 0,
|
||||
size: 1000,
|
||||
}),
|
||||
enabled: !!vrfcTrgtSn,
|
||||
});
|
||||
const { data: resultData, isLoading } = useStAnlsRsltList(
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
);
|
||||
|
||||
const rowData = useMemo(() => {
|
||||
let data = (resultData?.content ?? []) as StAnlsRsltResponse[];
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Scatter } from "react-chartjs-2";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
type ChartData,
|
||||
type ChartOptions,
|
||||
Legend,
|
||||
LinearScale,
|
||||
LineController,
|
||||
@@ -11,12 +13,16 @@ import {
|
||||
PointElement,
|
||||
ScatterController,
|
||||
Tooltip,
|
||||
type TooltipItem,
|
||||
} from "chart.js";
|
||||
import zoomPlugin from "chartjs-plugin-zoom";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
import { CodeApi } from "@/entities/code/api/code-service";
|
||||
import { stAnlsRsltApi } from "@/entities/statistics/api/statistics-analysis-result-api";
|
||||
import {
|
||||
useStAnlsChartData,
|
||||
useStAnlsRsltList,
|
||||
} from "@/entities/statistics/api/queries";
|
||||
import type { StAnlsRsltResponse } from "@/entities/statistics/model/statistics-analysis-result-interface";
|
||||
import { LoadingWithAnimation } from "@common/components";
|
||||
|
||||
@@ -37,6 +43,15 @@ interface ScatterChartProps {
|
||||
selectedSido: string | null;
|
||||
}
|
||||
|
||||
interface LinePoint {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
interface DataPoint extends LinePoint {
|
||||
item: StAnlsRsltResponse;
|
||||
}
|
||||
|
||||
const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
@@ -54,110 +69,79 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
);
|
||||
}, [regionCodes, selectedSido]);
|
||||
|
||||
const { data: resultData, isLoading: isLoadingGrid } = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT,
|
||||
"list",
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
],
|
||||
queryFn: () =>
|
||||
stAnlsRsltApi.getResultList({
|
||||
anlsSn,
|
||||
vrfcTrgtSn: Number(vrfcTrgtSn),
|
||||
page: 0,
|
||||
size: 1000,
|
||||
}),
|
||||
enabled: !!anlsSn && !!vrfcTrgtSn,
|
||||
});
|
||||
|
||||
const { data: chartData, isLoading: isLoadingChart } = useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT,
|
||||
"chart",
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
],
|
||||
queryFn: () => stAnlsRsltApi.getChartData(anlsSn, Number(vrfcTrgtSn)),
|
||||
enabled: !!anlsSn && !!vrfcTrgtSn,
|
||||
});
|
||||
const { data: resultData, isLoading: isLoadingGrid } = useStAnlsRsltList(
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
);
|
||||
const { data: chartData, isLoading: isLoadingChart } = useStAnlsChartData(
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
);
|
||||
|
||||
const isLoading = isLoadingGrid || isLoadingChart;
|
||||
|
||||
const chartConfig = useMemo(() => {
|
||||
if (!resultData || !chartData) return null;
|
||||
|
||||
const rawPoints = (resultData.content ?? []) as StAnlsRsltResponse[];
|
||||
const rawPoints = resultData.content ?? [];
|
||||
const boundaries = chartData.list ?? [];
|
||||
|
||||
const sortedBoundaries = [...boundaries].sort(
|
||||
(a, b) => a.xPrev - b.xPrev,
|
||||
(a, b) => a.xprev - b.xprev,
|
||||
);
|
||||
const upperLineData = sortedBoundaries.map((b) => ({
|
||||
x: b.xPrev,
|
||||
y: b.yUpper,
|
||||
}));
|
||||
const lowerLineData = sortedBoundaries.map((b) => ({
|
||||
x: b.xPrev,
|
||||
y: b.yLower,
|
||||
|
||||
const upperLineData: LinePoint[] = sortedBoundaries.map((b) => ({
|
||||
x: b.yupper,
|
||||
y: b.xprev,
|
||||
}));
|
||||
|
||||
const normalPoints: {
|
||||
x: number;
|
||||
y: number;
|
||||
item: StAnlsRsltResponse;
|
||||
}[] = [];
|
||||
const outlierPoints: {
|
||||
x: number;
|
||||
y: number;
|
||||
item: StAnlsRsltResponse;
|
||||
}[] = [];
|
||||
const lowerLineData: LinePoint[] = sortedBoundaries.map((b) => ({
|
||||
x: b.xprev,
|
||||
y: b.ylower,
|
||||
}));
|
||||
|
||||
const normalPoints: DataPoint[] = [];
|
||||
const outlierPoints: DataPoint[] = [];
|
||||
|
||||
rawPoints.forEach((item) => {
|
||||
if (selectedSidoName && item.sido !== selectedSidoName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pt = {
|
||||
const pt: DataPoint = {
|
||||
x: Number(item.prvyrVl ?? 0),
|
||||
y: Number(item.cryrVl ?? 0),
|
||||
item,
|
||||
};
|
||||
|
||||
if (item.otlrYn.toLowerCase() === "y") {
|
||||
if (item.otlrYn === "0") {
|
||||
normalPoints.push(pt);
|
||||
} else {
|
||||
outlierPoints.push(pt);
|
||||
}
|
||||
});
|
||||
|
||||
const data = {
|
||||
const data: ChartData<"scatter"> = {
|
||||
datasets: [
|
||||
{
|
||||
label: "상한선",
|
||||
data: upperLineData,
|
||||
type: "line" as const,
|
||||
showLine: true,
|
||||
borderColor: "rgba(255, 99, 132, 0.8)",
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
},
|
||||
{
|
||||
label: "하한선",
|
||||
data: lowerLineData,
|
||||
type: "line" as const,
|
||||
showLine: true,
|
||||
borderColor: "rgba(54, 162, 235, 0.8)",
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
},
|
||||
{
|
||||
label: "이상치",
|
||||
data: outlierPoints,
|
||||
backgroundColor: "rgba(255, 99, 132, 0.7)",
|
||||
pointRadius: 5,
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 8,
|
||||
},
|
||||
{
|
||||
@@ -165,19 +149,19 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
data: normalPoints,
|
||||
backgroundColor: "rgba(75, 192, 192, 0.7)",
|
||||
pointRadius: 4,
|
||||
pointHoverRadius: 7,
|
||||
pointHoverRadius: 8,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = {
|
||||
const options: ChartOptions<"scatter"> = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
zoom: {
|
||||
pan: {
|
||||
enabled: true,
|
||||
mode: "xy" as const,
|
||||
mode: "xy",
|
||||
},
|
||||
zoom: {
|
||||
wheel: {
|
||||
@@ -186,36 +170,34 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
pinch: {
|
||||
enabled: true,
|
||||
},
|
||||
mode: "xy" as const,
|
||||
mode: "xy",
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (context: any) => {
|
||||
if (context.dataset.type === "line") {
|
||||
return `${context.dataset.label}: (${context.parsed.x.toFixed(2)}, ${context.parsed.y.toFixed(2)})`;
|
||||
label: (context: TooltipItem<"scatter">) => {
|
||||
const dataset = context.dataset;
|
||||
if (dataset.showLine) {
|
||||
return `${context.dataset.label}: (${context?.parsed?.x?.toFixed(2) ?? "-"}, ${context?.parsed?.y?.toFixed(2) ?? "-"})`;
|
||||
}
|
||||
const pt = context.raw as {
|
||||
x: number;
|
||||
y: number;
|
||||
item: StAnlsRsltResponse;
|
||||
};
|
||||
return `${pt.item.sido} ${pt.item.gugun}: 이전(${pt.x}), 당해(${pt.y})`;
|
||||
|
||||
const pt = (context.raw as DataPoint) ?? {};
|
||||
return `${pt?.item?.sido ?? "시/도"} ${pt?.item?.gugun ?? "구/군"}: 이전(${pt?.x ?? "-"}), 당해(${pt?.y ?? "-"})`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: "linear" as const,
|
||||
position: "bottom" as const,
|
||||
type: "linear",
|
||||
position: "bottom",
|
||||
title: {
|
||||
display: true,
|
||||
text: "이전년도 값",
|
||||
},
|
||||
},
|
||||
y: {
|
||||
type: "linear" as const,
|
||||
type: "linear",
|
||||
title: {
|
||||
display: true,
|
||||
text: "당해년도 값",
|
||||
@@ -242,8 +224,8 @@ const ScatterChart: React.FC<ScatterChartProps> = ({
|
||||
) : chartConfig ? (
|
||||
<div className="h-[48rem] w-full">
|
||||
<Scatter
|
||||
data={chartConfig.data as any}
|
||||
options={chartConfig.options as any}
|
||||
data={chartConfig.data}
|
||||
options={chartConfig.options}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { ENDPOINT } from "@/shared/config";
|
||||
|
||||
import { stAnlsRsltApi } from "./statistics-analysis-result-api";
|
||||
|
||||
export const useStAnlsRsltList = (
|
||||
anlsSn: number,
|
||||
vrfcTrgtSn: string | null,
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT,
|
||||
"list",
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
],
|
||||
queryFn: () =>
|
||||
stAnlsRsltApi.getResultList({
|
||||
anlsSn,
|
||||
vrfcTrgtSn: Number(vrfcTrgtSn),
|
||||
page: 0,
|
||||
size: 1000,
|
||||
}),
|
||||
enabled: !!anlsSn && !!vrfcTrgtSn,
|
||||
});
|
||||
};
|
||||
|
||||
export const useStAnlsChartData = (
|
||||
anlsSn: number,
|
||||
vrfcTrgtSn: string | null,
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: [
|
||||
ENDPOINT.STATVERIFY_SERVICE.ST_ANLS_RSLT,
|
||||
"chart",
|
||||
anlsSn,
|
||||
vrfcTrgtSn,
|
||||
],
|
||||
queryFn: () => stAnlsRsltApi.getChartData(anlsSn, Number(vrfcTrgtSn)),
|
||||
enabled: !!anlsSn && !!vrfcTrgtSn,
|
||||
});
|
||||
};
|
||||
@@ -35,9 +35,9 @@ export interface StAnlsRsltSearchParams extends PageableParams {
|
||||
export type StAnlsRsltListApiResponse = PageableList<StAnlsRsltResponse>;
|
||||
|
||||
export interface ChartBoundaryPointDto {
|
||||
xPrev: number; // 전년도 값 (x축)
|
||||
yLower: number; // 이상치 하한 당년값 (y축)
|
||||
yUpper: number; // 이상치 상한 당년값 (y축)
|
||||
xprev: number; // 전년도 값 (x축)
|
||||
ylower: number; // 이상치 하한 당년값 (y축)
|
||||
yupper: number; // 이상치 상한 당년값 (y축)
|
||||
}
|
||||
|
||||
export interface ChartRetDto {
|
||||
|
||||
Reference in New Issue
Block a user