diff --git a/client/src/App.tsx b/client/src/App.tsx
index 710d79c..e6f217c 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -3,7 +3,9 @@ import { ErrorModal } from "@components/modal/ErrorModal";
import { WorkSpace } from "@features/workSpace/WorkSpace";
import { useErrorStore } from "@stores/useErrorStore";
import { useUserInfo } from "@stores/useUserStore";
+import PerformanceComparison from "./babo";
import { useSocketStore } from "./stores/useSocketStore";
+import PerformanceTest from "./test";
const App = () => {
// TODO 라우터, react query 설정
@@ -27,6 +29,8 @@ const App = () => {
<>
{isErrorModalOpen && }
+
+
>
);
};
diff --git a/client/src/babo.tsx b/client/src/babo.tsx
new file mode 100644
index 0000000..cc48db4
--- /dev/null
+++ b/client/src/babo.tsx
@@ -0,0 +1,198 @@
+import React, { useEffect, useState } from "react";
+import { LineChart, Line, XAxis, YAxis, Tooltip, Legend, ResponsiveContainer } from "recharts";
+import { useSocketStore } from "./stores/useSocketStore";
+
+const PerformanceComparison = () => {
+ const [performanceData, setPerformanceData] = useState({
+ individualEvents: {
+ latencies: [],
+ totalEvents: 0,
+ averageLatency: 0,
+ eventCounts: {
+ "insert/char": 0,
+ "delete/char": 0,
+ "update/char": 0,
+ "insert/block": 0,
+ "delete/block": 0,
+ "update/block": 0,
+ },
+ },
+ networkStats: {
+ requestCount: 0,
+ totalDataSent: 0,
+ avgRequestPerSecond: 0,
+ },
+ timeSeriesData: [],
+ });
+
+ const socket = useSocketStore((state) => state.socket);
+
+ useEffect(() => {
+ if (!socket) return;
+
+ let requestsLastSecond = 0;
+ let lastSecondTimestamp = Date.now();
+ const operationStartTimes = new Map();
+
+ const eventTypes = [
+ "insert/char",
+ "delete/char",
+ "update/char",
+ "insert/block",
+ "delete/block",
+ "update/block",
+ ];
+
+ const handleEvent = (eventType, data) => {
+ const operationId = `${eventType}-${Date.now()}`;
+ const startTime = performance.now();
+ operationStartTimes.set(operationId, startTime);
+
+ requestsLastSecond++;
+ const requestSize = JSON.stringify(data).length;
+
+ // 작업 완료 시점 측정
+ const endTime = performance.now();
+ const duration = endTime - startTime;
+
+ setPerformanceData((prev) => {
+ const timestamp = Date.now();
+ const newLatencies = [...prev.individualEvents.latencies, duration].slice(-50);
+ const totalEvents = prev.individualEvents.totalEvents + 1;
+ const avgLatency = newLatencies.reduce((a, b) => a + b, 0) / newLatencies.length;
+
+ // 초당 요청 수 계산
+ if (timestamp - lastSecondTimestamp >= 1000) {
+ prev.networkStats.avgRequestPerSecond = requestsLastSecond;
+ requestsLastSecond = 0;
+ lastSecondTimestamp = timestamp;
+ }
+
+ const eventCounts = {
+ ...prev.individualEvents.eventCounts,
+ [eventType]: (prev.individualEvents.eventCounts[eventType] || 0) + 1,
+ };
+
+ // 시계열 데이터 업데이트
+ const newTimeSeriesData = [
+ ...prev.timeSeriesData,
+ {
+ timestamp,
+ latency: duration,
+ requestsPerSecond: prev.networkStats.avgRequestPerSecond,
+ eventType,
+ },
+ ].slice(-100);
+
+ return {
+ individualEvents: {
+ latencies: newLatencies,
+ totalEvents,
+ averageLatency: avgLatency,
+ eventCounts,
+ },
+ networkStats: {
+ requestCount: prev.networkStats.requestCount + 1,
+ totalDataSent: prev.networkStats.totalDataSent + requestSize,
+ avgRequestPerSecond: prev.networkStats.avgRequestPerSecond,
+ },
+ timeSeriesData: newTimeSeriesData,
+ };
+ });
+
+ operationStartTimes.delete(operationId);
+ };
+
+ // 이벤트 리스너 등록
+ eventTypes.forEach((eventType) => {
+ socket.on(eventType, (data) => handleEvent(eventType, data));
+ });
+
+ return () => {
+ eventTypes.forEach((eventType) => {
+ socket.off(eventType);
+ });
+ };
+ }, [socket]);
+
+ const chartData = performanceData.timeSeriesData.map((data, index) => ({
+ name: index,
+ latency: data.latency,
+ requestsPerSecond: data.requestsPerSecond,
+ }));
+
+ return (
+
+
개별 이벤트 모드 성능 모니터링
+
+
+
+
이벤트 통계
+
+
총 이벤트 수: {performanceData.individualEvents.totalEvents}
+
평균 지연 시간: {performanceData.individualEvents.averageLatency.toFixed(2)}ms
+
초당 요청 수: {performanceData.networkStats.avgRequestPerSecond}
+
+
+
+
+
네트워크 통계
+
+
총 요청 수: {performanceData.networkStats.requestCount}
+
+ 전송된 데이터: {(performanceData.networkStats.totalDataSent / 1024).toFixed(2)} KB
+
+
+
+
+
+
+
+
이벤트 타입별 카운트
+
+ {Object.entries(performanceData.individualEvents.eventCounts).map(([type, count]) => (
+
+ {type}: {count}
+
+ ))}
+
+
+
+
+
+
+
지연 시간 & 요청 수 추이
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PerformanceComparison;
diff --git a/client/src/stores/useSocketStore.ts b/client/src/stores/useSocketStore.ts
index e48e227..f96d6c7 100644
--- a/client/src/stores/useSocketStore.ts
+++ b/client/src/stores/useSocketStore.ts
@@ -100,7 +100,7 @@ interface PageOperationsHandlers {
onRemotePageDelete: (operation: RemotePageDeleteOperation) => void;
onRemotePageUpdate: (operation: RemotePageUpdateOperation) => void;
}
-
+const test = false;
export const useSocketStore = create((set, get) => ({
socket: null,
clientId: null,
@@ -242,45 +242,62 @@ export const useSocketStore = create((set, get) => ({
},
sendBlockInsertOperation: (operation: RemoteBlockInsertOperation) => {
- // const { socket } = get();
- // socket?.emit("insert/block", operation);
- const { sendOperation } = get();
- sendOperation(operation);
+ if (test) {
+ const { socket } = get();
+ socket?.emit("insert/block", operation);
+ } else {
+ const { sendOperation } = get();
+ sendOperation(operation);
+ }
},
sendCharInsertOperation: (operation: RemoteCharInsertOperation) => {
- // const { socket } = get();
- // socket?.emit("insert/char", operation);
- const { sendOperation } = get();
- sendOperation(operation);
+ if (test) {
+ const { socket } = get();
+ socket?.emit("insert/char", operation);
+ } else {
+ const { sendOperation } = get();
+ sendOperation(operation);
+ }
},
sendBlockUpdateOperation: (operation: RemoteBlockUpdateOperation) => {
- // const { socket } = get();
- // socket?.emit("update/block", operation);
- const { sendOperation } = get();
- sendOperation(operation);
+ if (test) {
+ const { socket } = get();
+ socket?.emit("update/block", operation);
+ } else {
+ const { sendOperation } = get();
+ sendOperation(operation);
+ }
},
-
sendBlockDeleteOperation: (operation: RemoteBlockDeleteOperation) => {
- // const { socket } = get();
- // socket?.emit("delete/block", operation);
- const { sendOperation } = get();
- sendOperation(operation);
+ if (test) {
+ const { socket } = get();
+ socket?.emit("delete/block", operation);
+ } else {
+ const { sendOperation } = get();
+ sendOperation(operation);
+ }
},
sendCharDeleteOperation: (operation: RemoteCharDeleteOperation) => {
- // const { socket } = get();
- // socket?.emit("delete/char", operation);
- const { sendOperation } = get();
- sendOperation(operation);
+ if (test) {
+ const { socket } = get();
+ socket?.emit("delete/char", operation);
+ } else {
+ const { sendOperation } = get();
+ sendOperation(operation);
+ }
},
sendCharUpdateOperation: (operation: RemoteCharUpdateOperation) => {
- // const { socket } = get();
- // socket?.emit("update/char", operation);
- const { sendOperation } = get();
- sendOperation(operation);
+ if (test) {
+ const { socket } = get();
+ socket?.emit("update/char", operation);
+ } else {
+ const { sendOperation } = get();
+ sendOperation(operation);
+ }
},
sendCursorPosition: (position: CursorPosition) => {
@@ -289,10 +306,13 @@ export const useSocketStore = create((set, get) => ({
},
sendBlockReorderOperation: (operation: RemoteBlockReorderOperation) => {
- // const { socket } = get();
- // socket?.emit("reorder/block", operation);
- const { sendOperation } = get();
- sendOperation(operation);
+ if (test) {
+ const { socket } = get();
+ socket?.emit("reorder/block", operation);
+ } else {
+ const { sendOperation } = get();
+ sendOperation(operation);
+ }
},
sendBlockCheckboxOperation: (operation: RemoteBlockCheckboxOperation) => {
diff --git a/client/src/test.tsx b/client/src/test.tsx
new file mode 100644
index 0000000..db32fc3
--- /dev/null
+++ b/client/src/test.tsx
@@ -0,0 +1,246 @@
+import React, { useEffect, useState } from "react";
+import {
+ LineChart,
+ Line,
+ XAxis,
+ YAxis,
+ Tooltip,
+ Legend,
+ ResponsiveContainer,
+ BarChart,
+ Bar,
+} from "recharts";
+import { useSocketStore } from "./stores/useSocketStore";
+
+const BatchEfficiencyMonitor = () => {
+ // 네트워크 효율성과 처리 성능을 분리하여 저장
+ const [metrics, setMetrics] = useState({
+ network: {
+ totalRequests: 0,
+ savedRequests: 0, // 배치 처리로 절약된 요청 수
+ totalDataSent: 0,
+ totalDataReceived: 0,
+ averageLatency: 0,
+ latencyHistory: [],
+ },
+ processing: {
+ operationsProcessed: 0,
+ batchesProcessed: 0,
+ averageBatchSize: 0,
+ processingTimeHistory: [],
+ operationsPerSecond: 0,
+ },
+ resourceUsage: {
+ memoryUsage: [],
+ cpuUsage: [],
+ timestamp: [],
+ },
+ batchEfficiency: {
+ networkSavingsPercent: 0,
+ processingEfficiencyGain: 0,
+ timeHistory: [],
+ },
+ });
+
+ const socket = useSocketStore((state) => state.socket);
+
+ useEffect(() => {
+ if (!socket) return;
+
+ let operationCount = 0;
+ let batchStartTime = 0;
+ let operationsThisSecond = 0;
+ let lastSecondTimestamp = Date.now();
+
+ // 네트워크 지연시간 측정
+ const measureNetworkLatency = async () => {
+ const start = performance.now();
+ return new Promise((resolve) => {
+ socket.emit("ping", () => {
+ const latency = performance.now() - start;
+ resolve(latency);
+ });
+ });
+ };
+
+ // 주기적으로 네트워크 지연시간 측정
+ const latencyInterval = setInterval(async () => {
+ const latency = await measureNetworkLatency();
+ setMetrics((prev) => ({
+ ...prev,
+ network: {
+ ...prev.network,
+ latencyHistory: [...prev.network.latencyHistory.slice(-30), latency],
+ averageLatency: prev.network.averageLatency * 0.9 + latency * 0.1,
+ },
+ }));
+ }, 2000);
+
+ // 배치 작업 모니터링
+ socket.on("batch/operations", (batch) => {
+ const batchSize = batch.length;
+ const timestamp = Date.now();
+ const processingTime = performance.now() - batchStartTime;
+
+ // 배치 처리로 인한 네트워크 요청 절감 계산
+ const savedRequests = batchSize - 1; // 배치로 절약된 요청 수
+ const dataSize = new TextEncoder().encode(JSON.stringify(batch)).length;
+
+ setMetrics((prev) => {
+ // 초당 작업 수 계산
+ operationsThisSecond += batchSize;
+ if (timestamp - lastSecondTimestamp >= 1000) {
+ operationsThisSecond = 0;
+ lastSecondTimestamp = timestamp;
+ }
+
+ // 효율성 계산
+ const networkSavingsPercent = (savedRequests / batchSize) * 100;
+ const processingEfficiencyGain =
+ ((batchSize * prev.network.averageLatency - processingTime) /
+ (batchSize * prev.network.averageLatency)) *
+ 100;
+
+ return {
+ network: {
+ ...prev.network,
+ totalRequests: prev.network.totalRequests + 1,
+ savedRequests: prev.network.savedRequests + savedRequests,
+ totalDataSent: prev.network.totalDataSent + dataSize,
+ },
+ processing: {
+ ...prev.processing,
+ operationsProcessed: prev.processing.operationsProcessed + batchSize,
+ batchesProcessed: prev.processing.batchesProcessed + 1,
+ averageBatchSize:
+ (prev.processing.averageBatchSize * prev.processing.batchesProcessed + batchSize) /
+ (prev.processing.batchesProcessed + 1),
+ processingTimeHistory: [
+ ...prev.processing.processingTimeHistory.slice(-30),
+ { time: processingTime / batchSize, operations: batchSize },
+ ],
+ operationsPerSecond: operationsThisSecond,
+ },
+ batchEfficiency: {
+ ...prev.batchEfficiency,
+ networkSavingsPercent,
+ processingEfficiencyGain,
+ timeHistory: [
+ ...prev.batchEfficiency.timeHistory.slice(-30),
+ {
+ timestamp,
+ savings: networkSavingsPercent,
+ efficiency: processingEfficiencyGain,
+ },
+ ],
+ },
+ resourceUsage: {
+ ...prev.resourceUsage,
+ timestamp: [...prev.resourceUsage.timestamp.slice(-30), timestamp],
+ },
+ };
+ });
+ });
+
+ // 개별 작업 시작 시점 기록
+ const handleOperationStart = () => {
+ batchStartTime = performance.now();
+ operationCount++;
+ };
+
+ // 이벤트 리스너 등록
+ socket.on("operation/start", handleOperationStart);
+
+ return () => {
+ clearInterval(latencyInterval);
+ socket.off("batch/operations");
+ socket.off("operation/start");
+ };
+ }, [socket]);
+
+ return (
+
+
배치 처리 효율성 모니터링
+
+
+ {/* 네트워크 효율성 */}
+
+
네트워크 효율성
+
+
총 네트워크 요청 수: {metrics.network.totalRequests}
+
절약된 요청 수: {metrics.network.savedRequests}
+
+ 요청 절감률:{" "}
+ {(
+ (metrics.network.savedRequests /
+ (metrics.network.totalRequests + metrics.network.savedRequests)) *
+ 100
+ ).toFixed(2)}
+ %
+
+
평균 네트워크 지연시간: {metrics.network.averageLatency.toFixed(2)}ms
+
전송된 총 데이터: {(metrics.network.totalDataSent / 1024).toFixed(2)}KB
+
+
+
+ {/* 처리 효율성 */}
+
+
처리 효율성
+
+
처리된 총 작업 수: {metrics.processing.operationsProcessed}
+
처리된 배치 수: {metrics.processing.batchesProcessed}
+
평균 배치 크기: {metrics.processing.averageBatchSize.toFixed(2)}
+
초당 처리 작업 수: {metrics.processing.operationsPerSecond}
+
처리 효율 향상: {metrics.batchEfficiency.processingEfficiencyGain.toFixed(2)}%
+
+
+
+
+ {/* 효율성 추이 차트 */}
+
+
효율성 지표 추이
+
+
+ new Date(timestamp).toLocaleTimeString()}
+ />
+
+ new Date(timestamp).toLocaleTimeString()} />
+
+
+
+
+
+
+
+ {/* 작업 처리 시간 분포 */}
+
+
배치 크기별 처리 시간
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default BatchEfficiencyMonitor;
diff --git a/package.json b/package.json
index 3affd83..f29cb07 100644
--- a/package.json
+++ b/package.json
@@ -20,21 +20,24 @@
"author": "",
"license": "ISC",
"devDependencies": {
- "@noctaCrdt": "workspace:*",
"@eslint/js": "^9.14.0",
+ "@noctaCrdt": "workspace:*",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"eslint": "^8.57.1",
- "eslint-plugin-jsx-a11y": "^6.8.0",
- "eslint-plugin-react": "^7.33.2",
- "eslint-plugin-react-hooks": "^4.6.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.0.0",
+ "eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.29.1",
+ "eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prettier": "^5.0.0",
- "eslint-import-resolver-typescript": "^3.6.3",
+ "eslint-plugin-react": "^7.33.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^3.0.0",
"typescript": "~5.3.3"
+ },
+ "dependencies": {
+ "recharts": "^2.14.1"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2e743c8..f96400d 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7,6 +7,10 @@ settings:
importers:
.:
+ dependencies:
+ recharts:
+ specifier: ^2.14.1
+ version: 2.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
devDependencies:
'@eslint/js':
specifier: ^9.14.0
@@ -130,7 +134,7 @@ importers:
version: 3.2.1
eslint-plugin-import:
specifier: ^2.29.1
- version: 2.31.0(eslint@8.57.1)
+ version: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-jsx-a11y:
specifier: ^6.8.0
version: 6.10.2(eslint@8.57.1)
@@ -469,6 +473,10 @@ packages:
peerDependencies:
'@babel/core': ^7.0.0-0
+ '@babel/runtime@7.26.0':
+ resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==}
+ engines: {node: '>=6.9.0'}
+
'@babel/template@7.25.9':
resolution: {integrity: sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==}
engines: {node: '>=6.9.0'}
@@ -1493,6 +1501,33 @@ packages:
'@types/cors@2.8.17':
resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==}
+ '@types/d3-array@3.2.1':
+ resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
+
+ '@types/d3-color@3.1.3':
+ resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+
+ '@types/d3-ease@3.0.2':
+ resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
+
+ '@types/d3-interpolate@3.0.4':
+ resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+
+ '@types/d3-path@3.1.0':
+ resolution: {integrity: sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==}
+
+ '@types/d3-scale@4.0.8':
+ resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==}
+
+ '@types/d3-shape@3.1.6':
+ resolution: {integrity: sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==}
+
+ '@types/d3-time@3.0.4':
+ resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
+
+ '@types/d3-timer@3.0.2':
+ resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+
'@types/estree@1.0.6':
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
@@ -2143,6 +2178,10 @@ packages:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
+ clsx@2.1.1:
+ resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+ engines: {node: '>=6'}
+
co@4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
@@ -2279,6 +2318,50 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+ d3-array@3.2.4:
+ resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+ engines: {node: '>=12'}
+
+ d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+
+ d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+
+ d3-format@3.1.0:
+ resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
+ engines: {node: '>=12'}
+
+ d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+
+ d3-path@3.1.0:
+ resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+ engines: {node: '>=12'}
+
+ d3-scale@4.0.2:
+ resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+ engines: {node: '>=12'}
+
+ d3-shape@3.2.0:
+ resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+ engines: {node: '>=12'}
+
+ d3-time-format@4.1.0:
+ resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+ engines: {node: '>=12'}
+
+ d3-time@3.1.0:
+ resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+ engines: {node: '>=12'}
+
+ d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+
damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
@@ -2328,6 +2411,9 @@ packages:
supports-color:
optional: true
+ decimal.js-light@2.5.1:
+ resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+
dedent@1.5.3:
resolution: {integrity: sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==}
peerDependencies:
@@ -2405,6 +2491,9 @@ packages:
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
engines: {node: '>=6.0.0'}
+ dom-helpers@5.2.1:
+ resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
+
dompurify@3.2.1:
resolution: {integrity: sha512-NBHEsc0/kzRYQd+AY6HR6B/IgsqzBABrqJbpCDQII/OK6h7B7LXzweZTDsqSW2LkTRpoxf18YUP+YjGySk6B3w==}
@@ -2711,6 +2800,9 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
+ eventemitter3@4.0.7:
+ resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+
events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
@@ -2741,6 +2833,10 @@ packages:
fast-diff@1.3.0:
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
+ fast-equals@5.0.1:
+ resolution: {integrity: sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==}
+ engines: {node: '>=6.0.0'}
+
fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
@@ -3082,6 +3178,10 @@ packages:
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
engines: {node: '>= 0.4'}
+ internmap@2.0.3:
+ resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+ engines: {node: '>=12'}
+
ip-address@9.0.5:
resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==}
engines: {node: '>= 12'}
@@ -4363,6 +4463,18 @@ packages:
resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==}
engines: {node: '>=0.10.0'}
+ react-smooth@4.0.3:
+ resolution: {integrity: sha512-PyxIrra8WZWrMRFcCiJsZ+JqFaxEINAt+v/w++wQKQlmO99Eh3+JTLeKApdTsLX2roBdWYXqPsaS8sO4UmdzIg==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+
+ react-transition-group@4.4.5:
+ resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
+ peerDependencies:
+ react: '>=16.6.0'
+ react-dom: '>=16.6.0'
+
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@@ -4378,6 +4490,16 @@ packages:
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
engines: {node: '>=8.10.0'}
+ recharts-scale@0.4.5:
+ resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
+
+ recharts@2.14.1:
+ resolution: {integrity: sha512-xtWulflkA+/xu4/QClBdtZYN30dbvTHjxjkh5XTMrH/CQ3WGDDPHHa/LLKCbgoqz0z3UaSH2/blV1i6VNMeh1g==}
+ engines: {node: '>=14'}
+ peerDependencies:
+ react: ^16.0.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
+
reflect-metadata@0.2.2:
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
@@ -4385,6 +4507,9 @@ packages:
resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==}
engines: {node: '>= 0.4'}
+ regenerator-runtime@0.14.1:
+ resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
+
regexp.prototype.flags@1.5.3:
resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==}
engines: {node: '>= 0.4'}
@@ -4762,6 +4887,9 @@ packages:
through@2.3.8:
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
+ tiny-invariant@1.3.3:
+ resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
+
tmp@0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}
@@ -4992,6 +5120,9 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
+ victory-vendor@36.9.2:
+ resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
+
vite-plugin-svgr@4.3.0:
resolution: {integrity: sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==}
peerDependencies:
@@ -5432,6 +5563,10 @@ snapshots:
'@babel/core': 7.26.0
'@babel/helper-plugin-utils': 7.25.9
+ '@babel/runtime@7.26.0':
+ dependencies:
+ regenerator-runtime: 0.14.1
+
'@babel/template@7.25.9':
dependencies:
'@babel/code-frame': 7.26.2
@@ -6663,6 +6798,30 @@ snapshots:
dependencies:
'@types/node': 20.17.6
+ '@types/d3-array@3.2.1': {}
+
+ '@types/d3-color@3.1.3': {}
+
+ '@types/d3-ease@3.0.2': {}
+
+ '@types/d3-interpolate@3.0.4':
+ dependencies:
+ '@types/d3-color': 3.1.3
+
+ '@types/d3-path@3.1.0': {}
+
+ '@types/d3-scale@4.0.8':
+ dependencies:
+ '@types/d3-time': 3.0.4
+
+ '@types/d3-shape@3.1.6':
+ dependencies:
+ '@types/d3-path': 3.1.0
+
+ '@types/d3-time@3.0.4': {}
+
+ '@types/d3-timer@3.0.2': {}
+
'@types/estree@1.0.6': {}
'@types/express-serve-static-core@5.0.1':
@@ -7219,7 +7378,7 @@ snapshots:
axios@1.7.7:
dependencies:
- follow-redirects: 1.15.9
+ follow-redirects: 1.15.9(debug@4.3.7)
form-data: 4.0.1
proxy-from-env: 1.1.0
transitivePeerDependencies:
@@ -7474,6 +7633,8 @@ snapshots:
clone@1.0.4: {}
+ clsx@2.1.1: {}
+
co@4.6.0: {}
code-block-writer@12.0.0: {}
@@ -7601,6 +7762,44 @@ snapshots:
csstype@3.1.3: {}
+ d3-array@3.2.4:
+ dependencies:
+ internmap: 2.0.3
+
+ d3-color@3.1.0: {}
+
+ d3-ease@3.0.1: {}
+
+ d3-format@3.1.0: {}
+
+ d3-interpolate@3.0.1:
+ dependencies:
+ d3-color: 3.1.0
+
+ d3-path@3.1.0: {}
+
+ d3-scale@4.0.2:
+ dependencies:
+ d3-array: 3.2.4
+ d3-format: 3.1.0
+ d3-interpolate: 3.0.1
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+
+ d3-shape@3.2.0:
+ dependencies:
+ d3-path: 3.1.0
+
+ d3-time-format@4.1.0:
+ dependencies:
+ d3-time: 3.1.0
+
+ d3-time@3.1.0:
+ dependencies:
+ d3-array: 3.2.4
+
+ d3-timer@3.0.1: {}
+
damerau-levenshtein@1.0.8: {}
data-view-buffer@1.0.1:
@@ -7637,6 +7836,8 @@ snapshots:
dependencies:
ms: 2.1.3
+ decimal.js-light@2.5.1: {}
+
dedent@1.5.3: {}
deep-is@0.1.4: {}
@@ -7694,6 +7895,11 @@ snapshots:
dependencies:
esutils: 2.0.3
+ dom-helpers@5.2.1:
+ dependencies:
+ '@babel/runtime': 7.26.0
+ csstype: 3.1.3
+
dompurify@3.2.1:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -8002,15 +8208,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.0(eslint-import-resolver-node@0.3.9)(eslint@8.57.1):
- dependencies:
- debug: 3.2.7
- optionalDependencies:
- eslint: 8.57.1
- eslint-import-resolver-node: 0.3.9
- transitivePeerDependencies:
- - supports-color
-
eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1):
dependencies:
'@rtsao/scc': 1.1.0
@@ -8040,33 +8237,6 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
- eslint-plugin-import@2.31.0(eslint@8.57.1):
- dependencies:
- '@rtsao/scc': 1.1.0
- array-includes: 3.1.8
- array.prototype.findlastindex: 1.2.5
- array.prototype.flat: 1.3.2
- array.prototype.flatmap: 1.3.2
- debug: 3.2.7
- doctrine: 2.1.0
- eslint: 8.57.1
- eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(eslint-import-resolver-node@0.3.9)(eslint@8.57.1)
- hasown: 2.0.2
- is-core-module: 2.15.1
- is-glob: 4.0.3
- minimatch: 3.1.2
- object.fromentries: 2.0.8
- object.groupby: 1.0.3
- object.values: 1.2.0
- semver: 6.3.1
- string.prototype.trimend: 1.0.8
- tsconfig-paths: 3.15.0
- transitivePeerDependencies:
- - eslint-import-resolver-typescript
- - eslint-import-resolver-webpack
- - supports-color
-
eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1):
dependencies:
aria-query: 5.3.2
@@ -8206,6 +8376,8 @@ snapshots:
etag@1.8.1: {}
+ eventemitter3@4.0.7: {}
+
events@3.3.0: {}
execa@5.1.1:
@@ -8276,6 +8448,8 @@ snapshots:
fast-diff@1.3.0: {}
+ fast-equals@5.0.1: {}
+
fast-fifo@1.3.2: {}
fast-glob@3.3.2:
@@ -8363,8 +8537,6 @@ snapshots:
flatted@3.3.1: {}
- follow-redirects@1.15.9: {}
-
follow-redirects@1.15.9(debug@4.3.7):
optionalDependencies:
debug: 4.3.7
@@ -8671,6 +8843,8 @@ snapshots:
hasown: 2.0.2
side-channel: 1.0.6
+ internmap@2.0.3: {}
+
ip-address@9.0.5:
dependencies:
jsbn: 1.1.0
@@ -10091,6 +10265,23 @@ snapshots:
react-refresh@0.14.2: {}
+ react-smooth@4.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ fast-equals: 5.0.1
+ prop-types: 15.8.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-transition-group: 4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+
+ react-transition-group@4.4.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@babel/runtime': 7.26.0
+ dom-helpers: 5.2.1
+ loose-envify: 1.4.0
+ prop-types: 15.8.1
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
react@18.3.1:
dependencies:
loose-envify: 1.4.0
@@ -10115,6 +10306,23 @@ snapshots:
dependencies:
picomatch: 2.3.1
+ recharts-scale@0.4.5:
+ dependencies:
+ decimal.js-light: 2.5.1
+
+ recharts@2.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ clsx: 2.1.1
+ eventemitter3: 4.0.7
+ lodash: 4.17.21
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ react-is: 18.3.1
+ react-smooth: 4.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ recharts-scale: 0.4.5
+ tiny-invariant: 1.3.3
+ victory-vendor: 36.9.2
+
reflect-metadata@0.2.2: {}
reflect.getprototypeof@1.0.6:
@@ -10127,6 +10335,8 @@ snapshots:
globalthis: 1.0.4
which-builtin-type: 1.1.4
+ regenerator-runtime@0.14.1: {}
+
regexp.prototype.flags@1.5.3:
dependencies:
call-bind: 1.0.7
@@ -10600,6 +10810,8 @@ snapshots:
through@2.3.8: {}
+ tiny-invariant@1.3.3: {}
+
tmp@0.0.33:
dependencies:
os-tmpdir: 1.0.2
@@ -10828,6 +11040,23 @@ snapshots:
vary@1.1.2: {}
+ victory-vendor@36.9.2:
+ dependencies:
+ '@types/d3-array': 3.2.1
+ '@types/d3-ease': 3.0.2
+ '@types/d3-interpolate': 3.0.4
+ '@types/d3-scale': 4.0.8
+ '@types/d3-shape': 3.1.6
+ '@types/d3-time': 3.0.4
+ '@types/d3-timer': 3.0.2
+ d3-array: 3.2.4
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-scale: 4.0.2
+ d3-shape: 3.2.0
+ d3-time: 3.1.0
+ d3-timer: 3.0.1
+
vite-plugin-svgr@4.3.0(rollup@4.24.3)(typescript@5.3.3)(vite@5.4.10(@types/node@20.17.6)(lightningcss@1.25.1)(terser@5.36.0)):
dependencies:
'@rollup/pluginutils': 5.1.3(rollup@4.24.3)