(params: {
readonly axisMin: number | null;
readonly axisMax: number | null;
readonly xScale: LinearScale;
readonly plotClipLeft: number;
readonly plotClipRight: number;
readonly canvasCssWidth: number;
readonly visibleRangeMs: number;
readonly measureCtx: CanvasRenderingContext2D | null;
readonly measureCache?: Map<string, number>;
readonly fontSize: number;
readonly fontFamily: string;
})
| 1100 | }; |
| 1101 | |
| 1102 | const computeAdaptiveTimeXAxisTicks = (params: { |
| 1103 | readonly axisMin: number | null; |
| 1104 | readonly axisMax: number | null; |
| 1105 | readonly xScale: LinearScale; |
| 1106 | readonly plotClipLeft: number; |
| 1107 | readonly plotClipRight: number; |
| 1108 | readonly canvasCssWidth: number; |
| 1109 | readonly visibleRangeMs: number; |
| 1110 | readonly measureCtx: CanvasRenderingContext2D | null; |
| 1111 | readonly measureCache?: Map<string, number>; |
| 1112 | readonly fontSize: number; |
| 1113 | readonly fontFamily: string; |
| 1114 | }): { readonly tickCount: number; readonly tickValues: readonly number[] } => { |
| 1115 | const { |
| 1116 | axisMin, |
| 1117 | axisMax, |
| 1118 | xScale, |
| 1119 | plotClipLeft, |
| 1120 | plotClipRight, |
| 1121 | canvasCssWidth, |
| 1122 | visibleRangeMs, |
| 1123 | measureCtx, |
| 1124 | measureCache, |
| 1125 | fontSize, |
| 1126 | fontFamily, |
| 1127 | } = params; |
| 1128 | |
| 1129 | // Domain fallback matches `createAxisRenderer` (use explicit min/max when provided). |
| 1130 | const domainMin = finiteOrNull(axisMin) ?? xScale.invert(plotClipLeft); |
| 1131 | const domainMax = finiteOrNull(axisMax) ?? xScale.invert(plotClipRight); |
| 1132 | |
| 1133 | if (!measureCtx || canvasCssWidth <= 0) { |
| 1134 | return { tickCount: DEFAULT_TICK_COUNT, tickValues: generateLinearTicks(domainMin, domainMax, DEFAULT_TICK_COUNT) }; |
| 1135 | } |
| 1136 | |
| 1137 | // Ensure the measurement font matches the overlay labels. |
| 1138 | measureCtx.font = `${fontSize}px ${fontFamily}`; |
| 1139 | if (measureCache && measureCache.size > 2000) measureCache.clear(); |
| 1140 | |
| 1141 | // Pre-construct the font part of the cache key to avoid repeated concatenation. |
| 1142 | const cacheKeyPrefix = measureCache ? `${fontSize}px ${fontFamily}@@` : null; |
| 1143 | |
| 1144 | for (let tickCount = MAX_TIME_X_TICK_COUNT; tickCount >= MIN_TIME_X_TICK_COUNT; tickCount--) { |
| 1145 | const tickValues = generateLinearTicks(domainMin, domainMax, tickCount); |
| 1146 | |
| 1147 | // Compute label extents in *canvas-local CSS px* and ensure adjacent labels don't overlap. |
| 1148 | let prevRight = Number.NEGATIVE_INFINITY; |
| 1149 | let ok = true; |
| 1150 | |
| 1151 | for (let i = 0; i < tickValues.length; i++) { |
| 1152 | const v = tickValues[i]!; |
| 1153 | const label = formatTimeTickValue(v, visibleRangeMs); |
| 1154 | if (label == null) continue; |
| 1155 | |
| 1156 | const w = (() => { |
| 1157 | if (!cacheKeyPrefix) return measureCtx.measureText(label).width; |
| 1158 | const key = cacheKeyPrefix + label; |
| 1159 | const cached = measureCache!.get(key); |
no test coverage detected