({ selectedMarket, lineOnly = false }: ChartPanelProps)
| 135 | } |
| 136 | |
| 137 | function ChartPanelInner({ selectedMarket, lineOnly = false }: ChartPanelProps) { |
| 138 | const { t } = useI18n(); |
| 139 | const [timeRange, setTimeRange] = useState<TimeRange>("24h"); |
| 140 | const [chartMode, setChartMode] = useState<ChartMode>("candle"); |
| 141 | const [showMA, setShowMA] = useState(true); |
| 142 | const [showMACD, setShowMACD] = useState(true); |
| 143 | const [loading, setLoading] = useState(false); |
| 144 | const [error, setError] = useState<string | null>(null); |
| 145 | const [crosshairData, setCrosshairData] = useState<CrosshairData | null>(null); |
| 146 | const [chartData, setChartData] = useState<ChartData | null>(null); |
| 147 | const [useUTC, setUseUTC] = useState(true); |
| 148 | const retryCount = useRef(0); |
| 149 | |
| 150 | const chartContainerRef = useRef<HTMLDivElement>(null); |
| 151 | const macdContainerRef = useRef<HTMLDivElement>(null); |
| 152 | const chartInstanceRef = useRef<ReturnType<typeof import("lightweight-charts").createChart> | null>(null); |
| 153 | const macdChartRef = useRef<ReturnType<typeof import("lightweight-charts").createChart> | null>(null); |
| 154 | |
| 155 | const isMulti = useMemo(() => { |
| 156 | if (!selectedMarket) return false; |
| 157 | return selectedMarket.markets.filter(m => m.active !== false).length > 1; |
| 158 | }, [selectedMarket]); |
| 159 | |
| 160 | const effectiveMode = lineOnly ? "line" : (isMulti && chartMode === "candle" ? "line" : chartMode); |
| 161 | |
| 162 | // ──────────────────────────────────────────────────────── |
| 163 | // Step 1: FETCH DATA — only when market or timeRange changes |
| 164 | // ──────────────────────────────────────────────────────── |
| 165 | useEffect(() => { |
| 166 | if (!selectedMarket) { setChartData(null); return; } |
| 167 | |
| 168 | const cacheKey = `${selectedMarket.id}:${timeRange}`; |
| 169 | // Already have this data cached? |
| 170 | if (chartData?.key === cacheKey) return; |
| 171 | |
| 172 | let cancelled = false; |
| 173 | setLoading(true); |
| 174 | |
| 175 | const hours = TIME_HOURS[timeRange]; |
| 176 | const baseUrl = `/api/snapshots?eventId=${encodeURIComponent(selectedMarket.id)}&hours=${hours}`; |
| 177 | const bucketMs = BUCKET_MS[timeRange]; |
| 178 | |
| 179 | (async () => { |
| 180 | try { |
| 181 | if (isMulti) { |
| 182 | const res = await fetch(`${baseUrl}&perMarket=1`); |
| 183 | const result = await res.json(); |
| 184 | const seriesList: SeriesData[] = (result?.series || []).filter((s: SeriesData) => s.data.length >= 2); |
| 185 | const sorted = [...seriesList].sort((a, b) => { |
| 186 | const aLast = a.data[a.data.length - 1]?.prob ?? 0; |
| 187 | const bLast = b.data[b.data.length - 1]?.prob ?? 0; |
| 188 | return bLast - aLast; |
| 189 | }); |
| 190 | const multi = sorted.map(s => ({ |
| 191 | label: s.label, |
| 192 | points: dedupByTime(s.data.map(d => ({ |
| 193 | time: Math.floor(new Date(d.recorded_at).getTime() / 1000), |
| 194 | value: d.prob, |
nothing calls this directly
no test coverage detected