From b0612aa9bc7e7ca0e69d799d2ee2ee6e8a7c049d Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Mon, 15 Sep 2025 18:01:06 +0800 Subject: [PATCH 01/10] chore: ignore .vite --- frontend/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/.gitignore b/frontend/.gitignore index 3ab038692..054200b12 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -21,3 +21,4 @@ dist-ssr # custom !lib .react-router +.vite From db14ba7623474db4c3b1b1edbbcff77a5060e7fc Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Mon, 15 Sep 2025 18:03:38 +0800 Subject: [PATCH 02/10] style: price info place end --- frontend/src/components/valuecell/menus/stock-menus.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/valuecell/menus/stock-menus.tsx b/frontend/src/components/valuecell/menus/stock-menus.tsx index d78ffc104..83652a16f 100644 --- a/frontend/src/components/valuecell/menus/stock-menus.tsx +++ b/frontend/src/components/valuecell/menus/stock-menus.tsx @@ -120,7 +120,7 @@ function StockMenuListItem({ - - - ); -} - -export default Home; diff --git a/frontend/src/app/home/_layout.tsx b/frontend/src/app/home/_layout.tsx new file mode 100644 index 000000000..c86ed3da0 --- /dev/null +++ b/frontend/src/app/home/_layout.tsx @@ -0,0 +1,47 @@ +import { Plus } from "lucide-react"; +import { Outlet } from "react-router"; +import { Button } from "@/components/ui/button"; +import { + StockMenu, + StockMenuContent, + StockMenuGroup, + StockMenuGroupHeader, + StockMenuHeader, + StockMenuListItem, +} from "@/components/valuecell/menus/stock-menus"; +import { stockData } from "@/mock/stock-data"; + +export default function HomeLayout() { + return ( +
+
+ +
+ + +
+ ); +} diff --git a/frontend/src/app/_home/components/agent-recommend-list.tsx b/frontend/src/app/home/components/agent-recommend-list.tsx similarity index 100% rename from frontend/src/app/_home/components/agent-recommend-list.tsx rename to frontend/src/app/home/components/agent-recommend-list.tsx diff --git a/frontend/src/app/_home/components/agent-suggestions-list.tsx b/frontend/src/app/home/components/agent-suggestions-list.tsx similarity index 100% rename from frontend/src/app/_home/components/agent-suggestions-list.tsx rename to frontend/src/app/home/components/agent-suggestions-list.tsx diff --git a/frontend/src/app/_home/components/index.tsx b/frontend/src/app/home/components/index.tsx similarity index 100% rename from frontend/src/app/_home/components/index.tsx rename to frontend/src/app/home/components/index.tsx diff --git a/frontend/src/app/_home/components/sparkline-stock-list.tsx b/frontend/src/app/home/components/sparkline-stock-list.tsx similarity index 100% rename from frontend/src/app/_home/components/sparkline-stock-list.tsx rename to frontend/src/app/home/components/sparkline-stock-list.tsx diff --git a/frontend/src/app/home/home.tsx b/frontend/src/app/home/home.tsx new file mode 100644 index 000000000..e1abdcadc --- /dev/null +++ b/frontend/src/app/home/home.tsx @@ -0,0 +1,40 @@ +import { agentRecommendations, agentSuggestions } from "@/mock/agent-data"; +import { sparklineStockData } from "@/mock/stock-data"; +import { + AgentRecommendList, + AgentSuggestionsList, + SparklineStockList, +} from "./components"; + +function Home() { + const handleAgentClick = (agentId: string, title: string) => { + console.log(`Agent clicked: ${title} (${agentId})`); + }; + + return ( +
+

👋 Welcome to ValueCell !

+ + + + ({ + ...suggestion, + onClick: () => handleAgentClick(suggestion.id, suggestion.title), + }))} + /> + + ({ + ...recommendation, + onClick: () => + handleAgentClick(recommendation.id, recommendation.title), + }))} + /> +
+ ); +} + +export default Home; diff --git a/frontend/src/app/home/stock.tsx b/frontend/src/app/home/stock.tsx new file mode 100644 index 000000000..f0a26b59a --- /dev/null +++ b/frontend/src/app/home/stock.tsx @@ -0,0 +1,9 @@ +import { useParams } from "react-router"; + +function Stock() { + const { stockId } = useParams(); + + return
Stock {stockId}
; +} + +export default Stock; diff --git a/frontend/src/components/valuecell/app-sidebar.tsx b/frontend/src/components/valuecell/app-sidebar.tsx index 71067d514..6eb2f3309 100644 --- a/frontend/src/components/valuecell/app-sidebar.tsx +++ b/frontend/src/components/valuecell/app-sidebar.tsx @@ -1,4 +1,10 @@ -import { type FC, type HTMLAttributes, type ReactNode, useMemo } from "react"; +import { + type FC, + type HTMLAttributes, + memo, + type ReactNode, + useMemo, +} from "react"; import { NavLink, useLocation } from "react-router"; import { BookOpen, ChartBarVertical, Logo, Setting, User } from "@/assets/svg"; import { Separator } from "@/components/ui/separator"; @@ -193,4 +199,4 @@ const AppSidebar: FC = () => { ); }; -export default AppSidebar; +export default memo(AppSidebar); diff --git a/frontend/src/components/valuecell/menus/stock-menus.tsx b/frontend/src/components/valuecell/menus/stock-menus.tsx index 83652a16f..23f35e828 100644 --- a/frontend/src/components/valuecell/menus/stock-menus.tsx +++ b/frontend/src/components/valuecell/menus/stock-menus.tsx @@ -1,4 +1,5 @@ import type { ScrollAreaProps } from "@radix-ui/react-scroll-area"; +import { Link, type LinkProps } from "react-router"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { ScrollArea } from "@/components/ui/scroll-area"; import { cn, formatChange, formatPrice, getChangeType } from "@/lib/utils"; @@ -40,10 +41,8 @@ interface StockMenuGroupHeaderProps children: React.ReactNode; } -interface StockMenuListItemProps - extends Omit, "onClick"> { +interface StockMenuListItemProps extends LinkProps { stock: Stock; - onClick?: (stock: Stock) => void; } function StockMenuHeader({ @@ -117,13 +116,12 @@ function StockMenuListItem({ const changeType = getChangeType(stock.changePercent); return ( - + ); } diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index dad9f7e84..a404f1c81 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -1,9 +1,13 @@ -import { type RouteConfig, route } from "@react-router/dev/routes"; -import { flatRoutes } from "@react-router/fs-routes"; +import { + index, + layout, + type RouteConfig, + route, +} from "@react-router/dev/routes"; export default [ - route("/", "app/_home/route.tsx"), - ...(await flatRoutes({ - rootDirectory: "app", - })), + layout("app/home/_layout.tsx", [ + index("app/home/home.tsx"), + route("/stock/:stockId", "app/home/stock.tsx"), + ]), ] satisfies RouteConfig; From 8da10b6df6b7b4fe2226c5c9f7f067f5a0451547 Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Tue, 16 Sep 2025 01:18:19 +0800 Subject: [PATCH 04/10] feat: add detailed stock chart & refactor imports --- .../home/components/sparkline-stock-list.tsx | 2 +- frontend/src/app/home/stock.tsx | 123 ++++++++- .../valuecell/charts/mini-sparkline.tsx | 2 +- .../components/valuecell/charts/sparkline.tsx | 259 ++++++++++++++++++ frontend/src/mock/agent-data.tsx | 4 +- frontend/src/mock/stock-data.ts | 2 +- 6 files changed, 386 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/valuecell/charts/sparkline.tsx diff --git a/frontend/src/app/home/components/sparkline-stock-list.tsx b/frontend/src/app/home/components/sparkline-stock-list.tsx index 392b74144..f829bd1cb 100644 --- a/frontend/src/app/home/components/sparkline-stock-list.tsx +++ b/frontend/src/app/home/components/sparkline-stock-list.tsx @@ -1,4 +1,4 @@ -import { MiniSparkline } from "@valuecell/charts/mini-sparkline"; +import MiniSparkline from "@valuecell/charts/mini-sparkline"; import { cn, formatChange, formatPrice, getChangeType } from "@/lib/utils"; import type { StockChangeType } from "@/types/stock"; diff --git a/frontend/src/app/home/stock.tsx b/frontend/src/app/home/stock.tsx index f0a26b59a..e82c51c26 100644 --- a/frontend/src/app/home/stock.tsx +++ b/frontend/src/app/home/stock.tsx @@ -1,9 +1,130 @@ +import { useMemo } from "react"; import { useParams } from "react-router"; +import Sparkline from "@/components/valuecell/charts/sparkline"; +import { stockData } from "@/mock/stock-data"; + +// 生成历史价格数据 +function generateHistoricalData(basePrice: number, days: number = 30) { + const data = []; + const now = new Date(); + + for (let i = days; i >= 0; i--) { + const date = new Date(now); + date.setDate(date.getDate() - i); + + // 模拟价格波动 (±5%) + const variation = (Math.random() - 0.5) * 0.1; + const price = basePrice * (1 + variation * (i / days)); // 添加趋势 + + data.push({ + timestamp: date.toISOString(), + value: Math.max(0, price), + }); + } + + return data; +} function Stock() { const { stockId } = useParams(); - return
Stock {stockId}
; + // 从 mock 数据中查找股票信息 + const stockInfo = useMemo(() => { + for (const group of stockData) { + const stock = group.stocks.find((s) => s.symbol === stockId); + if (stock) return stock; + } + return null; + }, [stockId]); + + // 生成历史价格数据 + const chartData = useMemo(() => { + if (!stockInfo) return []; + return generateHistoricalData(stockInfo.price, 60); // 60天历史数据 + }, [stockInfo]); + + if (!stockInfo) { + return ( +
+
未找到股票 {stockId}
+
+ ); + } + + const isPositive = stockInfo.changePercent >= 0; + const chartColor = isPositive ? "#41C3A9" : "#EF4444"; + const gradientColors: [string, string] = isPositive + ? ["rgba(65, 195, 169, 0.6)", "rgba(65, 195, 169, 0)"] + : ["rgba(239, 68, 68, 0.6)", "rgba(239, 68, 68, 0)"]; + + return ( +
+ {/* 股票信息头部 */} +
+
+

{stockInfo.symbol}

+
+
+ {stockInfo.currency} + {stockInfo.price.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} +
+
+ {isPositive ? "+" : ""} + {stockInfo.changePercent.toFixed(2)}% +
+
+
+
{stockInfo.companyName}
+
+ + {/* 价格图表 */} +
+
+

价格走势

+

过去60天的价格变化

+
+ + { + const date = new Date(timestamp); + const formatDate = date.toLocaleDateString("zh-CN", { + month: "short", + day: "numeric", + }); + const formatTime = date.toLocaleTimeString("zh-CN", { + hour: "2-digit", + minute: "2-digit", + }); + + return ` +
+ ${formatDate} ${formatTime} +
+
+ ${stockInfo.currency}${value.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })} +
+ `; + }} + /> +
+
+ ); } export default Stock; diff --git a/frontend/src/components/valuecell/charts/mini-sparkline.tsx b/frontend/src/components/valuecell/charts/mini-sparkline.tsx index 027d16a1b..428ab10ce 100644 --- a/frontend/src/components/valuecell/charts/mini-sparkline.tsx +++ b/frontend/src/components/valuecell/charts/mini-sparkline.tsx @@ -17,7 +17,7 @@ interface MiniSparklineProps { className?: string; } -export function MiniSparkline({ +function MiniSparkline({ data, color = "#22c55e", gradientColors = ["rgba(34, 197, 94, 0.8)", "rgba(34, 197, 94, 0.1)"], diff --git a/frontend/src/components/valuecell/charts/sparkline.tsx b/frontend/src/components/valuecell/charts/sparkline.tsx new file mode 100644 index 000000000..28c059ec1 --- /dev/null +++ b/frontend/src/components/valuecell/charts/sparkline.tsx @@ -0,0 +1,259 @@ +import { LineChart } from "echarts/charts"; +import { + DataZoomComponent, + GridComponent, + TooltipComponent, +} from "echarts/components"; +import type { ECharts, EChartsCoreOption } from "echarts/core"; +import * as echarts from "echarts/core"; +import { CanvasRenderer } from "echarts/renderers"; +import { useEffect, useMemo, useRef } from "react"; +import { cn } from "@/lib/utils"; + +echarts.use([ + LineChart, + GridComponent, + TooltipComponent, + DataZoomComponent, + CanvasRenderer, +]); + +interface DataPoint { + timestamp: string; // ISO 日期字符串或时间戳 + value: number; +} + +interface SparklineProps { + data: DataPoint[]; + color?: string; + gradientColors?: [string, string]; + width?: number | string; + height?: number | string; + className?: string; + showGrid?: boolean; + showTooltip?: boolean; + yAxisRange?: [number, number]; + formatTooltip?: (value: number, timestamp: string) => string; +} + +function Sparkline({ + data, + color = "#41C3A9", + gradientColors = ["rgba(65, 195, 169, 0.6)", "rgba(65, 195, 169, 0)"], + width = "100%", + height = 400, + className, + showGrid = true, + showTooltip = true, + yAxisRange, + formatTooltip, +}: SparklineProps) { + const chartRef = useRef(null); + const chartInstance = useRef(null); + + // 处理数据格式 + const chartData = useMemo(() => { + return data.map((item) => [item.timestamp, item.value]); + }, [data]); + + // 计算Y轴范围 + const calculatedYRange = useMemo(() => { + if (yAxisRange) return yAxisRange; + + const values = data.map((item) => item.value); + const min = Math.min(...values); + const max = Math.max(...values); + const padding = (max - min) * 0.1; // 10% 的填充 + + return [Math.max(0, min - padding), max + padding]; + }, [data, yAxisRange]); + + const option: EChartsCoreOption = useMemo(() => { + return { + grid: { + left: 60, + right: 40, + top: 40, + bottom: 40, + containLabel: true, + }, + xAxis: { + type: "time", + show: false, + boundaryGap: false, + }, + yAxis: { + type: "value", + show: showGrid, + min: calculatedYRange[0], + max: calculatedYRange[1], + splitNumber: 5, + axisLine: { + show: false, + }, + axisTick: { + show: false, + }, + axisLabel: { + show: true, + color: "rgba(18, 18, 18, 0.7)", + fontSize: 14, + fontFamily: "SF Pro Text, sans-serif", + fontWeight: 500, + formatter: (value: number) => { + return value.toLocaleString(); + }, + }, + splitLine: { + show: showGrid, + lineStyle: { + color: "rgba(174, 174, 174, 0.5)", + opacity: 0.3, + type: "solid", + }, + }, + }, + series: [ + { + type: "line", + data: chartData, + symbol: "none", + lineStyle: { + color: color, + width: 2, + }, + areaStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { + offset: 0, + color: gradientColors[0], + }, + { + offset: 1, + color: gradientColors[1], + }, + ]), + }, + emphasis: { + focus: "series", + itemStyle: { + color: color, + borderColor: "white", + borderWidth: 4, + shadowBlur: 10, + shadowColor: "rgba(0, 0, 0, 0.3)", + }, + }, + animationDuration: 500, + animationEasing: "quadraticOut", + }, + ], + tooltip: showTooltip + ? { + trigger: "axis", + backgroundColor: "rgba(0, 0, 0, 0.7)", + borderColor: "transparent", + textStyle: { + color: "#fff", + fontSize: 12, + fontFamily: "SF Pro Text, sans-serif", + }, + padding: [14, 16], + borderRadius: 12, + formatter: (params: unknown) => { + if (!Array.isArray(params) || params.length === 0) return ""; + + const param = params[0] as { data: [string, number] }; + if (!param || !param.data) return ""; + + const timestamp = param.data[0]; + const value = param.data[1]; + + if (formatTooltip) { + return formatTooltip(value, timestamp); + } + + // 默认格式化 + const date = new Date(timestamp); + const formatDate = date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + const formatTime = date.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + second: "2-digit", + hour12: true, + }); + + return ` +
+ ${formatDate}, ${formatTime} +
+
+ ${value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} +
+ `; + }, + axisPointer: { + type: "cross", + crossStyle: { + color: color, + opacity: 0.6, + }, + lineStyle: { + color: color, + opacity: 0.6, + }, + }, + } + : undefined, + animation: true, + }; + }, [ + chartData, + color, + gradientColors, + showGrid, + showTooltip, + calculatedYRange, + formatTooltip, + ]); + + useEffect(() => { + if (!chartRef.current) return; + + chartInstance.current = echarts.init(chartRef.current); + chartInstance.current.setOption(option); + + const handleResize = () => { + chartInstance.current?.resize(); + }; + + window.addEventListener("resize", handleResize); + + return () => { + window.removeEventListener("resize", handleResize); + chartInstance.current?.dispose(); + }; + }, [option]); + + useEffect(() => { + // 更新图表数据 + if (chartInstance.current) { + chartInstance.current.setOption({ + series: [{ data: chartData }], + }); + } + }, [chartData]); + + return ( +
+ ); +} + +export default Sparkline; diff --git a/frontend/src/mock/agent-data.tsx b/frontend/src/mock/agent-data.tsx index 16c9f0b67..1b41285e6 100644 --- a/frontend/src/mock/agent-data.tsx +++ b/frontend/src/mock/agent-data.tsx @@ -10,8 +10,8 @@ import { TrendingUp, User, } from "lucide-react"; -import type { AgentRecommendation } from "@/app/_home/components/agent-recommend-list"; -import type { AgentSuggestion } from "@/app/_home/components/agent-suggestions-list"; +import type { AgentRecommendation } from "@/app/home/components/agent-recommend-list"; +import type { AgentSuggestion } from "@/app/home/components/agent-suggestions-list"; const UserAvatar = ({ bgColor, text }: { bgColor: string; text: string }) => (
Date: Tue, 16 Sep 2025 09:42:33 +0800 Subject: [PATCH 05/10] feat: stock highlight when active --- frontend/src/app/home/_layout.tsx | 8 +++++++- frontend/src/components/valuecell/app-sidebar.tsx | 2 +- frontend/src/components/valuecell/menus/stock-menus.tsx | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/home/_layout.tsx b/frontend/src/app/home/_layout.tsx index c86ed3da0..2be50c496 100644 --- a/frontend/src/app/home/_layout.tsx +++ b/frontend/src/app/home/_layout.tsx @@ -1,5 +1,5 @@ import { Plus } from "lucide-react"; -import { Outlet } from "react-router"; +import { Outlet, useLocation } from "react-router"; import { Button } from "@/components/ui/button"; import { StockMenu, @@ -12,6 +12,11 @@ import { import { stockData } from "@/mock/stock-data"; export default function HomeLayout() { + const { pathname } = useLocation(); + + // Extract stock symbol (e.g., AAPL) from path like /stock/AAPL + const stockSymbol = pathname.split("/")[2]; + return (
@@ -30,6 +35,7 @@ export default function HomeLayout() { key={stock.symbol} stock={stock} to={`/stock/${stock.symbol}`} + isActive={stockSymbol === stock.symbol} /> ))} diff --git a/frontend/src/components/valuecell/app-sidebar.tsx b/frontend/src/components/valuecell/app-sidebar.tsx index 6eb2f3309..3e8bafcf2 100644 --- a/frontend/src/components/valuecell/app-sidebar.tsx +++ b/frontend/src/components/valuecell/app-sidebar.tsx @@ -111,7 +111,7 @@ const SidebarMenuItem: FC = ({ const AppSidebar: FC = () => { const { pathname } = useLocation(); - const prefixPath = pathname.split("/")[0]; + const prefixPath = pathname.split("/")[1]; const navItems = useMemo(() => { return { diff --git a/frontend/src/components/valuecell/menus/stock-menus.tsx b/frontend/src/components/valuecell/menus/stock-menus.tsx index 23f35e828..a4e64210b 100644 --- a/frontend/src/components/valuecell/menus/stock-menus.tsx +++ b/frontend/src/components/valuecell/menus/stock-menus.tsx @@ -43,6 +43,7 @@ interface StockMenuGroupHeaderProps interface StockMenuListItemProps extends LinkProps { stock: Stock; + isActive?: boolean; } function StockMenuHeader({ @@ -111,6 +112,7 @@ function StockMenuListItem({ className, stock, onClick, + isActive, ...props }: StockMenuListItemProps) { const changeType = getChangeType(stock.changePercent); @@ -121,6 +123,7 @@ function StockMenuListItem({ "flex items-center justify-between gap-4 rounded-xl p-2", "cursor-pointer transition-colors hover:bg-accent/80", className, + { "bg-accent/80": isActive }, )} {...props} > From c9bd2b668d00cb8aa3343bb5b4893bf78a3c9ce6 Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Tue, 16 Sep 2025 10:57:22 +0800 Subject: [PATCH 06/10] feat: scrollbar solution by overlay --- frontend/bun.lock | 13 ++- frontend/package.json | 5 +- frontend/src/app/home/_layout.tsx | 10 +-- frontend/src/components/ui/scroll-area.tsx | 56 ------------- .../components/valuecell/charts/sparkline.tsx | 1 - .../valuecell/menus/stock-menus.tsx | 19 ----- .../components/valuecell/scroll-container.tsx | 22 +++++ frontend/src/global.css | 50 +++-------- frontend/src/root.tsx | 1 + .../ai-hedge-fund/app/frontend/src/index.css | 84 ++++++++----------- 10 files changed, 81 insertions(+), 180 deletions(-) delete mode 100644 frontend/src/components/ui/scroll-area.tsx create mode 100644 frontend/src/components/valuecell/scroll-container.tsx diff --git a/frontend/bun.lock b/frontend/bun.lock index 8c85aaae6..779258d73 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -6,7 +6,6 @@ "dependencies": { "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", @@ -18,6 +17,8 @@ "echarts": "^6.0.0", "isbot": "^5", "lucide-react": "^0.544.0", + "overlayscrollbars": "^2.12.0", + "overlayscrollbars-react": "^0.5.6", "react": "^19.1.1", "react-dom": "^19.1.1", "tailwind-merge": "^3.3.1", @@ -243,8 +244,6 @@ "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.3.tgz", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], @@ -257,8 +256,6 @@ "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "https://registry.npmmirror.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], - "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], - "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], @@ -275,8 +272,6 @@ "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], - "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "https://registry.npmmirror.com/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="], "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -893,6 +888,10 @@ "outvariant": ["outvariant@1.4.3", "https://registry.npmmirror.com/outvariant/-/outvariant-1.4.3.tgz", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], + "overlayscrollbars": ["overlayscrollbars@2.12.0", "https://registry.npmmirror.com/overlayscrollbars/-/overlayscrollbars-2.12.0.tgz", {}, "sha512-mWJ5MOkcZ/ljHwfLw8+bN0V9ziGCoNoqULcp994j5DTGNQvnkWKWkA7rnO29Kyew5AoHxUnJ4Ndqfcl0HSQjXg=="], + + "overlayscrollbars-react": ["overlayscrollbars-react@0.5.6", "https://registry.npmmirror.com/overlayscrollbars-react/-/overlayscrollbars-react-0.5.6.tgz", { "peerDependencies": { "overlayscrollbars": "^2.0.0", "react": ">=16.8.0" } }, "sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], "package-manager-detector": ["package-manager-detector@1.3.0", "https://registry.npmmirror.com/package-manager-detector/-/package-manager-detector-1.3.0.tgz", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="], diff --git a/frontend/package.json b/frontend/package.json index aca912b20..ceac9ee7b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,7 +12,6 @@ "dependencies": { "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dialog": "^1.1.15", - "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", @@ -24,6 +23,8 @@ "echarts": "^6.0.0", "isbot": "^5", "lucide-react": "^0.544.0", + "overlayscrollbars": "^2.12.0", + "overlayscrollbars-react": "^0.5.6", "react": "^19.1.1", "react-dom": "^19.1.1", "tailwind-merge": "^3.3.1" @@ -45,4 +46,4 @@ "vite-tsconfig-paths": "^5.1.4" }, "packageManager": "bun@1.2.21" -} +} \ No newline at end of file diff --git a/frontend/src/app/home/_layout.tsx b/frontend/src/app/home/_layout.tsx index 2be50c496..8cf883627 100644 --- a/frontend/src/app/home/_layout.tsx +++ b/frontend/src/app/home/_layout.tsx @@ -3,12 +3,12 @@ import { Outlet, useLocation } from "react-router"; import { Button } from "@/components/ui/button"; import { StockMenu, - StockMenuContent, StockMenuGroup, StockMenuGroupHeader, StockMenuHeader, StockMenuListItem, } from "@/components/valuecell/menus/stock-menus"; +import ScrollContainer from "@/components/valuecell/scroll-container"; import { stockData } from "@/mock/stock-data"; export default function HomeLayout() { @@ -19,14 +19,14 @@ export default function HomeLayout() { return (
-
+ -
+
+ +
+
+ - {isPositive ? "+" : ""} - {stockInfo.changePercent.toFixed(2)}% + {stockInfo.price.toFixed(2)} + +
+ + {isPositive ? "+" : ""} + {stockInfo.changePercent.toFixed(2)}% +
-
-
{stockInfo.companyName}
-
- - {/* 价格图表 */} -
-
-

价格走势

-

过去60天的价格变化

+

+ Oct 25, 5:26:38PM UTC-4 . INDEXSP . Disclaimer +

{ - const date = new Date(timestamp); - const formatDate = date.toLocaleDateString("zh-CN", { - month: "short", - day: "numeric", - }); - const formatTime = date.toLocaleTimeString("zh-CN", { - hour: "2-digit", - minute: "2-digit", - }); - - return ` -
- ${formatDate} ${formatTime} -
-
- ${stockInfo.currency}${value.toLocaleString(undefined, { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - })} -
- `; - }} + formatTooltip={formatTooltip} />
+ +
+

Details

+ + +
+ +
+

About

+ +

+ Apple Inc. is an American multinational technology company that + specializes in consumer electronics, computer software, and online + services. Apple is the world's largest technology company by revenue + (totalling $274.5 billion in 2020) and, since January 2021, the + world's most valuable company. As of 2021, Apple is the world's + fourth-largest PC vendor by unit sales, and fourth-largest smartphone + manufacturer. It is one of the Big Five American information + technology companies, along with Amazon, Google, Microsoft, and + Facebook. +

+
); -} +}); export default Stock; diff --git a/frontend/src/components/valuecell/button/back-button.tsx b/frontend/src/components/valuecell/button/back-button.tsx new file mode 100644 index 000000000..8d3956cab --- /dev/null +++ b/frontend/src/components/valuecell/button/back-button.tsx @@ -0,0 +1,22 @@ +import { ArrowLeft } from "lucide-react"; +import type { ComponentProps } from "react"; +import { useNavigate } from "react-router"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +function BackButton({ className, ...props }: ComponentProps<"button">) { + const navigate = useNavigate(); + return ( + + ); +} + +export default BackButton; diff --git a/frontend/src/components/valuecell/menus/stock-menus.tsx b/frontend/src/components/valuecell/menus/stock-menus.tsx index 83a6ed4d5..b66c893e2 100644 --- a/frontend/src/components/valuecell/menus/stock-menus.tsx +++ b/frontend/src/components/valuecell/menus/stock-menus.tsx @@ -35,6 +35,10 @@ interface StockMenuGroupHeaderProps children: React.ReactNode; } +interface StockIconProps extends React.HTMLAttributes { + stock: Stock; +} + interface StockMenuListItemProps extends LinkProps { stock: Stock; isActive?: boolean; @@ -90,6 +94,25 @@ function StockMenuGroupHeader({ ); } +function StockIcon({ className, stock, ...props }: StockIconProps) { + return ( +
+ + + + {stock.symbol.slice(0, 2)} + + +
+ ); +} + function StockMenuListItem({ className, stock, @@ -111,23 +134,7 @@ function StockMenuListItem({ >
{/* icon */} -
- {stock.icon ? ( - - - - {stock.symbol.slice(0, 2)} - - - ) : ( - - {stock.symbol.slice(0, 2)} - - )} -
+ {/* stock info */}
@@ -165,4 +172,5 @@ export { StockMenuGroup, StockMenuGroupHeader, StockMenuListItem, + StockIcon, }; diff --git a/frontend/src/components/valuecell/scroll-container.tsx b/frontend/src/components/valuecell/scroll-container.tsx index 55671e76e..75c73990d 100644 --- a/frontend/src/components/valuecell/scroll-container.tsx +++ b/frontend/src/components/valuecell/scroll-container.tsx @@ -11,7 +11,7 @@ function ScrollContainer({ children, ...props }: ScrollContainerProps) { return ( {children} From 5dc4b02255c0b0a9e2a900d7914c505fc67c5821 Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:21:58 +0800 Subject: [PATCH 08/10] feat: chart resize hook --- .../valuecell/charts/mini-sparkline.tsx | 13 +- .../components/valuecell/charts/sparkline.tsx | 147 +++++++----------- frontend/src/constants/stock.ts | 0 frontend/src/hooks/use-chart-resize.ts | 26 ++++ 4 files changed, 89 insertions(+), 97 deletions(-) create mode 100644 frontend/src/constants/stock.ts create mode 100644 frontend/src/hooks/use-chart-resize.ts diff --git a/frontend/src/components/valuecell/charts/mini-sparkline.tsx b/frontend/src/components/valuecell/charts/mini-sparkline.tsx index 428ab10ce..0c144d1fb 100644 --- a/frontend/src/components/valuecell/charts/mini-sparkline.tsx +++ b/frontend/src/components/valuecell/charts/mini-sparkline.tsx @@ -4,6 +4,7 @@ import type { ECharts, EChartsCoreOption } from "echarts/core"; import * as echarts from "echarts/core"; import { CanvasRenderer } from "echarts/renderers"; import { useEffect, useMemo, useRef } from "react"; +import { useChartResize } from "@/hooks/use-chart-resize"; import { cn } from "@/lib/utils"; echarts.use([LineChart, GridComponent, CanvasRenderer]); @@ -28,6 +29,8 @@ function MiniSparkline({ const chartRef = useRef(null); const chartInstance = useRef(null); + useChartResize(chartInstance); + const option: EChartsCoreOption = useMemo(() => { return { grid: { @@ -75,24 +78,16 @@ function MiniSparkline({ useEffect(() => { if (!chartRef.current) return; - chartInstance.current = echarts.init(chartRef.current); + chartInstance.current = echarts.init(chartRef.current); chartInstance.current.setOption(option); - const handleResize = () => { - chartInstance.current?.resize(); - }; - - window.addEventListener("resize", handleResize); - return () => { - window.removeEventListener("resize", handleResize); chartInstance.current?.dispose(); }; }, [option]); useEffect(() => { - // update chart when data changes if (chartInstance.current) { chartInstance.current.setOption({ series: [{ data }], diff --git a/frontend/src/components/valuecell/charts/sparkline.tsx b/frontend/src/components/valuecell/charts/sparkline.tsx index 7c1f24b67..b6912b2a3 100644 --- a/frontend/src/components/valuecell/charts/sparkline.tsx +++ b/frontend/src/components/valuecell/charts/sparkline.tsx @@ -8,6 +8,7 @@ import type { ECharts, EChartsCoreOption } from "echarts/core"; import * as echarts from "echarts/core"; import { CanvasRenderer } from "echarts/renderers"; import { useEffect, useMemo, useRef } from "react"; +import { useChartResize } from "@/hooks/use-chart-resize"; import { cn } from "@/lib/utils"; echarts.use([ @@ -19,7 +20,7 @@ echarts.use([ ]); interface DataPoint { - timestamp: string; // ISO 日期字符串或时间戳 + timestamp: string; value: number; } @@ -30,10 +31,6 @@ interface SparklineProps { width?: number | string; height?: number | string; className?: string; - showGrid?: boolean; - showTooltip?: boolean; - yAxisRange?: [number, number]; - formatTooltip?: (value: number, timestamp: string) => string; } function Sparkline({ @@ -43,30 +40,24 @@ function Sparkline({ width = "100%", height = 400, className, - showGrid = true, - showTooltip = true, - yAxisRange, - formatTooltip, }: SparklineProps) { const chartRef = useRef(null); const chartInstance = useRef(null); - // 处理数据格式 + // deal with data format const chartData = useMemo(() => { return data.map((item) => [item.timestamp, item.value]); }, [data]); - // 计算Y轴范围 + // calculate Y axis range const calculatedYRange = useMemo(() => { - if (yAxisRange) return yAxisRange; - const values = data.map((item) => item.value); const min = Math.min(...values); const max = Math.max(...values); - const padding = (max - min) * 0.1; // 10% 的填充 + const padding = (max - min) * 0.1; // 10% padding return [Math.max(0, min - padding), max + padding]; - }, [data, yAxisRange]); + }, [data]); const option: EChartsCoreOption = useMemo(() => { return { @@ -83,7 +74,7 @@ function Sparkline({ }, yAxis: { type: "value", - show: showGrid, + show: true, min: calculatedYRange[0], max: calculatedYRange[1], splitNumber: 5, @@ -104,7 +95,7 @@ function Sparkline({ }, }, splitLine: { - show: showGrid, + show: true, lineStyle: { color: "rgba(174, 174, 174, 0.5)", opacity: 0.3, @@ -147,45 +138,40 @@ function Sparkline({ animationEasing: "quadraticOut", }, ], - tooltip: showTooltip - ? { - trigger: "axis", - backgroundColor: "rgba(0, 0, 0, 0.7)", - borderColor: "transparent", - textStyle: { - color: "#fff", - fontSize: 12, - fontFamily: "SF Pro Text, sans-serif", - }, - padding: [14, 16], - borderRadius: 12, - formatter: (params: unknown) => { - if (!Array.isArray(params) || params.length === 0) return ""; - - const param = params[0] as { data: [string, number] }; - if (!param || !param.data) return ""; - - const timestamp = param.data[0]; - const value = param.data[1]; - - if (formatTooltip) { - return formatTooltip(value, timestamp); - } - - // 默认格式化 - const date = new Date(timestamp); - const formatDate = date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - const formatTime = date.toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - second: "2-digit", - hour12: true, - }); - - return ` + tooltip: { + trigger: "axis", + backgroundColor: "rgba(0, 0, 0, 0.7)", + borderColor: "transparent", + textStyle: { + color: "#fff", + fontSize: 12, + fontFamily: "SF Pro Text, sans-serif", + }, + padding: [14, 16], + borderRadius: 12, + formatter: (params: unknown) => { + if (!Array.isArray(params) || params.length === 0) return ""; + + const param = params[0] as { data: [string, number] }; + if (!param || !param.data) return ""; + + const timestamp = param.data[0]; + const value = param.data[1]; + + // default formatting + const date = new Date(timestamp); + const formatDate = date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + const formatTime = date.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + second: "2-digit", + hour12: true, + }); + + return `
${formatDate}, ${formatTime}
@@ -193,31 +179,24 @@ function Sparkline({ ${value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
`; - }, - axisPointer: { - type: "cross", - crossStyle: { - color: color, - opacity: 0.6, - }, - lineStyle: { - color: color, - opacity: 0.6, - }, - }, - } - : undefined, + }, + axisPointer: { + type: "cross", + crossStyle: { + color: color, + opacity: 0.6, + }, + lineStyle: { + color: color, + opacity: 0.6, + }, + }, + }, animation: true, }; - }, [ - chartData, - color, - gradientColors, - showGrid, - showTooltip, - calculatedYRange, - formatTooltip, - ]); + }, [chartData, color, gradientColors, calculatedYRange]); + + useChartResize(chartInstance); useEffect(() => { if (!chartRef.current) return; @@ -225,20 +204,12 @@ function Sparkline({ chartInstance.current = echarts.init(chartRef.current); chartInstance.current.setOption(option); - const handleResize = () => { - chartInstance.current?.resize(); - }; - - window.addEventListener("resize", handleResize); - return () => { - window.removeEventListener("resize", handleResize); chartInstance.current?.dispose(); }; }, [option]); useEffect(() => { - // 更新图表数据 if (chartInstance.current) { chartInstance.current.setOption({ series: [{ data: chartData }], @@ -249,7 +220,7 @@ function Sparkline({ return (
); diff --git a/frontend/src/constants/stock.ts b/frontend/src/constants/stock.ts new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/hooks/use-chart-resize.ts b/frontend/src/hooks/use-chart-resize.ts new file mode 100644 index 000000000..120cc4add --- /dev/null +++ b/frontend/src/hooks/use-chart-resize.ts @@ -0,0 +1,26 @@ +import type { ECharts } from "echarts/core"; +import { useEffect } from "react"; + +/** + * deal with ECharts window resize event + * @param chartInstance ECharts instance ref + * @param dependencies additional dependencies array, when these dependencies change, the resize listener will be re-set + */ +export function useChartResize( + chartInstance: React.RefObject, + dependencies: React.DependencyList = [], +) { + useEffect(() => { + const handleResize = () => { + chartInstance.current?.resize(); + }; + + // add resize event listener + window.addEventListener("resize", handleResize); + + // cleanup function: remove event listener + return () => { + window.removeEventListener("resize", handleResize); + }; + }, [chartInstance, ...dependencies]); +} From cd8728a7722844afdde8c293a721a5471be518ac Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:57:14 +0800 Subject: [PATCH 09/10] refactor: color constants in stock --- .../home/components/sparkline-stock-list.tsx | 26 ++---- frontend/src/app/home/stock.tsx | 83 +++++-------------- .../src/components/valuecell/app-sidebar.tsx | 10 +-- .../valuecell/charts/mini-sparkline.tsx | 12 ++- .../components/valuecell/charts/sparkline.tsx | 16 ++-- frontend/src/constants/stock.ts | 26 ++++++ frontend/src/mock/agent-data.tsx | 2 +- 7 files changed, 77 insertions(+), 98 deletions(-) diff --git a/frontend/src/app/home/components/sparkline-stock-list.tsx b/frontend/src/app/home/components/sparkline-stock-list.tsx index f829bd1cb..a93e27372 100644 --- a/frontend/src/app/home/components/sparkline-stock-list.tsx +++ b/frontend/src/app/home/components/sparkline-stock-list.tsx @@ -1,6 +1,6 @@ import MiniSparkline from "@valuecell/charts/mini-sparkline"; +import { STOCK_COLORS } from "@/constants/stock"; import { cn, formatChange, formatPrice, getChangeType } from "@/lib/utils"; -import type { StockChangeType } from "@/types/stock"; export interface SparklineStock { symbol: string; @@ -20,18 +20,6 @@ interface SparklineStockItemProps stock: SparklineStock; } -const BASE_COLOR: Record = { - positive: "#3F845F", - negative: "#E25C5C", - neutral: "#707070", -}; - -const GRADIENT_COLORS: Record = { - positive: ["rgba(63, 132, 95, 0.5)", "rgba(63, 132, 95, 0)"], - negative: ["rgba(226, 92, 92, 0.5)", "rgba(226, 92, 92, 0)"], - neutral: ["rgba(112, 112, 112, 0.5)", "rgba(112, 112, 112, 0)"], -}; - function SparklineStockItem({ className, stock, @@ -54,21 +42,18 @@ function SparklineStockItem({

{formatPrice(stock.price, stock.currency)}

{formatChange(stock.changeAmount)} {formatChange(stock.changePercent, "%")} @@ -78,8 +63,7 @@ function SparklineStockItem({ diff --git a/frontend/src/app/home/stock.tsx b/frontend/src/app/home/stock.tsx index 3d2f78086..7d8b32b53 100644 --- a/frontend/src/app/home/stock.tsx +++ b/frontend/src/app/home/stock.tsx @@ -1,13 +1,15 @@ import BackButton from "@valuecell/button/back-button"; import Sparkline from "@valuecell/charts/sparkline"; import { StockIcon } from "@valuecell/menus/stock-menus"; -import { memo, useCallback, useMemo } from "react"; +import { memo, useMemo } from "react"; import { useParams } from "react-router"; import { StockDetailsList } from "@/app/home/components"; import { Button } from "@/components/ui/button"; +import { STOCK_BADGE_COLORS } from "@/constants/stock"; +import { formatChange, formatPrice, getChangeType } from "@/lib/utils"; import { stockData } from "@/mock/stock-data"; -// 生成历史价格数据 +// Generate historical price data function generateHistoricalData(basePrice: number, days: number = 30) { const data = []; const now = new Date(); @@ -16,9 +18,9 @@ function generateHistoricalData(basePrice: number, days: number = 30) { const date = new Date(now); date.setDate(date.getDate() - i); - // 模拟价格波动 (±5%) + // Simulate price fluctuation (±5%) const variation = (Math.random() - 0.5) * 0.1; - const price = basePrice * (1 + variation * (i / days)); // 添加趋势 + const price = basePrice * (1 + variation * (i / days)); // Add trend data.push({ timestamp: date.toISOString(), @@ -32,7 +34,7 @@ function generateHistoricalData(basePrice: number, days: number = 30) { const Stock = memo(function Stock() { const { stockId } = useParams(); - // 从 mock 数据中查找股票信息 + // Find stock information from mock data const stockInfo = useMemo(() => { for (const group of stockData) { const stock = group.stocks.find((s) => s.symbol === stockId); @@ -41,13 +43,13 @@ const Stock = memo(function Stock() { return null; }, [stockId]); - // 生成60天历史数据(固定,按设计) + // Generate 60-day historical data (fixed, as per design) const chartData = useMemo(() => { if (!stockInfo) return []; return generateHistoricalData(stockInfo.price, 60); }, [stockInfo]); - // 生成模拟的详细数据(按Figma设计) + // Generate simulated detailed data const detailsData = useMemo(() => { if (!stockInfo) return undefined; @@ -68,48 +70,15 @@ const Stock = memo(function Stock() { }; }, [stockInfo]); - const formatTooltip = useCallback( - (value: number, timestamp: string) => { - if (!stockInfo) return ""; - - const date = new Date(timestamp); - const formatDate = date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - const formatTime = date.toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - second: "2-digit", - hour12: true, - }); - - return ` -
- ${formatDate}, ${formatTime} -
-
- ${value.toFixed(2)} -
- `; - }, - [stockInfo], - ); - if (!stockInfo) { return (
-
未找到股票 {stockId}
+
Stock {stockId} not found
); } - const isPositive = stockInfo.changePercent >= 0; - const chartColor = "#41C3A9"; // 固定为设计中的绿色 - const gradientColors: [string, string] = [ - "rgba(65, 195, 169, 0.6)", - "rgba(65, 195, 169, 0)", - ]; + const changeType = getChangeType(stockInfo.changePercent); return (
@@ -127,34 +96,26 @@ const Stock = memo(function Stock() {
-
+
+ + {formatPrice(stockInfo.price, stockInfo.currency)} + - {stockInfo.price.toFixed(2)} + {formatChange(stockInfo.changePercent, "%")} -
- - {isPositive ? "+" : ""} - {stockInfo.changePercent.toFixed(2)}% - -

Oct 25, 5:26:38PM UTC-4 . INDEXSP . Disclaimer

- +
diff --git a/frontend/src/components/valuecell/app-sidebar.tsx b/frontend/src/components/valuecell/app-sidebar.tsx index 3e8bafcf2..1eb7fffc5 100644 --- a/frontend/src/components/valuecell/app-sidebar.tsx +++ b/frontend/src/components/valuecell/app-sidebar.tsx @@ -119,7 +119,7 @@ const AppSidebar: FC = () => { { id: "home", icon: Logo, - label: "首页", + label: "Home", to: "/", }, ], @@ -127,17 +127,17 @@ const AppSidebar: FC = () => { { id: "chart", icon: ChartBarVertical, - label: "图表", + label: "Chart", to: "chart", }, - { id: "book", icon: BookOpen, label: "书籍", to: "book" }, + { id: "book", icon: BookOpen, label: "Book", to: "book" }, { id: "settings", icon: Setting, - label: "设置", + label: "Settings", to: "settings", }, - { id: "user", icon: User, label: "用户", to: "user" }, + { id: "user", icon: User, label: "User", to: "user" }, ], }; }, []); diff --git a/frontend/src/components/valuecell/charts/mini-sparkline.tsx b/frontend/src/components/valuecell/charts/mini-sparkline.tsx index 0c144d1fb..8751bb686 100644 --- a/frontend/src/components/valuecell/charts/mini-sparkline.tsx +++ b/frontend/src/components/valuecell/charts/mini-sparkline.tsx @@ -4,15 +4,16 @@ import type { ECharts, EChartsCoreOption } from "echarts/core"; import * as echarts from "echarts/core"; import { CanvasRenderer } from "echarts/renderers"; import { useEffect, useMemo, useRef } from "react"; +import { STOCK_COLORS, STOCK_GRADIENT_COLORS } from "@/constants/stock"; import { useChartResize } from "@/hooks/use-chart-resize"; import { cn } from "@/lib/utils"; +import type { StockChangeType } from "@/types/stock"; echarts.use([LineChart, GridComponent, CanvasRenderer]); interface MiniSparklineProps { data: number[]; - color?: string; - gradientColors?: [string, string]; + changeType: StockChangeType; width?: number | string; height?: number | string; className?: string; @@ -20,8 +21,7 @@ interface MiniSparklineProps { function MiniSparkline({ data, - color = "#22c55e", - gradientColors = ["rgba(34, 197, 94, 0.8)", "rgba(34, 197, 94, 0.1)"], + changeType, width = 100, height = 40, className, @@ -31,6 +31,10 @@ function MiniSparkline({ useChartResize(chartInstance); + // Get colors based on change type + const color = STOCK_COLORS[changeType]; + const gradientColors = STOCK_GRADIENT_COLORS[changeType]; + const option: EChartsCoreOption = useMemo(() => { return { grid: { diff --git a/frontend/src/components/valuecell/charts/sparkline.tsx b/frontend/src/components/valuecell/charts/sparkline.tsx index b6912b2a3..9635328b9 100644 --- a/frontend/src/components/valuecell/charts/sparkline.tsx +++ b/frontend/src/components/valuecell/charts/sparkline.tsx @@ -8,8 +8,10 @@ import type { ECharts, EChartsCoreOption } from "echarts/core"; import * as echarts from "echarts/core"; import { CanvasRenderer } from "echarts/renderers"; import { useEffect, useMemo, useRef } from "react"; +import { STOCK_COLORS, STOCK_GRADIENT_COLORS } from "@/constants/stock"; import { useChartResize } from "@/hooks/use-chart-resize"; import { cn } from "@/lib/utils"; +import type { StockChangeType } from "@/types/stock"; echarts.use([ LineChart, @@ -26,8 +28,7 @@ interface DataPoint { interface SparklineProps { data: DataPoint[]; - color?: string; - gradientColors?: [string, string]; + changeType: StockChangeType; width?: number | string; height?: number | string; className?: string; @@ -35,8 +36,7 @@ interface SparklineProps { function Sparkline({ data, - color = "#41C3A9", - gradientColors = ["rgba(65, 195, 169, 0.6)", "rgba(65, 195, 169, 0)"], + changeType, width = "100%", height = 400, className, @@ -44,12 +44,16 @@ function Sparkline({ const chartRef = useRef(null); const chartInstance = useRef(null); - // deal with data format + // Get colors based on change type + const color = STOCK_COLORS[changeType]; + const gradientColors = STOCK_GRADIENT_COLORS[changeType]; + + // Format data for ECharts const chartData = useMemo(() => { return data.map((item) => [item.timestamp, item.value]); }, [data]); - // calculate Y axis range + // Calculate Y axis range with padding const calculatedYRange = useMemo(() => { const values = data.map((item) => item.value); const min = Math.min(...values); diff --git a/frontend/src/constants/stock.ts b/frontend/src/constants/stock.ts index e69de29bb..4f84ca30a 100644 --- a/frontend/src/constants/stock.ts +++ b/frontend/src/constants/stock.ts @@ -0,0 +1,26 @@ +import type { StockChangeType } from "@/types/stock"; + +// Stock change type color mappings +export const STOCK_COLORS: Record = { + positive: "#41C3A9", + negative: "#E25C5C", + neutral: "#707070", +}; + +// Stock change type gradient color mappings +export const STOCK_GRADIENT_COLORS: Record = + { + positive: ["rgba(65, 195, 169, 0.6)", "rgba(65, 195, 169, 0)"], + negative: ["rgba(226, 92, 92, 0.5)", "rgba(226, 92, 92, 0)"], + neutral: ["rgba(112, 112, 112, 0.5)", "rgba(112, 112, 112, 0)"], + }; + +// Stock change type badge color mappings (for percentage change display) +export const STOCK_BADGE_COLORS: Record< + StockChangeType, + { bg: string; text: string } +> = { + positive: { bg: "#EEFBF5", text: "#5CCDB3" }, + negative: { bg: "#FFEAEA", text: "#E25C5C" }, + neutral: { bg: "#F5F5F5", text: "#707070" }, +}; diff --git a/frontend/src/mock/agent-data.tsx b/frontend/src/mock/agent-data.tsx index 1b41285e6..1956ef5e5 100644 --- a/frontend/src/mock/agent-data.tsx +++ b/frontend/src/mock/agent-data.tsx @@ -119,7 +119,7 @@ export const agentRecommendations: AgentRecommendation[] = [ }, { id: "recommend-5", - title: "巴菲特投资Agent", + title: "Buffett Investment Agent", icon: (
From 2b17503a7d814e832bfc7ac83d1057642b7bf6c9 Mon Sep 17 00:00:00 2001 From: DigHuang <114602213+DigHuang@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:17:34 +0800 Subject: [PATCH 10/10] feat: dayjs format & chart refactor --- frontend/bun.lock | 3 + frontend/package.json | 1 + .../home/components/sparkline-stock-list.tsx | 18 +- frontend/src/app/home/stock.tsx | 21 +- .../valuecell/charts/mini-sparkline.tsx | 8 +- .../components/valuecell/charts/sparkline.tsx | 119 +++------ frontend/src/lib/time.ts | 233 ++++++++++++++++++ frontend/src/mock/stock-data.ts | 34 ++- frontend/src/types/chart.ts | 5 + 9 files changed, 330 insertions(+), 112 deletions(-) create mode 100644 frontend/src/lib/time.ts create mode 100644 frontend/src/types/chart.ts diff --git a/frontend/bun.lock b/frontend/bun.lock index 779258d73..f2933eb4d 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -14,6 +14,7 @@ "@tauri-apps/plugin-opener": "^2.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.18", "echarts": "^6.0.0", "isbot": "^5", "lucide-react": "^0.544.0", @@ -540,6 +541,8 @@ "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "dayjs": ["dayjs@1.11.18", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz", {}, "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="], + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], "dedent": ["dedent@1.7.0", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ=="], diff --git a/frontend/package.json b/frontend/package.json index f73c3b817..a64eb8434 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@tauri-apps/plugin-opener": "^2.5.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dayjs": "^1.11.18", "echarts": "^6.0.0", "isbot": "^5", "lucide-react": "^0.544.0", diff --git a/frontend/src/app/home/components/sparkline-stock-list.tsx b/frontend/src/app/home/components/sparkline-stock-list.tsx index a93e27372..b6e30d58b 100644 --- a/frontend/src/app/home/components/sparkline-stock-list.tsx +++ b/frontend/src/app/home/components/sparkline-stock-list.tsx @@ -1,6 +1,7 @@ import MiniSparkline from "@valuecell/charts/mini-sparkline"; import { STOCK_COLORS } from "@/constants/stock"; import { cn, formatChange, formatPrice, getChangeType } from "@/lib/utils"; +import type { SparklineData } from "@/types/chart"; export interface SparklineStock { symbol: string; @@ -8,7 +9,7 @@ export interface SparklineStock { currency: string; changeAmount: number; changePercent: number; - sparklineData: number[]; + sparklineData: SparklineData; } interface SparklineStockListProps extends React.HTMLAttributes { @@ -42,18 +43,27 @@ function SparklineStockItem({

{formatPrice(stock.price, stock.currency)}

{formatChange(stock.changeAmount)} {formatChange(stock.changePercent, "%")} diff --git a/frontend/src/app/home/stock.tsx b/frontend/src/app/home/stock.tsx index 7d8b32b53..b1a4fb6b0 100644 --- a/frontend/src/app/home/stock.tsx +++ b/frontend/src/app/home/stock.tsx @@ -8,10 +8,14 @@ import { Button } from "@/components/ui/button"; import { STOCK_BADGE_COLORS } from "@/constants/stock"; import { formatChange, formatPrice, getChangeType } from "@/lib/utils"; import { stockData } from "@/mock/stock-data"; - -// Generate historical price data -function generateHistoricalData(basePrice: number, days: number = 30) { - const data = []; +import type { SparklineData } from "@/types/chart"; + +// Generate historical price data in [timestamp, value] format +function generateHistoricalData( + basePrice: number, + days: number = 30, +): SparklineData { + const data: SparklineData = []; const now = new Date(); for (let i = days; i >= 0; i--) { @@ -22,10 +26,11 @@ function generateHistoricalData(basePrice: number, days: number = 30) { const variation = (Math.random() - 0.5) * 0.1; const price = basePrice * (1 + variation * (i / days)); // Add trend - data.push({ - timestamp: date.toISOString(), - value: Math.max(0, price), - }); + // Use [timestamp, value] format to match SparklineData + data.push([ + date.valueOf(), // Use timestamp number instead of ISO string + Math.max(0, Number(price.toFixed(2))), + ]); } return data; diff --git a/frontend/src/components/valuecell/charts/mini-sparkline.tsx b/frontend/src/components/valuecell/charts/mini-sparkline.tsx index 8751bb686..0f23e1f4c 100644 --- a/frontend/src/components/valuecell/charts/mini-sparkline.tsx +++ b/frontend/src/components/valuecell/charts/mini-sparkline.tsx @@ -7,12 +7,13 @@ import { useEffect, useMemo, useRef } from "react"; import { STOCK_COLORS, STOCK_GRADIENT_COLORS } from "@/constants/stock"; import { useChartResize } from "@/hooks/use-chart-resize"; import { cn } from "@/lib/utils"; +import type { SparklineData } from "@/types/chart"; import type { StockChangeType } from "@/types/stock"; echarts.use([LineChart, GridComponent, CanvasRenderer]); interface MiniSparklineProps { - data: number[]; + data: SparklineData; changeType: StockChangeType; width?: number | string; height?: number | string; @@ -44,8 +45,11 @@ function MiniSparkline({ bottom: 0, }, xAxis: { - type: "category", + type: "time", show: false, + axisLabel: { + show: false, + }, }, yAxis: { type: "value", diff --git a/frontend/src/components/valuecell/charts/sparkline.tsx b/frontend/src/components/valuecell/charts/sparkline.tsx index 9635328b9..8856f38b3 100644 --- a/frontend/src/components/valuecell/charts/sparkline.tsx +++ b/frontend/src/components/valuecell/charts/sparkline.tsx @@ -4,13 +4,16 @@ import { GridComponent, TooltipComponent, } from "echarts/components"; -import type { ECharts, EChartsCoreOption } from "echarts/core"; +import type { ECharts } from "echarts/core"; import * as echarts from "echarts/core"; import { CanvasRenderer } from "echarts/renderers"; +import type { EChartsOption } from "echarts/types/dist/shared"; import { useEffect, useMemo, useRef } from "react"; import { STOCK_COLORS, STOCK_GRADIENT_COLORS } from "@/constants/stock"; import { useChartResize } from "@/hooks/use-chart-resize"; +import { format } from "@/lib/time"; import { cn } from "@/lib/utils"; +import type { SparklineData } from "@/types/chart"; import type { StockChangeType } from "@/types/stock"; echarts.use([ @@ -21,13 +24,8 @@ echarts.use([ CanvasRenderer, ]); -interface DataPoint { - timestamp: string; - value: number; -} - interface SparklineProps { - data: DataPoint[]; + data: SparklineData; changeType: StockChangeType; width?: number | string; height?: number | string; @@ -48,56 +46,24 @@ function Sparkline({ const color = STOCK_COLORS[changeType]; const gradientColors = STOCK_GRADIENT_COLORS[changeType]; - // Format data for ECharts - const chartData = useMemo(() => { - return data.map((item) => [item.timestamp, item.value]); - }, [data]); - - // Calculate Y axis range with padding - const calculatedYRange = useMemo(() => { - const values = data.map((item) => item.value); - const min = Math.min(...values); - const max = Math.max(...values); - const padding = (max - min) * 0.1; // 10% padding - - return [Math.max(0, min - padding), max + padding]; - }, [data]); - - const option: EChartsCoreOption = useMemo(() => { + const option: EChartsOption = useMemo(() => { return { grid: { - left: 60, - right: 40, - top: 40, - bottom: 40, + left: 0, + right: 0, + top: 0, + bottom: 0, }, xAxis: { type: "time", show: false, - boundaryGap: false, + axisLabel: { + show: false, + }, }, yAxis: { type: "value", - show: true, - min: calculatedYRange[0], - max: calculatedYRange[1], - splitNumber: 5, - axisLine: { - show: false, - }, - axisTick: { - show: false, - }, - axisLabel: { - show: true, - color: "rgba(18, 18, 18, 0.7)", - fontSize: 14, - fontFamily: "SF Pro Text, sans-serif", - fontWeight: 500, - formatter: (value: number) => { - return value.toLocaleString(); - }, - }, + scale: true, splitLine: { show: true, lineStyle: { @@ -110,11 +76,14 @@ function Sparkline({ series: [ { type: "line", - data: chartData, - symbol: "none", - lineStyle: { + data: data, + symbol: "circle", + symbolSize: 12, + showSymbol: false, + itemStyle: { color: color, - width: 2, + borderColor: "#fff", + borderWidth: 4, }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ @@ -128,16 +97,6 @@ function Sparkline({ }, ]), }, - emphasis: { - focus: "series", - itemStyle: { - color: color, - borderColor: "white", - borderWidth: 4, - shadowBlur: 10, - shadowColor: "rgba(0, 0, 0, 0.3)", - }, - }, animationDuration: 500, animationEasing: "quadraticOut", }, @@ -145,35 +104,24 @@ function Sparkline({ tooltip: { trigger: "axis", backgroundColor: "rgba(0, 0, 0, 0.7)", - borderColor: "transparent", textStyle: { color: "#fff", fontSize: 12, - fontFamily: "SF Pro Text, sans-serif", }, padding: [14, 16], borderRadius: 12, formatter: (params: unknown) => { if (!Array.isArray(params) || params.length === 0) return ""; - const param = params[0] as { data: [string, number] }; + const param = params[0] as { data: [number, number] }; if (!param || !param.data) return ""; const timestamp = param.data[0]; const value = param.data[1]; - // default formatting - const date = new Date(timestamp); - const formatDate = date.toLocaleDateString("en-US", { - month: "short", - day: "numeric", - }); - const formatTime = date.toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - second: "2-digit", - hour12: true, - }); + // Use our time utility for formatting + const formatDate = format(timestamp, "MMM D"); + const formatTime = format(timestamp, "h:mm:ss A"); return `
@@ -185,20 +133,11 @@ function Sparkline({ `; }, axisPointer: { - type: "cross", - crossStyle: { - color: color, - opacity: 0.6, - }, - lineStyle: { - color: color, - opacity: 0.6, - }, + type: "none", }, }, - animation: true, }; - }, [chartData, color, gradientColors, calculatedYRange]); + }, [data, color, gradientColors]); useChartResize(chartInstance); @@ -216,10 +155,10 @@ function Sparkline({ useEffect(() => { if (chartInstance.current) { chartInstance.current.setOption({ - series: [{ data: chartData }], + series: [{ data }], }); } - }, [chartData]); + }, [data]); return (
;