Coverage for graphica/gui/tools/view_navigation.py: 90.3%

348 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-12 16:38 +0000

1"""表示範囲のマウス操作。モードではなく常に有効(モードとぶつかる操作だけ、モードの最中は譲る)。 

2 

3- 軸の上(目盛りラベルの帯): ホイールでその軸だけ拡大縮小、左ドラッグでその軸の方向に移動、ダブルクリックでその軸をリセット 

4- プロット内: どのモードも無いときの左ドラッグで矩形ズーム、右クリックで1つ前の矩形ズームの前に戻す、 

5 ダブルクリックでサブプロットの X・Y・Y2 をリセット、中ボタンのドラッグでパン 

6 

7計算はピクセル基準にする(データ座標で差を取ると、範囲を変えるたびに基準の座標がずれる。対数・反転の軸もそのまま扱える)。 

8リセットは描画と同じ apply_axis_range を通すので、設定の最小・最大か、自動なら全体の範囲になる。 

9""" 

10import logging 

11import weakref 

12from collections import namedtuple 

13 

14import numpy as np 

15from matplotlib.transforms import Bbox 

16from PySide6.QtCore import Qt 

17 

18from graphica.core.axis_settings import axis_inverted 

19from graphica.gui import app_settings 

20from graphica.gui.rendering.appearance import apply_axis_range, range_signature, waterfall_x_shift 

21from graphica.gui.tools.manager import MOUSE_MODES_BY_NAME 

22from graphica.gui.tools.pointer import legend_at 

23 

24logger = logging.getLogger(__name__) 

25 

26# 軸の上のホイールで、マウスのホイール1段あたりに何倍にするか(環境設定の「拡大の速さ」) 

27WHEEL_ZOOM_BASES = {'slow': 1.1, 'normal': 1.2, 'fast': 1.4} 

28# 1回の通知で変わる量の上限(トラックパッドを速く払っても一気に跳ばないように) 

29MAX_WHEEL_ZOOM_PER_EVENT = 1.5 

30# Qt の angleDelta でホイールの1段(1/8 度単位で 15 度) 

31WHEEL_NOTCH_ANGLE = 120 

32# 目盛りラベルを隠した軸(軸の共有など)でもつかめるよう、帯はプロットの枠から最低これだけ外へ取る 

33AXIS_BAND_MIN_PX = 15 

34# 矩形の幅か高さがこれ未満なら、ズームせずクリックとみなす(matplotlib のズームと同じ) 

35RECT_ZOOM_MIN_PX = 5 

36 

37# axis_key は 'x' / 'y' / 'y2'。ax は範囲を変える Axes('y2' なら第2Y軸) 

38AxisBand = namedtuple("AxisBand", "subplot_index axis_key ax bbox") 

39# ドラッグの始めの変換を固定して持つ(動かしている最中の変換で戻すと、動かした分だけ基準がずれる) 

40_PanTarget = namedtuple("_PanTarget", "ax which inverse span") 

41 

42_AXIS_CURSORS = { 

43 'x': Qt.CursorShape.SizeHorCursor, 

44 'y': Qt.CursorShape.SizeVerCursor, 

45 'y2': Qt.CursorShape.SizeVerCursor, 

46} 

47 

48 

49class ViewNavigationTool: 

50 

51 def __init__(self, app): 

52 self._app = app 

53 self._pan = None # {'button', 'start', 'targets', 'subplot_index', 'axis_keys'} 

54 self._rect_zoom = None # {'subplot_index', 'start', 'bbox'} 

55 # サブプロットの主の Axes → 矩形ズームの前の範囲の列。描き直しで Axes が作り直されたら一緒に消える 

56 self._zoom_history = weakref.WeakKeyDictionary() 

57 self._bands = None 

58 self._bands_key = None 

59 self._cursor_shape = None 

60 

61 def connect(self): 

62 canvas = self._app.canvas 

63 canvas.mpl_connect('scroll_event', self.on_scroll) 

64 canvas.mpl_connect('button_press_event', self.on_press) 

65 canvas.mpl_connect('motion_notify_event', self.on_motion) 

66 canvas.mpl_connect('button_release_event', self.on_release) 

67 # 帯は目盛りラベルの大きさで変わるので、描くたびに測り直す 

68 canvas.mpl_connect('draw_event', self._invalidate_bands) 

69 

70 # --- サブプロットと軸の帯 --- 

71 

72 def _subplot_axes(self, index): 

73 canvas = self._app.canvas 

74 secondary_axes = canvas.all_secondary_axes 

75 secondary = secondary_axes[index] if index < len(secondary_axes) else None 

76 return canvas.all_axes[index], secondary 

77 

78 def subplot_index_of(self, ax): 

79 canvas = self._app.canvas 

80 for index, primary in enumerate(canvas.all_axes): 

81 if ax is primary: 

82 return index 

83 for index, secondary in enumerate(canvas.all_secondary_axes): 83 ↛ 86line 83 didn't jump to line 86 because the loop on line 83 didn't complete

84 if secondary is not None and ax is secondary: 84 ↛ 83line 84 didn't jump to line 83 because the condition on line 84 was always true

85 return index 

86 return None 

87 

88 def _invalidate_bands(self, _event=None): 

89 self._bands = None 

90 # サブプロットが減ったら、もう無い番号の範囲は捨てる(あとで同じ番号が増えたときに古い範囲が出ないように) 

91 overrides = self._app.canvas.view_overrides 

92 for index in [i for i in overrides if i >= len(self._app.canvas.all_axes)]: 

93 del overrides[index] 

94 

95 # --- マウスで変えた範囲を、描き直しても保つ(当て直すのは rendering/appearance.apply_view_override) --- 

96 

97 def remember_view(self, subplot_index, axis_keys): 

98 settings_list = self._app.project.all_plot_settings 

99 if subplot_index >= len(settings_list) or subplot_index >= len(self._app.canvas.all_axes): 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true

100 return 

101 primary, secondary = self._subplot_axes(subplot_index) 

102 entry = self._app.canvas.view_overrides.setdefault(subplot_index, {}) 

103 for axis_key in axis_keys: 

104 ax = secondary if axis_key == 'y2' else primary 

105 if ax is None: 

106 continue 

107 limits = ax.get_xlim() if axis_key == 'x' else ax.get_ylim() 

108 entry[axis_key] = (tuple(float(v) for v in limits), range_signature(settings_list[subplot_index], axis_key)) 

109 self._view_changed() 

110 

111 def forget_view(self, subplot_index, axis_keys=('x', 'y', 'y2')): 

112 entry = self._app.canvas.view_overrides.get(subplot_index) 

113 if entry is None: 

114 return 

115 for axis_key in axis_keys: 

116 entry.pop(axis_key, None) 

117 if not entry: 

118 del self._app.canvas.view_overrides[subplot_index] 

119 self._view_changed() 

120 

121 def forget_all_views(self): 

122 self._app.canvas.view_overrides.clear() 

123 self._view_changed() 

124 

125 def _view_changed(self): 

126 """書き出しのプレビューは表示中の範囲で描くので、範囲が変わったら描き直させる(見えていなければ何もしない)。""" 

127 panel = getattr(self._app, 'export_preview_panel', None) 

128 if panel is not None: 128 ↛ exitline 128 didn't return from function '_view_changed' because the condition on line 128 was always true

129 panel.refresh_preview() 

130 

131 def remember_x_of_every_subplot(self): 

132 """ミニマップは全部の軸の X を変えるので、全部のサブプロットの X を覚える。""" 

133 for index in range(len(self._app.canvas.all_axes)): 

134 self.remember_view(index, ['x']) 

135 

136 def axis_bands(self): 

137 canvas = self._app.canvas 

138 key = tuple( 

139 (id(ax), tuple(ax.bbox.bounds)) for ax in list(canvas.all_axes) + list(canvas.all_secondary_axes) 

140 if ax is not None 

141 ) 

142 if self._bands is None or self._bands_key != key: 

143 self._bands = self._measure_bands() 

144 self._bands_key = key 

145 return self._bands 

146 

147 def _measure_bands(self): 

148 canvas = self._app.canvas 

149 renderer = canvas.get_renderer() 

150 bands = [] 

151 for index, ax in enumerate(canvas.all_axes): 

152 if not ax.get_visible(): 152 ↛ 153line 152 didn't jump to line 153 because the condition on line 152 was never true

153 continue 

154 _, secondary = self._subplot_axes(index) 

155 box = ax.bbox 

156 bands.append(AxisBand(index, 'x', ax, _outer_band(box, ax.xaxis, renderer, 'x'))) 

157 bands.append(AxisBand(index, 'y', ax, _outer_band(box, ax.yaxis, renderer, 'y'))) 

158 if secondary is not None: 

159 bands.append(AxisBand(index, 'y2', secondary, _outer_band(box, secondary.yaxis, renderer, 'y'))) 

160 return bands 

161 

162 def axis_band_at(self, x, y): 

163 """プロットの外の (x, y)(Figure のピクセル、原点は左下)にある軸の帯。無ければ None。""" 

164 if x is None or y is None: 164 ↛ 165line 164 didn't jump to line 165 because the condition on line 164 was never true

165 return None 

166 for band in self.axis_bands(): 

167 if band.bbox.contains(x, y): 

168 return band 

169 return None 

170 

171 # --- 範囲の計算 --- 

172 

173 @staticmethod 

174 def _pixel_span(ax, which): 

175 box = ax.bbox 

176 return (box.x0, box.x1) if which == 'x' else (box.y0, box.y1) 

177 

178 @staticmethod 

179 def _set_limits_from_pixels(ax, which, pixels, inverse): 

180 """ピクセルの (始め, 終わり) を inverse でデータ座標に戻して範囲にする。枠の始め側が範囲の始め側なので反転もそのまま残る。""" 

181 box = ax.bbox 

182 if which == 'x': 

183 points = [(p, box.y0) for p in pixels] 

184 else: 

185 points = [(box.x0, p) for p in pixels] 

186 values = inverse.transform(points)[:, 0 if which == 'x' else 1] 

187 if not np.all(np.isfinite(values)) or values[0] == values[1]: 187 ↛ 188line 187 didn't jump to line 188 because the condition on line 187 was never true

188 return False 

189 if which == 'x': 

190 ax.set_xlim(values[0], values[1]) 

191 else: 

192 ax.set_ylim(values[0], values[1]) 

193 return True 

194 

195 def zoom_axis(self, band, center_px, scale): 

196 """band の軸を、ピクセル位置 center_px を中心に scale 倍(1 未満で拡大)にする。""" 

197 which = 'x' if band.axis_key == 'x' else 'y' 

198 p0, p1 = self._pixel_span(band.ax, which) 

199 pixels = (center_px + (p0 - center_px) * scale, center_px + (p1 - center_px) * scale) 

200 if not self._set_limits_from_pixels(band.ax, which, pixels, band.ax.transData.inverted()): 200 ↛ 201line 200 didn't jump to line 201 because the condition on line 200 was never true

201 return False 

202 self.remember_view(band.subplot_index, [band.axis_key]) 

203 return True 

204 

205 def reset_axis(self, subplot_index, axis_key): 

206 """設定どおりの範囲(最小・最大を決めていればそれ、自動なら全体)に戻す。""" 

207 settings_list = self._app.project.all_plot_settings 

208 if subplot_index >= len(settings_list): 208 ↛ 209line 208 didn't jump to line 209 because the condition on line 208 was never true

209 return 

210 primary, secondary = self._subplot_axes(subplot_index) 

211 ax = secondary if axis_key == 'y2' else primary 

212 if ax is None: 212 ↛ 213line 212 didn't jump to line 213 because the condition on line 212 was never true

213 return 

214 settings = settings_list[subplot_index] 

215 is_category_x = self._app.canvas.axis_is_category_x 

216 shift = waterfall_x_shift(self._app.canvas, subplot_index) if axis_key == 'x' else (0.0, 0.0) 

217 apply_axis_range(ax, settings, axis_key, 

218 subplot_index < len(is_category_x) and is_category_x[subplot_index], shift) 

219 # set_xlim/set_ylim は反転を解くので、設定どおりに戻す 

220 (ax.xaxis if axis_key == 'x' else ax.yaxis).set_inverted(axis_inverted(settings, axis_key)) 

221 self.forget_view(subplot_index, [axis_key]) 

222 

223 def reset_subplot(self, subplot_index): 

224 _, secondary = self._subplot_axes(subplot_index) 

225 for axis_key in ('x', 'y', 'y2') if secondary is not None else ('x', 'y'): 

226 self.reset_axis(subplot_index, axis_key) 

227 primary, _ = self._subplot_axes(subplot_index) 

228 self._zoom_history.pop(primary, None) 

229 

230 def _snapshot(self, subplot_index): 

231 primary, secondary = self._subplot_axes(subplot_index) 

232 return { 

233 'x': primary.get_xlim(), 

234 'y': primary.get_ylim(), 

235 'y2': secondary.get_ylim() if secondary is not None else None, 

236 # 戻したときに「設定どおりの範囲」だったのか「マウスで変えた範囲」だったのかも戻すため 

237 'overrides': dict(self._app.canvas.view_overrides.get(subplot_index, {})), 

238 } 

239 

240 def zoom_to_rect(self, subplot_index, start, end): 

241 """ピクセルの2点で囲んだ範囲にする。幅か高さが RECT_ZOOM_MIN_PX 未満なら何もしない。ズームしたら True。""" 

242 (x0, y0), (x1, y1) = start, end 

243 if abs(x1 - x0) < RECT_ZOOM_MIN_PX or abs(y1 - y0) < RECT_ZOOM_MIN_PX: 

244 return False 

245 primary, secondary = self._subplot_axes(subplot_index) 

246 before = self._snapshot(subplot_index) 

247 xs, ys = sorted((x0, x1)), sorted((y0, y1)) 

248 self._set_limits_from_pixels(primary, 'x', xs, primary.transData.inverted()) 

249 self._set_limits_from_pixels(primary, 'y', ys, primary.transData.inverted()) 

250 if secondary is not None: 

251 self._set_limits_from_pixels(secondary, 'y', ys, secondary.transData.inverted()) 

252 self._zoom_history.setdefault(primary, []).append(before) 

253 self.remember_view(subplot_index, ['x', 'y', 'y2']) 

254 return True 

255 

256 def zoom_back(self, subplot_index): 

257 """1つ前の矩形ズームの前の範囲に戻す。戻せたら True。""" 

258 primary, secondary = self._subplot_axes(subplot_index) 

259 history = self._zoom_history.get(primary) 

260 if not history: 

261 return False 

262 limits = history.pop() 

263 primary.set_xlim(limits['x']) 

264 primary.set_ylim(limits['y']) 

265 if secondary is not None and limits['y2'] is not None: 265 ↛ 266line 265 didn't jump to line 266 because the condition on line 265 was never true

266 secondary.set_ylim(limits['y2']) 

267 if limits['overrides']: 

268 self._app.canvas.view_overrides[subplot_index] = dict(limits['overrides']) 

269 else: 

270 self._app.canvas.view_overrides.pop(subplot_index, None) 

271 self._view_changed() 

272 return True 

273 

274 def _start_pan(self, button, event, targets, subplot_index, axis_keys): 

275 frozen = [ 

276 _PanTarget(ax, which, ax.transData.inverted().frozen(), self._pixel_span(ax, which)) 

277 for ax, which in targets 

278 ] 

279 self._pan = {'button': button, 'start': (event.x, event.y), 'targets': frozen, 

280 'subplot_index': subplot_index, 'axis_keys': axis_keys} 

281 

282 def _pan_to(self, event): 

283 start_x, start_y = self._pan['start'] 

284 for target in self._pan['targets']: 

285 delta = (event.x - start_x) if target.which == 'x' else (event.y - start_y) 

286 p0, p1 = target.span 

287 self._set_limits_from_pixels(target.ax, target.which, (p0 - delta, p1 - delta), target.inverse) 

288 

289 # --- モードとの譲り合い --- 

290 

291 def _active_mode(self): 

292 name = self._app._active_mouse_mode() 

293 return MOUSE_MODES_BY_NAME.get(name) if name else None 

294 

295 # --- イベント --- 

296 

297 def on_scroll(self, event): 

298 if event.inaxes is not None: 

299 return 

300 band = self.axis_band_at(event.x, event.y) 

301 if band is None: 301 ↛ 302line 301 didn't jump to line 302 because the condition on line 301 was never true

302 return 

303 notches = wheel_notches(event) 

304 if not notches: 

305 return 

306 base = WHEEL_ZOOM_BASES.get(app_settings.WHEEL_ZOOM_SPEED.read(self._app.settings), WHEEL_ZOOM_BASES['normal']) 

307 scale = min(max(base ** (-notches), 1 / MAX_WHEEL_ZOOM_PER_EVENT), MAX_WHEEL_ZOOM_PER_EVENT) 

308 center = event.x if band.axis_key == 'x' else event.y 

309 if self.zoom_axis(band, center, scale): 309 ↛ exitline 309 didn't return from function 'on_scroll' because the condition on line 309 was always true

310 self._app.canvas.draw_idle() 

311 

312 def on_press(self, event): 

313 if self._pan is not None or self._rect_zoom is not None: 313 ↛ 314line 313 didn't jump to line 314 because the condition on line 313 was never true

314 return 

315 if event.inaxes is None: 

316 self._on_press_outside_plot(event) 

317 return 

318 index = self.subplot_index_of(event.inaxes) 

319 if index is None: 319 ↛ 320line 319 didn't jump to line 320 because the condition on line 319 was never true

320 return 

321 primary, secondary = self._subplot_axes(index) 

322 mode = self._active_mode() 

323 

324 if event.button == 2: 

325 targets = [(primary, 'x'), (primary, 'y')] + ([(secondary, 'y')] if secondary is not None else []) 

326 self._start_pan(2, event, targets, index, ['x', 'y', 'y2']) 

327 elif event.button == 3: 

328 if (mode is None or not mode.uses_right_click) and self.zoom_back(index): 

329 self._app.canvas.draw_idle() 

330 elif event.button == 1: 330 ↛ exitline 330 didn't return from function 'on_press' because the condition on line 330 was always true

331 if legend_at(self._app.canvas, event) is not None: 

332 return 

333 if event.dblclick: 

334 if mode is None or not mode.uses_left_click: 

335 self.reset_subplot(index) 

336 self._app.canvas.draw_idle() 

337 elif mode is None: 

338 self._rect_zoom = {'subplot_index': index, 'start': (event.x, event.y), 'bbox': primary.bbox} 

339 

340 def _on_press_outside_plot(self, event): 

341 if event.button != 1: 341 ↛ 342line 341 didn't jump to line 342 because the condition on line 341 was never true

342 return 

343 band = self.axis_band_at(event.x, event.y) 

344 if band is None: 

345 self._start_rect_zoom_from_margin(event) 

346 return 

347 # 枠の外へドラッグした凡例が帯に重なっていれば、凡例のドラッグに譲る 

348 if legend_at(self._app.canvas, event) is not None: 

349 return 

350 if event.dblclick: 

351 self.reset_axis(band.subplot_index, band.axis_key) 

352 self._app.canvas.draw_idle() 

353 return 

354 self._start_pan(1, event, [(band.ax, 'x' if band.axis_key == 'x' else 'y')], 

355 band.subplot_index, [band.axis_key]) 

356 

357 def _start_rect_zoom_from_margin(self, event): 

358 """軸の帯の外の余白から、いちばん近いサブプロットの矩形ズームを、その縁に合わせた位置から始める(縁ぴったりまで囲めるように)。""" 

359 if event.dblclick or event.x is None or event.y is None or self._active_mode() is not None: 

360 return 

361 if legend_at(self._app.canvas, event) is not None: 361 ↛ 362line 361 didn't jump to line 362 because the condition on line 361 was never true

362 return 

363 nearest = None 

364 for index, ax in enumerate(self._app.canvas.all_axes): 

365 if not ax.get_visible(): 365 ↛ 366line 365 didn't jump to line 366 because the condition on line 365 was never true

366 continue 

367 box = ax.bbox 

368 dx = max(box.x0 - event.x, 0, event.x - box.x1) 

369 dy = max(box.y0 - event.y, 0, event.y - box.y1) 

370 distance = dx * dx + dy * dy 

371 if nearest is None or distance < nearest[0]: 371 ↛ 364line 371 didn't jump to line 364 because the condition on line 371 was always true

372 nearest = (distance, index, box) 

373 if nearest is None: 373 ↛ 374line 373 didn't jump to line 374 because the condition on line 373 was never true

374 return 

375 _, index, box = nearest 

376 start = (min(max(event.x, box.x0), box.x1), min(max(event.y, box.y0), box.y1)) 

377 self._rect_zoom = {'subplot_index': index, 'start': start, 'bbox': box} 

378 

379 def on_motion(self, event): 

380 if self._pan is not None: 

381 if event.x is not None and event.y is not None: 381 ↛ 384line 381 didn't jump to line 384 because the condition on line 381 was always true

382 self._pan_to(event) 

383 self._app.canvas.draw_idle() 

384 return 

385 if self._rect_zoom is not None: 

386 if event.x is not None and event.y is not None: 386 ↛ 388line 386 didn't jump to line 388 because the condition on line 386 was always true

387 self._draw_rubberband(self._rect_zoom['start'], self._clamp_to_plot(event)) 

388 return 

389 band = self.axis_band_at(event.x, event.y) if event.inaxes is None else None 

390 self._set_cursor(_AXIS_CURSORS[band.axis_key] if band is not None else None) 

391 

392 def on_release(self, event): 

393 if self._pan is not None: 

394 if event.button == self._pan['button']: 394 ↛ 397line 394 didn't jump to line 397 because the condition on line 394 was always true

395 state, self._pan = self._pan, None 

396 self.remember_view(state['subplot_index'], state['axis_keys']) 

397 return 

398 if self._rect_zoom is not None and event.button == 1: 

399 state, self._rect_zoom = self._rect_zoom, None 

400 self._remove_rubberband() 

401 if event.x is None or event.y is None: 401 ↛ 402line 401 didn't jump to line 402 because the condition on line 401 was never true

402 return 

403 if self.zoom_to_rect(state['subplot_index'], state['start'], self._clamp_to_plot(event, state['bbox'])): 

404 self._app.canvas.draw_idle() 

405 

406 # --- 表示 --- 

407 

408 def _clamp_to_plot(self, event, bbox=None): 

409 box = bbox if bbox is not None else self._rect_zoom['bbox'] 

410 return (min(max(event.x, box.x0), box.x1), min(max(event.y, box.y0), box.y1)) 

411 

412 def _draw_rubberband(self, start, end): 

413 canvas = self._app.canvas 

414 # Qt の座標は原点が左上(matplotlib の NavigationToolbar2QT.draw_rubberband と同じ変換) 

415 height = canvas.figure.bbox.height 

416 x0, y0 = start 

417 x1, y1 = end 

418 y0, y1 = height - y0, height - y1 

419 canvas.drawRectangle([int(v) for v in (x0, y0, x1 - x0, y1 - y0)]) 

420 

421 def _remove_rubberband(self): 

422 self._app.canvas.drawRectangle(None) 

423 

424 def _set_cursor(self, shape): 

425 if shape == self._cursor_shape: 

426 return 

427 canvas = self._app.canvas 

428 if shape is None: 

429 canvas.unsetCursor() 

430 else: 

431 canvas.setCursor(shape) 

432 self._cursor_shape = shape 

433 

434 

435def wheel_notches(event): 

436 """ホイールの動きを「マウスのホイール何段分か」で返す(上で正)。指を離した後の慣性の分は 0。 

437 

438 matplotlib の step は、OS がピクセル単位の量を渡すとき(macOS のトラックパッドや Magic Mouse)はピクセル数になり、 

439 1回で何十にもなる。Qt の angleDelta はどの OS でも渡されるので、そちらを段の数に直す。 

440 """ 

441 gui_event = getattr(event, 'guiEvent', None) 

442 if gui_event is None or not hasattr(gui_event, 'angleDelta'): 

443 return event.step 

444 if hasattr(gui_event, 'phase') and gui_event.phase() == Qt.ScrollPhase.ScrollMomentum: 

445 return 0.0 

446 return gui_event.angleDelta().y() / WHEEL_NOTCH_ANGLE 

447 

448 

449def _outer_band(box, axis, renderer, which): 

450 """プロットの枠の外側の、axis の目盛りラベル(と軸ラベル)がある帯。帯の長さはプロットの枠の辺に合わせる。""" 

451 try: 

452 tight = axis.get_tightbbox(renderer) 

453 except (ValueError, RuntimeError): 

454 logger.debug("軸の大きさを測れませんでした", exc_info=True) 

455 tight = None 

456 label_side = axis.get_label_position() 

457 if which == 'x': 

458 if label_side == 'top': 458 ↛ 459line 458 didn't jump to line 459 because the condition on line 458 was never true

459 outer = max(box.y1 + AXIS_BAND_MIN_PX, tight.y1 if tight is not None else -np.inf) 

460 return Bbox.from_extents(box.x0, box.y1, box.x1, outer) 

461 outer = min(box.y0 - AXIS_BAND_MIN_PX, tight.y0 if tight is not None else np.inf) 

462 return Bbox.from_extents(box.x0, outer, box.x1, box.y0) 

463 if label_side == 'right': 

464 outer = max(box.x1 + AXIS_BAND_MIN_PX, tight.x1 if tight is not None else -np.inf) 

465 return Bbox.from_extents(box.x1, box.y0, outer, box.y1) 

466 outer = min(box.x0 - AXIS_BAND_MIN_PX, tight.x0 if tight is not None else np.inf) 

467 return Bbox.from_extents(outer, box.y0, box.x0, box.y1)