pyredshift 1.9__tar.gz → 1.10__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyredshift
3
- Version: 1.9
3
+ Version: 1.10
4
4
  Summary: Interactive redshifting of 1D astronomical spectra (Python successor of pdlredshift / redshift.f)
5
5
  Home-page: https://github.com/karlglazebrook/pyredshift
6
6
  Author: Karl Glazebrook
@@ -134,7 +134,9 @@ short guide in separate tabs. The essentials:
134
134
  - **Navigate**: drag the mouse for a rubber-band zoom (a purely horizontal
135
135
  drag zooms X only), `z`/`u` and `i`/`o` zoom about the cursor, `[` `]`
136
136
  pan, `w` whole range, `a` autoscale, `h` home. The matplotlib toolbar
137
- works too and stays in sync.
137
+ works too and stays in sync. As in PGPLOT the cursor works over the
138
+ whole window — press `x`/`y`/`e` just outside the axis to select a
139
+ range flush with the plot edge.
138
140
  - **Analyse**: `b` bin, `s` smooth, `_` iterative continuum fit,
139
141
  `m` equivalent width and line flux, `B` zap artefacts, `p` print to PDF.
140
142
  - **Quit**: `q` — the final redshift is reported on the terminal.
@@ -100,7 +100,9 @@ short guide in separate tabs. The essentials:
100
100
  - **Navigate**: drag the mouse for a rubber-band zoom (a purely horizontal
101
101
  drag zooms X only), `z`/`u` and `i`/`o` zoom about the cursor, `[` `]`
102
102
  pan, `w` whole range, `a` autoscale, `h` home. The matplotlib toolbar
103
- works too and stays in sync.
103
+ works too and stays in sync. As in PGPLOT the cursor works over the
104
+ whole window — press `x`/`y`/`e` just outside the axis to select a
105
+ range flush with the plot edge.
104
106
  - **Analyse**: `b` bin, `s` smooth, `_` iterative continuum fit,
105
107
  `m` equivalent width and line flux, `B` zap artefacts, `p` print to PDF.
106
108
  - **Quit**: `q` — the final redshift is reported on the terminal.
@@ -13,7 +13,7 @@ from setuptools import setup
13
13
 
14
14
  setup(
15
15
  name="pyredshift",
16
- version="1.9",
16
+ version="1.10",
17
17
  description="Interactive redshifting of 1D astronomical spectra "
18
18
  "(Python successor of pdlredshift / redshift.f)",
19
19
  long_description=open("README.md").read(),
@@ -57,6 +57,11 @@ redshift at that position (same as ESC + shortcut key)</td></tr>
57
57
  <tr><td>d</td><td>display again</td></tr>
58
58
  </table>
59
59
 
60
+ <p>The cursor works over the whole window, as in PGPLOT: keys and clicks
61
+ outside the axes box extrapolate their coordinates, so <code>x</code>,
62
+ <code>y</code> and <code>e</code> can be pressed just outside the axis to
63
+ select a range flush with (or beyond) the plot edge.</p>
64
+
60
65
  <h2>Analysis</h2>
61
66
  <table>
62
67
  <tr><th>Key</th><th>Action</th></tr>
@@ -44,6 +44,11 @@ V1.8 - Right-click pops up the quick line list as a menu: pick a line
44
44
  lines are now goldenrod on white, yellow on the retro background.
45
45
  V1.9 - First PyPI release. --microns flag (keeps micron wavelengths so
46
46
  micron-mode display is reachable from files); packaging metadata.
47
+ V1.10 - The cursor works over the whole window, as PGPLOT's did: keys and
48
+ clicks outside the axes box extrapolate their coordinates (so
49
+ 'x'/'y'/'e' can be pressed just outside the axis, pdlredshift
50
+ style), the crosshair and the readout follow into the margins,
51
+ and rubber-band drags may extend past the plot edge.
47
52
  """
48
53
 
49
54
  import ctypes
@@ -83,7 +88,7 @@ try:
83
88
  except AttributeError:
84
89
  pass
85
90
 
86
- __version__ = "1.9"
91
+ __version__ = "1.10"
87
92
 
88
93
  C_LIGHT = 2.99792458e8 # m/s
89
94
 
@@ -260,6 +265,19 @@ norm = 1.0
260
265
  # ---------------------------------------------------------------------------
261
266
  # Cursor and blocking input - the pgband() replacement
262
267
  # ---------------------------------------------------------------------------
268
+ def surface_xy(ax_, ev):
269
+ """Data coords for an event anywhere on the figure surface.
270
+
271
+ PGPLOT's cursor was not confined to the axes box - outside it (margins,
272
+ over widgets) extrapolate from the pixel position."""
273
+ if ev.inaxes is ax_:
274
+ return ev.xdata, ev.ydata
275
+ if ev.x is None or ev.y is None:
276
+ return None, None
277
+ xd, yd = ax_.transData.inverted().transform((ev.x, ev.y))
278
+ return float(xd), float(yd)
279
+
280
+
263
281
  class StickyCursor(Cursor):
264
282
  """Cursor that survives full canvas repaints and toolbar pan/zoom.
265
283
 
@@ -268,19 +286,37 @@ class StickyCursor(Cursor):
268
286
  after every repaint. Also reimplements onmove() without the base class's
269
287
  widgetlock check, so the crosshair stays live while the toolbar pan/zoom
270
288
  mode is switched on.
289
+
290
+ The crosshair spans the whole window like PGPLOT's, not just the axes
291
+ box, so the background snapshot and blit cover the full figure - and the
292
+ cursor re-draws the (animated) readout text as part of its pass, since a
293
+ full-figure restore would otherwise erase it.
271
294
  """
272
295
 
273
296
  def __init__(self, ax_, **kwargs):
274
297
  self._last_event = None
275
298
  super().__init__(ax_, **kwargs)
299
+ self.linev.set_clip_on(False) # let the crosshair escape the box
300
+ self.lineh.set_clip_on(False)
276
301
  self.connect_event("draw_event", self._redraw)
277
302
 
303
+ def clear(self, event):
304
+ # As Cursor.clear (mpl 3.9), but snapshot the whole figure: the
305
+ # crosshair now extends into the margins. Runs on draw_event while
306
+ # animated artists are excluded, so the snapshot is clean.
307
+ if self.ignore(event) or self.canvas.is_saving():
308
+ return
309
+ if self.useblit:
310
+ self.background = self.canvas.copy_from_bbox(self.canvas.figure.bbox)
311
+
278
312
  def onmove(self, event):
279
313
  # Copy of Cursor.onmove from matplotlib 3.9, minus the widgetlock
280
- # check (version sensitive - revisit if matplotlib is upgraded)
314
+ # check (version sensitive - revisit if matplotlib is upgraded),
315
+ # with whole-window coords and full-figure blitting.
281
316
  if self.ignore(event):
282
317
  return
283
- if not self.ax.contains(event)[0]:
318
+ xdata, ydata = surface_xy(self.ax, event)
319
+ if xdata is None:
284
320
  self._last_event = None # or _redraw() would resurrect it
285
321
  self.linev.set_visible(False)
286
322
  self.lineh.set_visible(False)
@@ -290,10 +326,16 @@ class StickyCursor(Cursor):
290
326
  return
291
327
  self._last_event = event
292
328
  self.needclear = True
293
- xdata, ydata = self._get_data_coords(event)
294
- self.linev.set_xdata((xdata, xdata))
329
+ # Span the full window: line extents are in axes-fraction coords
330
+ figbb = self.canvas.figure.bbox
331
+ axbb = self.ax.bbox
332
+ self.linev.set_data((xdata, xdata),
333
+ ((figbb.y0 - axbb.y0) / axbb.height,
334
+ (figbb.y1 - axbb.y0) / axbb.height))
295
335
  self.linev.set_visible(self.visible and self.vertOn)
296
- self.lineh.set_ydata((ydata, ydata))
336
+ self.lineh.set_data(((figbb.x0 - axbb.x0) / axbb.width,
337
+ (figbb.x1 - axbb.x0) / axbb.width),
338
+ (ydata, ydata))
297
339
  self.lineh.set_visible(self.visible and self.horizOn)
298
340
  if not (self.visible and (self.vertOn or self.horizOn)):
299
341
  return
@@ -303,13 +345,21 @@ class StickyCursor(Cursor):
303
345
  self.canvas.restore_region(self.background)
304
346
  self.ax.draw_artist(self.linev)
305
347
  self.ax.draw_artist(self.lineh)
306
- self.canvas.blit(self.ax.bbox)
348
+ if readout_artist is not None:
349
+ set_readout_text(event) # restore wiped the corner -
350
+ fig.draw_artist(readout_artist) # redraw with fresh values
351
+ self.canvas.blit(figbb)
307
352
  else:
308
353
  self.canvas.draw_idle()
309
354
 
310
355
  def _redraw(self, event):
311
- if self._last_event is not None:
356
+ if self.active and self._last_event is not None:
312
357
  self.onmove(self._last_event)
358
+ elif readout_lastev is not None:
359
+ # No crosshair to re-draw (fresh cursor after draw_plot, or
360
+ # deactivated while the line menu is up) - the readout must
361
+ # still survive the repaint
362
+ update_readout(readout_lastev)
313
363
 
314
364
 
315
365
  def normkey(ch):
@@ -325,6 +375,11 @@ def pgband(allow_drag=False):
325
375
  Mouse button gives ch='A', as PGPLOT did. Returns ch='q' if the window
326
376
  is closed.
327
377
 
378
+ Like the PGPLOT cursor, key presses and left clicks work over the whole
379
+ window, not just inside the axes box - coordinates are extrapolated from
380
+ the pixel position (handy for 'x'/'y'/'e' just outside the axis).
381
+ Clicks on widgets (the ? button) are still ignored.
382
+
328
383
  With allow_drag=True a left-button drag rubber-band zooms (like 'e') and
329
384
  returns ch='drag' after updating the view state; a purely horizontal drag
330
385
  zooms X only, a purely vertical one Y only. Short drags (<5 pixels)
@@ -343,31 +398,40 @@ def pgband(allow_drag=False):
343
398
  toolbar = getattr(fig.canvas.manager, "toolbar", None)
344
399
  return getattr(toolbar, "mode", "") if toolbar is not None else ""
345
400
 
401
+ def data_xy(ev):
402
+ # PGPLOT-style whole-surface cursor read (see surface_xy)
403
+ return surface_xy(ax, ev)
404
+
346
405
  def on_key(ev):
347
- done(ev.xdata, ev.ydata, normkey(ev.key))
406
+ xd, yd = data_xy(ev)
407
+ done(xd, yd, normkey(ev.key))
348
408
 
349
409
  def on_press(ev):
350
- if ev.inaxes is not ax:
351
- return # clicks elsewhere (margins, the ? button) aren't cursor reads
352
- if allow_drag and ev.button == 3:
410
+ if ev.inaxes is not ax and ev.inaxes is not None:
411
+ return # clicks on widgets (the ? button) aren't cursor reads
412
+ if allow_drag and ev.button == 3 and ev.inaxes is ax:
353
413
  done(ev.xdata, ev.ydata, "menu") # right-click: quick line menu
354
414
  return
355
- if allow_drag and ev.button == 1 and not toolbar_mode():
415
+ xd, yd = data_xy(ev)
416
+ if allow_drag and ev.button == 1 and ev.inaxes is ax and not toolbar_mode():
356
417
  drag["xpx"], drag["ypx"] = ev.x, ev.y # pixels, for threshold
357
- drag["x0"], drag["y0"] = ev.xdata, ev.ydata
358
- drag["x1"], drag["y1"] = ev.xdata, ev.ydata
418
+ drag["x0"], drag["y0"] = xd, yd
419
+ drag["x1"], drag["y1"] = xd, yd
359
420
  drag["rect"] = ax.add_patch(Rectangle(
360
- (ev.xdata, ev.ydata), 0, 0, fill=False,
421
+ (xd, yd), 0, 0, fill=False,
361
422
  edgecolor="red", lw=0.8, ls="--"))
362
423
  else:
363
- done(ev.xdata, ev.ydata, "A")
424
+ done(xd, yd, "A")
364
425
 
365
426
  def on_motion(ev):
366
- if "rect" not in drag or ev.inaxes is not ax:
427
+ if "rect" not in drag or (ev.inaxes is not ax and ev.inaxes is not None):
367
428
  return
368
- drag["x1"], drag["y1"] = ev.xdata, ev.ydata
429
+ xd, yd = data_xy(ev)
430
+ if xd is None:
431
+ return
432
+ drag["x1"], drag["y1"] = xd, yd
369
433
  drag["rect"].set_bounds(drag["x0"], drag["y0"],
370
- ev.xdata - drag["x0"], ev.ydata - drag["y0"])
434
+ xd - drag["x0"], yd - drag["y0"])
371
435
  fig.canvas.draw_idle()
372
436
 
373
437
  def on_release(ev):
@@ -547,31 +611,40 @@ def make_readout():
547
611
 
548
612
  def snapshot_readout(ev):
549
613
  """Cache the bottom-right corner after every full draw (the readout
550
- artist is animated, so it is never part of the cached image), then
551
- re-render the readout so redraws don't blank it - recomputed, so a
552
- new redshift updates the rest wavelength immediately."""
614
+ artist is animated, so it is never part of the cached image). Only a
615
+ snapshot: re-rendering here would bake the text into the buffer before
616
+ the cursor takes ITS (full-figure) snapshot; the cursor's _redraw
617
+ re-renders the readout after every repaint instead."""
553
618
  global readout_bg
554
619
  W, H = fig.bbox.width, fig.bbox.height
555
620
  box = Bbox([[0.40 * W, 0.0], [W, 0.055 * H]])
556
621
  readout_bg = (fig.canvas.copy_from_bbox(box), box)
557
- if readout_lastev is not None:
558
- update_readout(readout_lastev)
559
622
 
560
623
 
561
- def update_readout(ev):
624
+ def set_readout_text(ev):
625
+ """Recompute the readout for an event - separate from the blit so the
626
+ cursor can refresh the text mid-pass (a new redshift must update the
627
+ rest wavelength immediately, not on the next mouse move)."""
562
628
  global readout_lastev
563
- if readout_artist is None or readout_bg is None or f is None:
629
+ if readout_artist is None or f is None:
564
630
  return
565
631
  readout_lastev = ev
566
- if ev.inaxes is ax and ev.xdata is not None:
567
- i = int(np.argmin(np.abs(w - ev.xdata)))
632
+ xd, yd = surface_xy(ax, ev)
633
+ if xd is not None: # whole window, like the crosshair (pix clamps at ends)
634
+ i = int(np.argmin(np.abs(w - xd)))
568
635
  wfmt = "%.5f" if micron_mode else "%.2f"
569
- rest = wfmt % (ev.xdata / (1 + zshift)) if found else "-"
636
+ rest = wfmt % (xd / (1 + zshift)) if found else "-"
570
637
  text = ("pix %d y %.4g λ %s rest %s flux %.4g"
571
- % (i, ev.ydata, wfmt % ev.xdata, rest, f[i]))
638
+ % (i, yd, wfmt % xd, rest, f[i]))
572
639
  else:
573
640
  text = ""
574
641
  readout_artist.set_text(text)
642
+
643
+
644
+ def update_readout(ev):
645
+ if readout_artist is None or readout_bg is None or f is None:
646
+ return
647
+ set_readout_text(ev)
575
648
  bg, box = readout_bg
576
649
  fig.canvas.restore_region(bg)
577
650
  fig.draw_artist(readout_artist)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyredshift
3
- Version: 1.9
3
+ Version: 1.10
4
4
  Summary: Interactive redshifting of 1D astronomical spectra (Python successor of pdlredshift / redshift.f)
5
5
  Home-page: https://github.com/karlglazebrook/pyredshift
6
6
  Author: Karl Glazebrook
@@ -134,7 +134,9 @@ short guide in separate tabs. The essentials:
134
134
  - **Navigate**: drag the mouse for a rubber-band zoom (a purely horizontal
135
135
  drag zooms X only), `z`/`u` and `i`/`o` zoom about the cursor, `[` `]`
136
136
  pan, `w` whole range, `a` autoscale, `h` home. The matplotlib toolbar
137
- works too and stays in sync.
137
+ works too and stays in sync. As in PGPLOT the cursor works over the
138
+ whole window — press `x`/`y`/`e` just outside the axis to select a
139
+ range flush with the plot edge.
138
140
  - **Analyse**: `b` bin, `s` smooth, `_` iterative continuum fit,
139
141
  `m` equivalent width and line flux, `B` zap artefacts, `p` print to PDF.
140
142
  - **Quit**: `q` — the final redshift is reported on the terminal.
File without changes
File without changes
File without changes
File without changes