( series: ReadonlyArray<ResolvedCandlestickSeriesConfig>, x: number, y: number, xScale: LinearScale, yScale: LinearScale, barWidthClip: number, )
| 171 | * - Returns the closest in x (min abs dx) among hits; ties broken by smaller `dataIndex` (then smaller `seriesIndex`). |
| 172 | */ |
| 173 | export function findCandlestick( |
| 174 | series: ReadonlyArray<ResolvedCandlestickSeriesConfig>, |
| 175 | x: number, |
| 176 | y: number, |
| 177 | xScale: LinearScale, |
| 178 | yScale: LinearScale, |
| 179 | barWidthClip: number, |
| 180 | ): CandlestickMatch | null { |
| 181 | if (!Number.isFinite(x) || !Number.isFinite(y)) return null; |
| 182 | if (!Number.isFinite(barWidthClip) || !(barWidthClip > 0)) return null; |
| 183 | |
| 184 | const xTarget = xScale.invert(x); |
| 185 | if (!Number.isFinite(xTarget)) return null; |
| 186 | |
| 187 | const halfW = barWidthClip / 2; |
| 188 | |
| 189 | let best: CandlestickMatch | null = null; |
| 190 | let bestDx = Number.POSITIVE_INFINITY; |
| 191 | |
| 192 | const tryUpdate = ( |
| 193 | seriesIndex: number, |
| 194 | dataIndex: number, |
| 195 | point: OHLCDataPoint, |
| 196 | dx: number, |
| 197 | ): void => { |
| 198 | if (!Number.isFinite(dx)) return; |
| 199 | if (dx < bestDx) { |
| 200 | bestDx = dx; |
| 201 | best = { seriesIndex, dataIndex, point }; |
| 202 | return; |
| 203 | } |
| 204 | if (dx === bestDx && best) { |
| 205 | if (dataIndex < best.dataIndex) { |
| 206 | best = { seriesIndex, dataIndex, point }; |
| 207 | } else if (dataIndex === best.dataIndex && seriesIndex < best.seriesIndex) { |
| 208 | best = { seriesIndex, dataIndex, point }; |
| 209 | } |
| 210 | } |
| 211 | }; |
| 212 | |
| 213 | const isBodyHitAt = (p: OHLCDataPoint): boolean => { |
| 214 | const open = getOpen(p); |
| 215 | const close = getClose(p); |
| 216 | if (!Number.isFinite(open) || !Number.isFinite(close)) return false; |
| 217 | |
| 218 | const yOpen = yScale.scale(open); |
| 219 | const yClose = yScale.scale(close); |
| 220 | if (!Number.isFinite(yOpen) || !Number.isFinite(yClose)) return false; |
| 221 | |
| 222 | const yMin = Math.min(yOpen, yClose); |
| 223 | const yMax = Math.max(yOpen, yClose); |
| 224 | return y >= yMin && y <= yMax; |
| 225 | }; |
| 226 | |
| 227 | for (let s = 0; s < series.length; s++) { |
| 228 | const cfg = series[s]; |
| 229 | const data = cfg.data; |
| 230 | const n = data.length; |
no test coverage detected