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)