Spaces:
Running
Running
| import React from 'react' | |
| import { | |
| Bar, | |
| CartesianGrid, | |
| Cell, | |
| Legend, | |
| ResponsiveContainer, | |
| Tooltip, | |
| XAxis, | |
| YAxis, | |
| ComposedChart, | |
| Line, | |
| ReferenceLine, | |
| } from 'recharts' | |
| import { OHLCVPoint } from '../api/stockApi' | |
| interface Props { | |
| data: OHLCVPoint[] | |
| } | |
| function formatShares(v: number): string { | |
| const abs = Math.abs(v) | |
| const sign = v < 0 ? '-' : '' | |
| if (abs >= 1_000_000) return `${sign}${(abs / 1_000_000).toFixed(1)}M` | |
| if (abs >= 1_000) return `${sign}${(abs / 1_000).toFixed(0)}K` | |
| return `${v.toFixed(0)}` | |
| } | |
| const CustomTooltip = ({ active, payload, label }: any) => { | |
| if (!active || !payload?.length) return null | |
| const d: OHLCVPoint = payload[0]?.payload | |
| return ( | |
| <div className="bg-slate-800 border border-slate-600 rounded p-2 text-xs shadow-lg space-y-1"> | |
| <p className="text-slate-300">{label}</p> | |
| <p>外資: <span className={Number(d.foreign_net ?? 0) >= 0 ? 'text-green-400' : 'text-red-400'}>{formatShares(Number(d.foreign_net ?? 0))}</span></p> | |
| <p>投信: <span className={Number(d.trust_net ?? 0) >= 0 ? 'text-green-400' : 'text-red-400'}>{formatShares(Number(d.trust_net ?? 0))}</span></p> | |
| <p>自營商: <span className={Number(d.dealer_net ?? 0) >= 0 ? 'text-green-400' : 'text-red-400'}>{formatShares(Number(d.dealer_net ?? 0))}</span></p> | |
| <p>合計: <span className={Number(d.institutional_net ?? 0) >= 0 ? 'text-green-300' : 'text-red-300'}>{formatShares(Number(d.institutional_net ?? 0))}</span></p> | |
| {d.institutional_as_of && ( | |
| <p className="text-slate-500">資料日: {d.institutional_as_of}</p> | |
| )} | |
| </div> | |
| ) | |
| } | |
| export const InstitutionalFlowChart: React.FC<Props> = ({ data }) => { | |
| const len = data.length | |
| const chartData = data.filter((d) => d.institutional_net != null) | |
| return ( | |
| <ResponsiveContainer width="100%" height={190}> | |
| <ComposedChart data={chartData} margin={{ top: 4, right: 16, left: 0, bottom: 0 }}> | |
| <CartesianGrid strokeDasharray="3 3" stroke="#1e293b" /> | |
| <XAxis | |
| dataKey="date" | |
| tick={{ fill: '#94a3b8', fontSize: 10 }} | |
| tickLine={false} | |
| tickFormatter={(val, idx) => { | |
| const step = Math.max(1, Math.floor(len / 10)) | |
| return idx % step === 0 ? val : '' | |
| }} | |
| interval={0} | |
| /> | |
| <YAxis | |
| tick={{ fill: '#94a3b8', fontSize: 10 }} | |
| tickLine={false} | |
| tickFormatter={formatShares} | |
| width={52} | |
| /> | |
| <Tooltip content={<CustomTooltip />} /> | |
| <Legend | |
| wrapperStyle={{ fontSize: 12 }} | |
| formatter={(value) => <span style={{ color: '#94a3b8' }}>{value}</span>} | |
| /> | |
| <ReferenceLine y={0} stroke="#475569" strokeWidth={1} /> | |
| <Bar dataKey="institutional_net" name="三大法人合計" isAnimationActive={false} maxBarSize={6}> | |
| {chartData.map((d, i) => ( | |
| <Cell | |
| key={i} | |
| fill={Number(d.institutional_net ?? 0) >= 0 ? '#22c55e' : '#ef4444'} | |
| fillOpacity={0.72} | |
| /> | |
| ))} | |
| </Bar> | |
| <Line | |
| dataKey="foreign_net" | |
| name="外資" | |
| stroke="#60a5fa" | |
| strokeWidth={1.4} | |
| dot={false} | |
| isAnimationActive={false} | |
| connectNulls | |
| /> | |
| <Line | |
| dataKey="trust_net" | |
| name="投信" | |
| stroke="#facc15" | |
| strokeWidth={1.2} | |
| dot={false} | |
| isAnimationActive={false} | |
| connectNulls | |
| /> | |
| <Line | |
| dataKey="dealer_net" | |
| name="自營商" | |
| stroke="#f472b6" | |
| strokeWidth={1.2} | |
| dot={false} | |
| isAnimationActive={false} | |
| connectNulls | |
| /> | |
| </ComposedChart> | |
| </ResponsiveContainer> | |
| ) | |
| } | |