( series: ReadonlyArray<ResolvedSeriesConfig>, xScale: LinearScale )
| 55 | }>; |
| 56 | |
| 57 | const computeBarHitTestLayout = ( |
| 58 | series: ReadonlyArray<ResolvedSeriesConfig>, |
| 59 | xScale: LinearScale |
| 60 | ): BarHitTestLayout | null => { |
| 61 | // Mirror the bar renderer's shared layout math via `computeBarLayoutPx(...)`, but in xScale range units. |
| 62 | // IMPORTANT: Bar layout depends on all bar series (stacking + grouped slots), not per-series. |
| 63 | const barSeries: { readonly globalSeriesIndex: number; readonly s: ResolvedBarSeriesConfig }[] = []; |
| 64 | for (let i = 0; i < series.length; i++) { |
| 65 | const s = series[i]; |
| 66 | if (s?.type === 'bar') barSeries.push({ globalSeriesIndex: i, s }); |
| 67 | } |
| 68 | if (barSeries.length === 0) return null; |
| 69 | |
| 70 | const layout = computeBarLayoutPx( |
| 71 | barSeries.map((b) => b.s), |
| 72 | xScale |
| 73 | ); |
| 74 | |
| 75 | const barWidthRange = layout.barWidthPx; |
| 76 | const gap = layout.gapPx; |
| 77 | const clusterWidth = layout.clusterWidthPx; |
| 78 | if (!Number.isFinite(barWidthRange) || !(barWidthRange > 0)) return null; |
| 79 | |
| 80 | const clusterIndexByGlobalSeriesIndex = new Map<number, number>(); |
| 81 | for (let i = 0; i < barSeries.length; i++) { |
| 82 | const globalSeriesIndex = barSeries[i].globalSeriesIndex; |
| 83 | const clusterIndex = layout.clusterSlots.clusterIndexBySeries[i] ?? 0; |
| 84 | clusterIndexByGlobalSeriesIndex.set(globalSeriesIndex, clusterIndex); |
| 85 | } |
| 86 | |
| 87 | return { |
| 88 | barWidth: barWidthRange, |
| 89 | gap, |
| 90 | clusterWidth, |
| 91 | clusterIndexByGlobalSeriesIndex, |
| 92 | }; |
| 93 | }; |
| 94 | |
| 95 | const lowerBoundTuple = (data: ReadonlyArray<TuplePoint>, xTarget: number): number => { |
| 96 | let lo = 0; |
no test coverage detected