fix: replace default chart tooltip with custom premium glassmorphic tooltip
Browse files- StockChart.tsx: Implemented CustomTooltip component for hover interactions, rendering Open/High/Low/Close data and active SMA/EMA overlays in color-coded, high-contrast, fully readable typography.
frontend/src/components/StockChart.tsx
CHANGED
|
@@ -80,6 +80,80 @@ const CandlestickBar = (props: any) => {
|
|
| 80 |
);
|
| 81 |
};
|
| 82 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
export default function StockChart({ activeTicker, chartData, activeStats, chartRange, onRangeChange }: StockChartProps) {
|
| 84 |
const currentPrice = chartData.length > 0 ? chartData[chartData.length - 1].price : null;
|
| 85 |
|
|
@@ -315,15 +389,7 @@ export default function StockChart({ activeTicker, chartData, activeStats, chart
|
|
| 315 |
tickLine={false}
|
| 316 |
/>
|
| 317 |
|
| 318 |
-
<Tooltip
|
| 319 |
-
contentStyle={{
|
| 320 |
-
background: '#0d101b',
|
| 321 |
-
borderColor: '#2e303a',
|
| 322 |
-
borderRadius: '8px',
|
| 323 |
-
color: '#fff',
|
| 324 |
-
fontSize: '12px'
|
| 325 |
-
}}
|
| 326 |
-
/>
|
| 327 |
|
| 328 |
{/* Primary Chart Type Representation */}
|
| 329 |
{chartType === 'line' ? (
|
|
|
|
| 80 |
);
|
| 81 |
};
|
| 82 |
|
| 83 |
+
// Custom premium Tooltip component for stock chart hover stats
|
| 84 |
+
const CustomTooltip = ({ active, payload, label }: any) => {
|
| 85 |
+
if (active && payload && payload.length) {
|
| 86 |
+
const data = payload[0].payload;
|
| 87 |
+
const isCandle = data.open !== undefined && data.close !== undefined;
|
| 88 |
+
|
| 89 |
+
return (
|
| 90 |
+
<div style={{
|
| 91 |
+
background: 'rgba(13, 16, 27, 0.9)',
|
| 92 |
+
backdropFilter: 'blur(12px)',
|
| 93 |
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
| 94 |
+
borderRadius: '8px',
|
| 95 |
+
padding: '12px 16px',
|
| 96 |
+
boxShadow: '0 12px 24px rgba(0, 0, 0, 0.6)',
|
| 97 |
+
color: '#fff',
|
| 98 |
+
fontSize: '12px',
|
| 99 |
+
fontFamily: 'inherit',
|
| 100 |
+
minWidth: '160px'
|
| 101 |
+
}}>
|
| 102 |
+
<div style={{ fontWeight: 600, color: 'var(--text-secondary)', borderBottom: '1px solid rgba(255,255,255,0.08)', paddingBottom: '6px', marginBottom: '8px' }}>
|
| 103 |
+
{label}
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
{isCandle ? (
|
| 107 |
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
|
| 108 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '16px' }}>
|
| 109 |
+
<span style={{ color: 'var(--text-muted)' }}>Open</span>
|
| 110 |
+
<span style={{ fontWeight: 600, color: '#fff' }}>${data.open.toFixed(2)}</span>
|
| 111 |
+
</div>
|
| 112 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '16px' }}>
|
| 113 |
+
<span style={{ color: 'var(--text-muted)' }}>High</span>
|
| 114 |
+
<span style={{ fontWeight: 600, color: '#10b981' }}>${data.high?.toFixed(2)}</span>
|
| 115 |
+
</div>
|
| 116 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '16px' }}>
|
| 117 |
+
<span style={{ color: 'var(--text-muted)' }}>Low</span>
|
| 118 |
+
<span style={{ fontWeight: 600, color: '#ef4444' }}>${data.low?.toFixed(2)}</span>
|
| 119 |
+
</div>
|
| 120 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '16px' }}>
|
| 121 |
+
<span style={{ color: 'var(--text-muted)' }}>Close</span>
|
| 122 |
+
<span style={{ fontWeight: 600, color: '#fff' }}>${data.close.toFixed(2)}</span>
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
) : (
|
| 126 |
+
<div style={{ display: 'flex', justifyContent: 'space-between', gap: '16px' }}>
|
| 127 |
+
<span style={{ color: 'var(--text-muted)' }}>Price</span>
|
| 128 |
+
<span style={{ fontWeight: 600, color: 'var(--neon-cyan)' }}>${data.price.toFixed(2)}</span>
|
| 129 |
+
</div>
|
| 130 |
+
)}
|
| 131 |
+
|
| 132 |
+
{payload.map((entry: any, index: number) => {
|
| 133 |
+
if (entry.dataKey === 'sma' && entry.value !== undefined && entry.value !== null) {
|
| 134 |
+
return (
|
| 135 |
+
<div key={`sma-${index}`} style={{ display: 'flex', justifyContent: 'space-between', gap: '16px', color: 'var(--neon-violet)', borderTop: '1px solid rgba(255,255,255,0.06)', paddingTop: '4px', marginTop: '4px' }}>
|
| 136 |
+
<span>SMA 20</span>
|
| 137 |
+
<span style={{ fontWeight: 600 }}>${entry.value.toFixed(2)}</span>
|
| 138 |
+
</div>
|
| 139 |
+
);
|
| 140 |
+
}
|
| 141 |
+
if (entry.dataKey === 'ema' && entry.value !== undefined && entry.value !== null) {
|
| 142 |
+
return (
|
| 143 |
+
<div key={`ema-${index}`} style={{ display: 'flex', justifyContent: 'space-between', gap: '16px', color: '#fb923c', borderTop: '1px solid rgba(255,255,255,0.06)', paddingTop: '4px', marginTop: '4px' }}>
|
| 144 |
+
<span>EMA 20</span>
|
| 145 |
+
<span style={{ fontWeight: 600 }}>${entry.value.toFixed(2)}</span>
|
| 146 |
+
</div>
|
| 147 |
+
);
|
| 148 |
+
}
|
| 149 |
+
return null;
|
| 150 |
+
})}
|
| 151 |
+
</div>
|
| 152 |
+
);
|
| 153 |
+
}
|
| 154 |
+
return null;
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
export default function StockChart({ activeTicker, chartData, activeStats, chartRange, onRangeChange }: StockChartProps) {
|
| 158 |
const currentPrice = chartData.length > 0 ? chartData[chartData.length - 1].price : null;
|
| 159 |
|
|
|
|
| 389 |
tickLine={false}
|
| 390 |
/>
|
| 391 |
|
| 392 |
+
<Tooltip content={<CustomTooltip />} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
|
| 394 |
{/* Primary Chart Type Representation */}
|
| 395 |
{chartType === 'line' ? (
|