DockerSpace / frontend /src /components /InstitutionalFlowChart.tsx
DennisChan0909's picture
feat: add 三大法人 buy/sell/net table and dealer_net line to institutional chart
42fcd29
Raw
History Blame Contribute Delete
3.81 kB
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>
)
}