From 44fd92850df10b8dd618ea16552f0158a9c46d32 Mon Sep 17 00:00:00 2001 From: MarkLo Date: Thu, 20 Nov 2025 23:19:22 +0800 Subject: [PATCH] --- backend/app/models/schemas.py | 22 ++ backend/app/services/price_service.py | 102 ++++++ backend/app/services/trading_service.py | 16 + frontend/app/analysis/page.tsx | 13 + frontend/components/analysis/AnalysisForm.tsx | 88 ++++- frontend/components/analysis/PriceChart.tsx | 194 +++++++++++ frontend/components/ui/checkbox.tsx | 32 ++ frontend/lib/types.ts | 26 +- frontend/package.json | 2 + frontend/pnpm-lock.yaml | 329 ++++++++++++++++++ 10 files changed, 817 insertions(+), 7 deletions(-) create mode 100644 backend/app/services/price_service.py create mode 100644 frontend/components/analysis/PriceChart.tsx create mode 100644 frontend/components/ui/checkbox.tsx diff --git a/backend/app/models/schemas.py b/backend/app/models/schemas.py index 50783cb6..6c16dd4b 100644 --- a/backend/app/models/schemas.py +++ b/backend/app/models/schemas.py @@ -30,6 +30,26 @@ class AnalysisRequest(BaseModel): ) +class PriceData(BaseModel): + """Stock price data model""" + Date: str + Open: float + High: float + Low: float + Close: float + Volume: int + + +class PriceStats(BaseModel): + """Price statistics model""" + growth_rate: float = Field(..., description="Price growth rate in percentage") + duration_days: int = Field(..., description="Data duration in days") + start_date: str + end_date: str + start_price: float + end_price: float + + class AnalysisResponse(BaseModel): """Response model for trading analysis""" status: str = Field(..., description="Analysis status (success, error, processing)") @@ -38,6 +58,8 @@ class AnalysisResponse(BaseModel): decision: Optional[Dict[str, Any]] = Field(None, description="Trading decision details") reports: Optional[Dict[str, Any]] = Field(None, description="Analysis reports from different teams") error: Optional[str] = Field(None, description="Error message if analysis failed") + price_data: Optional[List[PriceData]] = Field(None, description="Historical price data") + price_stats: Optional[PriceStats] = Field(None, description="Price statistics") class ConfigResponse(BaseModel): diff --git a/backend/app/services/price_service.py b/backend/app/services/price_service.py new file mode 100644 index 00000000..b71fc39a --- /dev/null +++ b/backend/app/services/price_service.py @@ -0,0 +1,102 @@ +""" +Price data service for loading and processing stock price data +""" +import pandas as pd +from pathlib import Path +from typing import List, Dict, Any, Optional +import logging + +logger = logging.getLogger(__name__) + + +class PriceService: + """Service for loading and processing price data from data_cache""" + + @staticmethod + def load_price_data(ticker: str, data_cache_dir: str) -> Optional[pd.DataFrame]: + """ + Load price data from data_cache CSV files + + Args: + ticker: Stock ticker symbol + data_cache_dir: Path to data cache directory + + Returns: + DataFrame with price data or None if not found + """ + try: + cache_path = Path(data_cache_dir) + + # Search for {ticker}-YFin-data-*.csv files + csv_files = list(cache_path.glob(f"{ticker}-YFin-data-*.csv")) + + if not csv_files: + logger.warning(f"No price data found for {ticker} in {data_cache_dir}") + return None + + # Use the most recent file + latest_file = max(csv_files, key=lambda p: p.stat().st_mtime) + logger.info(f"Loading price data from {latest_file}") + + df = pd.read_csv(latest_file) + df['Date'] = pd.to_datetime(df['Date']) + + return df.sort_values('Date') + + except Exception as e: + logger.error(f"Error loading price data for {ticker}: {e}") + return None + + @staticmethod + def calculate_stats(df: pd.DataFrame) -> Dict[str, Any]: + """ + Calculate price statistics + + Args: + df: DataFrame with price data + + Returns: + Dictionary with statistics + """ + start_price = float(df.iloc[0]['Close']) + end_price = float(df.iloc[-1]['Close']) + growth_rate = ((end_price - start_price) / start_price) * 100 + duration_days = (df.iloc[-1]['Date'] - df.iloc[0]['Date']).days + + return { + "growth_rate": round(growth_rate, 2), + "duration_days": int(duration_days), + "start_date": df.iloc[0]['Date'].strftime('%Y-%m-%d'), + "end_date": df.iloc[-1]['Date'].strftime('%Y-%m-%d'), + "start_price": round(start_price, 2), + "end_price": round(end_price, 2), + } + + @staticmethod + def prepare_chart_data(df: pd.DataFrame, limit: int = 365) -> List[Dict[str, Any]]: + """ + Prepare price data for charting (limit to recent data) + + Args: + df: DataFrame with price data + limit: Maximum number of data points to return + + Returns: + List of dictionaries with price data + """ + # Get recent data + recent_df = df.tail(limit) + + # Convert to list of dicts + data = [] + for _, row in recent_df.iterrows(): + data.append({ + "Date": row['Date'].strftime('%Y-%m-%d'), + "Open": round(float(row['Open']), 2), + "High": round(float(row['High']), 2), + "Low": round(float(row['Low']), 2), + "Close": round(float(row['Close']), 2), + "Volume": int(row['Volume']), + }) + + return data diff --git a/backend/app/services/trading_service.py b/backend/app/services/trading_service.py index d3a2cd9e..20850d96 100644 --- a/backend/app/services/trading_service.py +++ b/backend/app/services/trading_service.py @@ -111,12 +111,28 @@ class TradingService: "risk_debate_state": final_state.get("risk_debate_state"), } + # Load price data + from backend.app.services.price_service import PriceService + price_data = None + price_stats = None + + try: + price_df = PriceService.load_price_data(ticker, config.get("data_cache_dir")) + if price_df is not None: + price_data = PriceService.prepare_chart_data(price_df) + price_stats = PriceService.calculate_stats(price_df) + logger.info(f"Loaded {len(price_data)} price data points for {ticker}") + except Exception as e: + logger.warning(f"Could not load price data for {ticker}: {e}") + return { "status": "success", "ticker": ticker, "analysis_date": analysis_date, "decision": decision, "reports": reports, + "price_data": price_data, + "price_stats": price_stats, } finally: diff --git a/frontend/app/analysis/page.tsx b/frontend/app/analysis/page.tsx index c30378b1..32f6e80b 100644 --- a/frontend/app/analysis/page.tsx +++ b/frontend/app/analysis/page.tsx @@ -7,6 +7,7 @@ import { useState } from "react"; import { AnalysisForm } from "@/components/analysis/AnalysisForm"; import { TradingDecision } from "@/components/analysis/TradingDecision"; import { AnalystReport } from "@/components/analysis/AnalystReport"; +import { PriceChart } from "@/components/analysis/PriceChart"; import { LoadingSpinner } from "@/components/shared/LoadingSpinner"; import { useAnalysis } from "@/hooks/useAnalysis"; import type { AnalysisRequest } from "@/lib/types"; @@ -48,7 +49,19 @@ export default function AnalysisPage() { {result && !loading && (
+ {/* 價格圖表 */} + {result.price_data && result.price_stats && ( + + )} + + {/* 交易決策 */} + + {/* 分析報告 */} {result.reports && }
)} diff --git a/frontend/components/analysis/AnalysisForm.tsx b/frontend/components/analysis/AnalysisForm.tsx index 50a2ec95..eb28f537 100644 --- a/frontend/components/analysis/AnalysisForm.tsx +++ b/frontend/components/analysis/AnalysisForm.tsx @@ -20,6 +20,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, @@ -33,6 +34,7 @@ import type { AnalysisRequest } from "@/lib/types"; const formSchema = z.object({ ticker: z.string().min(1, "股票代碼為必填").max(10), analysis_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "日期格式必須為 YYYY-MM-DD"), + analysts: z.array(z.string()).min(1, "請至少選擇一位分析師"), research_depth: z.number().min(1).max(5), deep_think_llm: z.string(), quick_think_llm: z.string(), @@ -48,12 +50,20 @@ interface AnalysisFormProps { loading?: boolean; } +const ANALYSTS = [ + { value: "market", label: "市場分析師" }, + { value: "social", label: "社群媒體分析師" }, + { value: "news", label: "新聞分析師" }, + { value: "fundamentals", label: "基本面分析師" }, +]; + export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { ticker: "NVDA", analysis_date: format(new Date(), "yyyy-MM-dd"), + analysts: ["market", "social", "news", "fundamentals"], // 預設全選 research_depth: 1, deep_think_llm: "gpt-4o-mini", quick_think_llm: "gpt-4o-mini", @@ -63,10 +73,19 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) { }, }); + // 全選/取消全選 + const toggleSelectAll = () => { + const currentAnalysts = form.getValues("analysts"); + if (currentAnalysts.length === ANALYSTS.length) { + form.setValue("analysts", []); + } else { + form.setValue("analysts", ANALYSTS.map(a => a.value)); + } + }; + function handleSubmit(values: z.infer) { const request: AnalysisRequest = { ...values, - analysts: ["market", "sentiment", "news", "fundamentals"], }; onSubmit(request); } @@ -83,6 +102,65 @@ export function AnalysisForm({ onSubmit, loading = false }: AnalysisFormProps) {
+ {/* 分析師選擇區塊 */} +
+
+ 分析師團隊 + +
+ ( + +
+ {ANALYSTS.map((analyst) => ( + { + return ( + + + { + return checked + ? field.onChange([...field.value, analyst.value]) + : field.onChange( + field.value?.filter( + (value: string) => value !== analyst.value + ) + ); + }} + /> + + + {analyst.label} + + + ); + }} + /> + ))} +
+ +
+ )} + /> +
+ - 淺層 - 快速研究,較少的辯論和策略討論 - 中等 - 中等程度,適度的辯論和策略討論 - 深層 - 全面研究,深入的辯論和策略討論 + 1 - 快速 + 2 - 標準 + 3 - 詳盡 + 4 - 深入 + 5 - 全面 diff --git a/frontend/components/analysis/PriceChart.tsx b/frontend/components/analysis/PriceChart.tsx new file mode 100644 index 00000000..3ed35385 --- /dev/null +++ b/frontend/components/analysis/PriceChart.tsx @@ -0,0 +1,194 @@ +"use client"; + +import { useState } from "react"; +import { + LineChart, + Line, + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + ResponsiveContainer, + ComposedChart, + Area, +} from "recharts"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import type { PriceData, PriceStats } from "@/lib/types"; + +interface PriceChartProps { + priceData: PriceData[]; + priceStats: PriceStats; + ticker: string; +} + +export function PriceChart({ priceData, priceStats, ticker }: PriceChartProps) { + const [chartType, setChartType] = useState<"line" | "candlestick">("line"); + + // 格式化數字 + const formatNumber = (num: number) => { + return num.toLocaleString('zh-TW', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + }; + + // 格式化日期(只顯示月-日) + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return `${date.getMonth() + 1}/${date.getDate()}`; + }; + + return ( + + +
+ {ticker} 價格走勢 + setChartType(v as "line" | "candlestick")}> + + 折線圖 + K線圖 + + +
+ + {/* 統計資訊 */} +
+
+

增長率

+

= 0 ? 'text-green-600' : 'text-red-600'}`}> + {priceStats.growth_rate >= 0 ? '+' : ''}{priceStats.growth_rate}% +

+
+
+

時長

+

{priceStats.duration_days} 天

+
+
+

起始價格

+

${formatNumber(priceStats.start_price)}

+

{priceStats.start_date}

+
+
+

結束價格

+

${formatNumber(priceStats.end_price)}

+

{priceStats.end_date}

+
+
+
+ + + {/* 價格圖表 */} +
+

價格走勢

+ + {chartType === "line" ? ( + + + + `$${value.toFixed(0)}`} + /> + [`$${formatNumber(value)}`, '收盤價']} + labelFormatter={(label) => `日期: ${label}`} + /> + + + + ) : ( + + + + `$${value.toFixed(0)}`} + /> + [`$${formatNumber(value)}`, name]} + labelFormatter={(label) => `日期: ${label}`} + /> + + + + + + + )} + +
+ + {/* 交易量圖表 */} +
+

交易量

+ + + + + { + if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`; + if (value >= 1000) return `${(value / 1000).toFixed(1)}K`; + return value.toString(); + }} + /> + [value.toLocaleString(), '交易量']} + labelFormatter={(label) => `日期: ${label}`} + /> + + + +
+
+
+ ); +} diff --git a/frontend/components/ui/checkbox.tsx b/frontend/components/ui/checkbox.tsx new file mode 100644 index 00000000..cb0b07b4 --- /dev/null +++ b/frontend/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Checkbox({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts index 6d6072a0..b8a1c799 100644 --- a/frontend/lib/types.ts +++ b/frontend/lib/types.ts @@ -16,13 +16,33 @@ export interface AnalysisRequest { alpha_vantage_api_key?: string; } +export interface PriceData { + Date: string; + Open: number; + High: number; + Low: number; + Close: number; + Volume: number; +} + +export interface PriceStats { + growth_rate: number; + duration_days: number; + start_date: string; + end_date: string; + start_price: number; + end_price: number; +} + export interface AnalysisResponse { - status: "success" | "error" | "processing"; + status: string; ticker: string; analysis_date: string; - decision?: Decision; - reports?: Reports; + decision?: any; + reports?: any; error?: string; + price_data?: PriceData[]; + price_stats?: PriceStats; } export interface Decision { diff --git a/frontend/package.json b/frontend/package.json index c4304162..20dd0711 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-label": "^2.1.8", "@radix-ui/react-select": "^2.2.6", @@ -24,6 +25,7 @@ "react": "19.2.0", "react-dom": "19.2.0", "react-hook-form": "^7.66.1", + "recharts": "^3.4.1", "tailwind-merge": "^3.4.0", "zod": "^4.1.12" }, diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index b40da588..1587b2f4 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@hookform/resolvers': specifier: ^5.2.2 version: 5.2.2(react-hook-form@7.66.1(react@19.2.0)) + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) '@radix-ui/react-dialog': specifier: ^1.1.15 version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -53,6 +56,9 @@ importers: react-hook-form: specifier: ^7.66.1 version: 7.66.1(react@19.2.0) + recharts: + specifier: ^3.4.1 + version: 3.4.1(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react-is@16.13.1)(react@19.2.0)(redux@5.0.1) tailwind-merge: specifier: ^3.4.0 version: 3.4.0 @@ -492,6 +498,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: @@ -812,9 +831,23 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@reduxjs/toolkit@2.10.1': + resolution: {integrity: sha512-/U17EXQ9Do9Yx4DlNGU6eVNfZvFJfYpUtRRdLf19PbPjdWBxNlxGZXywQZ1p1Nz8nMkWplTI7iD/23m07nolDA==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + '@standard-schema/utils@0.3.0': resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} @@ -912,6 +945,33 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@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.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@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.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -932,6 +992,9 @@ packages: '@types/react@19.2.6': resolution: {integrity: sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@typescript-eslint/eslint-plugin@8.47.0': resolution: {integrity: sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1253,6 +1316,50 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + 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==} @@ -1288,6 +1395,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1360,6 +1470,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.42.0: + resolution: {integrity: sha512-SLHIyY7VfDJBM8clz4+T2oquwTQxEzu263AyhVK4jREOAwJ+8eebaa4wM3nlvnAqhDrMm2EsA6hWHaQsMPQ1nA==} + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1484,6 +1597,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1657,6 +1773,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1669,6 +1788,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -2113,6 +2236,18 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -2147,6 +2282,22 @@ packages: resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} engines: {node: '>=0.10.0'} + recharts@3.4.1: + resolution: {integrity: sha512-35kYg6JoOgwq8sE4rhYkVWwa6aAIgOtT+Ob0gitnShjwUwZmhrmy7Jco/5kJNF4PnLXgt9Hwq+geEMS+WrjU1g==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -2155,6 +2306,9 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2315,6 +2469,9 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -2409,6 +2566,14 @@ packages: '@types/react': optional: true + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -2829,6 +2994,22 @@ snapshots: '@types/react': 19.2.6 '@types/react-dom': 19.2.3(@types/react@19.2.6) + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.6)(react@19.2.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.6)(react@19.2.0) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.6)(react@19.2.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.6)(react@19.2.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.6)(react@19.2.0) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.6 + '@types/react-dom': 19.2.3(@types/react@19.2.6) + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.6))(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.6)(react@19.2.0) @@ -3124,8 +3305,22 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@reduxjs/toolkit@2.10.1(react-redux@9.2.0(@types/react@19.2.6)(react@19.2.0)(redux@5.0.1))(react@19.2.0)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@standard-schema/utils': 0.3.0 + immer: 10.2.0 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.0 + react-redux: 9.2.0(@types/react@19.2.6)(react@19.2.0)(redux@5.0.1) + '@rtsao/scc@1.1.0': {} + '@standard-schema/spec@1.0.0': {} + '@standard-schema/utils@0.3.0': {} '@swc/helpers@0.5.15': @@ -3206,6 +3401,30 @@ snapshots: tslib: 2.8.1 optional: true + '@types/d3-array@3.2.2': {} + + '@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.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/estree@1.0.8': {} '@types/json-schema@7.0.15': {} @@ -3224,6 +3443,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/use-sync-external-store@0.0.6': {} + '@typescript-eslint/eslint-plugin@8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -3575,6 +3796,44 @@ snapshots: csstype@3.2.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.2: @@ -3605,6 +3864,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + deep-is@0.1.4: {} define-data-property@1.1.4: @@ -3745,6 +4006,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.42.0: {} + escalade@3.2.0: {} escape-string-regexp@4.0.0: {} @@ -3952,6 +4215,8 @@ snapshots: esutils@2.0.3: {} + eventemitter3@5.0.1: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -4118,6 +4383,8 @@ snapshots: ignore@7.0.5: {} + immer@10.2.0: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -4131,6 +4398,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + internmap@2.0.3: {} + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -4550,6 +4819,15 @@ snapshots: react-is@16.13.1: {} + react-redux@9.2.0(@types/react@19.2.6)(react@19.2.0)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.0 + use-sync-external-store: 1.6.0(react@19.2.0) + optionalDependencies: + '@types/react': 19.2.6 + redux: 5.0.1 + react-remove-scroll-bar@2.3.8(@types/react@19.2.6)(react@19.2.0): dependencies: react: 19.2.0 @@ -4579,6 +4857,32 @@ snapshots: react@19.2.0: {} + recharts@3.4.1(@types/react@19.2.6)(react-dom@19.2.0(react@19.2.0))(react-is@16.13.1)(react@19.2.0)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.10.1(react-redux@9.2.0(@types/react@19.2.6)(react@19.2.0)(redux@5.0.1))(react@19.2.0) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.42.0 + eventemitter3: 5.0.1 + immer: 10.2.0 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + react-is: 16.13.1 + react-redux: 9.2.0(@types/react@19.2.6)(react@19.2.0)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.0) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -4599,6 +4903,8 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + reselect@5.1.1: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -4816,6 +5122,8 @@ snapshots: tapable@2.3.0: {} + tiny-invariant@1.3.3: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -4948,6 +5256,27 @@ snapshots: optionalDependencies: '@types/react': 19.2.6 + use-sync-external-store@1.6.0(react@19.2.0): + dependencies: + react: 19.2.0 + + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@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 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0