Spaces:
Sleeping
Sleeping
| import { | |
| ResponsiveContainer, | |
| ComposedChart, | |
| Line, | |
| XAxis, | |
| YAxis, | |
| Tooltip, | |
| ReferenceLine, | |
| CartesianGrid, | |
| } from 'recharts' | |
| function CustomTooltip({ active, payload, label }) { | |
| if (!active || !payload?.length) return null | |
| const price = payload.find(p => p.dataKey === 'price') | |
| return ( | |
| <div style={{ | |
| background: 'rgba(15,15,30,0.95)', | |
| border: '1px solid rgba(255,255,255,0.1)', | |
| borderRadius: 8, | |
| padding: '0.5rem 0.75rem', | |
| fontSize: '0.78rem', | |
| }}> | |
| <div style={{ color: 'var(--text-muted)', marginBottom: 4 }}>{label}</div> | |
| {price && ( | |
| <div style={{ color: 'var(--text)', fontWeight: 600 }}> | |
| ${Number(price.value).toFixed(2)} | |
| </div> | |
| )} | |
| </div> | |
| ) | |
| } | |
| export default function PriceChart({ data }) { | |
| const { price_series, ma20, ma50, bb_upper, bb_lower, current_price } = data | |
| if (!price_series || Object.keys(price_series).length === 0) return null | |
| const chartData = Object.entries(price_series).map(([date, price]) => ({ | |
| date: date.slice(5), | |
| price: Number(price), | |
| ma20: ma20 ? Number(ma20) : undefined, | |
| ma50: ma50 ? Number(ma50) : undefined, | |
| bbUpper: bb_upper ? Number(bb_upper) : undefined, | |
| bbLower: bb_lower ? Number(bb_lower) : undefined, | |
| })) | |
| const prices = chartData.map(d => d.price) | |
| const lastPrice = prices[prices.length - 1] | |
| const isUp = lastPrice >= prices[0] | |
| const lineColor = isUp ? 'var(--buy)' : 'var(--sell)' | |
| const allValues = [ | |
| ...prices, | |
| bb_upper ? Number(bb_upper) : null, | |
| bb_lower ? Number(bb_lower) : null, | |
| ].filter(Boolean) | |
| const minY = Math.min(...allValues) * 0.995 | |
| const maxY = Math.max(...allValues) * 1.005 | |
| const change = lastPrice - prices[0] | |
| const changePct = ((change / prices[0]) * 100).toFixed(2) | |
| return ( | |
| <div className="card price-chart-card"> | |
| <div className="price-chart-header"> | |
| <h3>30-Day Price</h3> | |
| <span className="price-chart-change" style={{ color: lineColor }}> | |
| {change >= 0 ? '+' : ''}{changePct}% | |
| </span> | |
| </div> | |
| <div style={{ width: '100%', height: 200 }}> | |
| <ResponsiveContainer> | |
| <ComposedChart data={chartData} margin={{ top: 4, right: 4, left: 0, bottom: 0 }}> | |
| <CartesianGrid stroke="rgba(255,255,255,0.04)" vertical={false} /> | |
| <XAxis | |
| dataKey="date" | |
| tick={{ fill: 'var(--text-faint)', fontSize: 10 }} | |
| tickLine={false} | |
| axisLine={false} | |
| interval="preserveStartEnd" | |
| /> | |
| <YAxis | |
| domain={[minY, maxY]} | |
| tick={{ fill: 'var(--text-faint)', fontSize: 10 }} | |
| tickLine={false} | |
| axisLine={false} | |
| tickFormatter={v => `$${v.toFixed(0)}`} | |
| width={50} | |
| /> | |
| <Tooltip content={<CustomTooltip />} /> | |
| {/* Bollinger band lines */} | |
| {chartData[0]?.bbUpper && ( | |
| <Line dataKey="bbUpper" stroke="rgba(255,255,255,0.08)" strokeWidth={1} | |
| dot={false} isAnimationActive={false} strokeDasharray="2 4" /> | |
| )} | |
| {chartData[0]?.bbLower && ( | |
| <Line dataKey="bbLower" stroke="rgba(255,255,255,0.08)" strokeWidth={1} | |
| dot={false} isAnimationActive={false} strokeDasharray="2 4" /> | |
| )} | |
| {/* MA50 */} | |
| {ma50 && ( | |
| <Line dataKey="ma50" stroke="rgba(255,204,68,0.55)" strokeWidth={1.5} | |
| dot={false} isAnimationActive={false} strokeDasharray="4 2" /> | |
| )} | |
| {/* MA20 */} | |
| {ma20 && ( | |
| <Line dataKey="ma20" stroke="rgba(68,170,255,0.65)" strokeWidth={1.5} | |
| dot={false} isAnimationActive={false} strokeDasharray="4 2" /> | |
| )} | |
| {/* Price */} | |
| <Line dataKey="price" stroke={lineColor} strokeWidth={2} | |
| dot={false} activeDot={{ r: 4, fill: lineColor, stroke: 'none' }} | |
| isAnimationActive={false} /> | |
| {/* Current price dashed line */} | |
| <ReferenceLine y={lastPrice} stroke={lineColor} | |
| strokeOpacity={0.25} strokeDasharray="2 5" /> | |
| </ComposedChart> | |
| </ResponsiveContainer> | |
| </div> | |
| <div className="price-chart-legend"> | |
| <span style={{ color: lineColor }}>— Price</span> | |
| {ma20 && <span style={{ color: 'rgba(68,170,255,0.65)' }}>-- MA20</span>} | |
| {ma50 && <span style={{ color: 'rgba(255,204,68,0.55)' }}>-- MA50</span>} | |
| {bb_upper && <span style={{ color: 'rgba(255,255,255,0.3)' }}>-- Bollinger</span>} | |
| </div> | |
| </div> | |
| ) | |
| } | |