starplot 0.12.4__py2.py3-none-any.whl → 0.13.0__py2.py3-none-any.whl

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.

Potentially problematic release.


This version of starplot might be problematic. Click here for more details.

Files changed (63) hide show
  1. starplot/__init__.py +1 -1
  2. starplot/base.py +371 -60
  3. starplot/callables.py +61 -7
  4. starplot/data/bayer.py +1532 -3
  5. starplot/data/flamsteed.py +2682 -0
  6. starplot/data/library/constellation_borders_inv.gpkg +0 -0
  7. starplot/data/library/constellation_lines_hips.json +3 -1
  8. starplot/data/stars.py +408 -87
  9. starplot/horizon.py +398 -0
  10. starplot/map.py +149 -24
  11. starplot/models/constellation.py +1 -0
  12. starplot/optic.py +26 -14
  13. starplot/plotters/dsos.py +6 -2
  14. starplot/plotters/stars.py +114 -13
  15. starplot/styles/base.py +263 -156
  16. starplot/styles/ext/antique.yml +45 -39
  17. starplot/styles/ext/blue_dark.yml +32 -36
  18. starplot/styles/ext/blue_light.yml +43 -25
  19. starplot/styles/ext/blue_medium.yml +48 -44
  20. starplot/styles/ext/cb_wong.yml +7 -0
  21. starplot/styles/ext/grayscale.yml +13 -7
  22. starplot/styles/ext/grayscale_dark.yml +12 -4
  23. starplot/styles/ext/map.yml +4 -4
  24. starplot/styles/ext/nord.yml +32 -32
  25. starplot/styles/ext/optic.yml +7 -5
  26. starplot/styles/fonts-library/gfs-didot/DESCRIPTION.en_us.html +9 -0
  27. starplot/styles/fonts-library/gfs-didot/GFSDidot-Regular.ttf +0 -0
  28. starplot/styles/fonts-library/gfs-didot/METADATA.pb +16 -0
  29. starplot/styles/fonts-library/gfs-didot/OFL.txt +94 -0
  30. starplot/styles/fonts-library/hind/DESCRIPTION.en_us.html +28 -0
  31. starplot/styles/fonts-library/hind/Hind-Bold.ttf +0 -0
  32. starplot/styles/fonts-library/hind/Hind-Light.ttf +0 -0
  33. starplot/styles/fonts-library/hind/Hind-Medium.ttf +0 -0
  34. starplot/styles/fonts-library/hind/Hind-Regular.ttf +0 -0
  35. starplot/styles/fonts-library/hind/Hind-SemiBold.ttf +0 -0
  36. starplot/styles/fonts-library/hind/METADATA.pb +58 -0
  37. starplot/styles/fonts-library/hind/OFL.txt +93 -0
  38. starplot/styles/fonts-library/inter/Inter-Black.ttf +0 -0
  39. starplot/styles/fonts-library/inter/Inter-BlackItalic.ttf +0 -0
  40. starplot/styles/fonts-library/inter/Inter-Bold.ttf +0 -0
  41. starplot/styles/fonts-library/inter/Inter-BoldItalic.ttf +0 -0
  42. starplot/styles/fonts-library/inter/Inter-ExtraBold.ttf +0 -0
  43. starplot/styles/fonts-library/inter/Inter-ExtraBoldItalic.ttf +0 -0
  44. starplot/styles/fonts-library/inter/Inter-ExtraLight.ttf +0 -0
  45. starplot/styles/fonts-library/inter/Inter-ExtraLightItalic.ttf +0 -0
  46. starplot/styles/fonts-library/inter/Inter-Italic.ttf +0 -0
  47. starplot/styles/fonts-library/inter/Inter-Light.ttf +0 -0
  48. starplot/styles/fonts-library/inter/Inter-LightItalic.ttf +0 -0
  49. starplot/styles/fonts-library/inter/Inter-Medium.ttf +0 -0
  50. starplot/styles/fonts-library/inter/Inter-MediumItalic.ttf +0 -0
  51. starplot/styles/fonts-library/inter/Inter-Regular.ttf +0 -0
  52. starplot/styles/fonts-library/inter/Inter-SemiBold.ttf +0 -0
  53. starplot/styles/fonts-library/inter/Inter-SemiBoldItalic.ttf +0 -0
  54. starplot/styles/fonts-library/inter/Inter-Thin.ttf +0 -0
  55. starplot/styles/fonts-library/inter/Inter-ThinItalic.ttf +0 -0
  56. starplot/styles/fonts-library/inter/LICENSE.txt +92 -0
  57. starplot/styles/fonts.py +15 -0
  58. starplot/styles/markers.py +207 -6
  59. {starplot-0.12.4.dist-info → starplot-0.13.0.dist-info}/METADATA +9 -10
  60. starplot-0.13.0.dist-info/RECORD +101 -0
  61. starplot-0.12.4.dist-info/RECORD +0 -67
  62. {starplot-0.12.4.dist-info → starplot-0.13.0.dist-info}/LICENSE +0 -0
  63. {starplot-0.12.4.dist-info → starplot-0.13.0.dist-info}/WHEEL +0 -0
starplot/base.py CHANGED
@@ -2,6 +2,7 @@ from abc import ABC, abstractmethod
2
2
  from datetime import datetime
3
3
  from typing import Dict, Union, Optional
4
4
  import logging
5
+ import warnings
5
6
 
6
7
  import numpy as np
7
8
  import rtree
@@ -17,6 +18,7 @@ from starplot.data import load, ecliptic
17
18
  from starplot.models.planet import PlanetName, PLANET_LABELS_DEFAULT
18
19
  from starplot.models.moon import MoonPhase
19
20
  from starplot.styles import (
21
+ AnchorPointEnum,
20
22
  PlotStyle,
21
23
  MarkerStyle,
22
24
  ObjectStyle,
@@ -27,9 +29,16 @@ from starplot.styles import (
27
29
  MarkerSymbolEnum,
28
30
  PathStyle,
29
31
  PolygonStyle,
32
+ fonts,
30
33
  )
31
34
  from starplot.styles.helpers import use_style
32
35
 
36
+ # ignore noisy matplotlib warnings
37
+ warnings.filterwarnings(
38
+ "ignore",
39
+ message="Setting the 'color' property will override the edgecolor or facecolor properties",
40
+ )
41
+
33
42
  LOGGER = logging.getLogger("starplot")
34
43
  LOG_HANDLER = logging.StreamHandler()
35
44
  LOG_FORMATTER = logging.Formatter(
@@ -46,6 +55,10 @@ DEFAULT_FOV_STYLE = PolygonStyle(
46
55
 
47
56
  DEFAULT_STYLE = PlotStyle()
48
57
 
58
+ DEFAULT_RESOLUTION = 4096
59
+
60
+ DPI = 100
61
+
49
62
 
50
63
  class BasePlot(ABC):
51
64
  _background_clip_path = None
@@ -55,26 +68,38 @@ class BasePlot(ABC):
55
68
  dt: datetime = None,
56
69
  ephemeris: str = "de421_2001.bsp",
57
70
  style: PlotStyle = DEFAULT_STYLE,
58
- resolution: int = 2048,
71
+ resolution: int = 4096,
59
72
  hide_colliding_labels: bool = True,
73
+ scale: float = 1.0,
74
+ autoscale: bool = False,
60
75
  *args,
61
76
  **kwargs,
62
77
  ):
63
- px = 1 / plt.rcParams["figure.dpi"] # pixel in inches
78
+ px = 1 / DPI # plt.rcParams["figure.dpi"] # pixel in inches
64
79
 
65
- self.pixels_per_point = plt.rcParams["figure.dpi"] / 72
80
+ self.pixels_per_point = DPI / 72
66
81
 
67
82
  self.style = style
68
83
  self.figure_size = resolution * px
69
84
  self.resolution = resolution
70
85
  self.hide_colliding_labels = hide_colliding_labels
71
86
 
87
+ self.scale = scale
88
+ self.autoscale = autoscale
89
+ if self.autoscale:
90
+ self.scale = self.resolution / DEFAULT_RESOLUTION
91
+
72
92
  self.dt = dt or timezone("UTC").localize(datetime.now())
73
93
  self._ephemeris_name = ephemeris
74
94
  self.ephemeris = load(ephemeris)
75
95
 
76
96
  self.labels = []
77
97
  self._labels_rtree = rtree.index.Index()
98
+
99
+ # self.labels = []
100
+ self._constellations_rtree = rtree.index.Index()
101
+ self._stars_rtree = rtree.index.Index()
102
+
78
103
  self._background_clip_path = None
79
104
 
80
105
  self._legend = None
@@ -85,13 +110,14 @@ class BasePlot(ABC):
85
110
  self.logger.setLevel(self.log_level)
86
111
 
87
112
  self.text_border = patheffects.withStroke(
88
- linewidth=self.style.text_border_width,
113
+ linewidth=self.style.text_border_width * self.scale,
89
114
  foreground=self.style.text_border_color.as_hex(),
90
115
  )
91
- self._size_multiplier = self.resolution / 3000
92
116
  self.timescale = load.timescale().from_datetime(self.dt)
93
117
 
94
118
  self._objects = models.ObjectList()
119
+ self._labeled_stars = []
120
+ fonts.load()
95
121
 
96
122
  def _plot_kwargs(self) -> dict:
97
123
  return {}
@@ -107,36 +133,62 @@ class BasePlot(ABC):
107
133
  )
108
134
  return len(ix) > 0
109
135
 
136
+ def _is_object_collision(self, extent) -> bool:
137
+ ix = list(
138
+ self._constellations_rtree.intersection(
139
+ (extent.x0, extent.y0, extent.x1, extent.y1)
140
+ )
141
+ )
142
+ return len(ix) > 0
143
+
144
+ def _is_star_collision(self, extent) -> bool:
145
+ ix = list(
146
+ self._stars_rtree.intersection((extent.x0, extent.y0, extent.x1, extent.y1))
147
+ )
148
+ return len(ix) > 0
149
+
110
150
  def _is_clipped(self, extent) -> bool:
111
151
  return self._background_clip_path is not None and not all(
112
152
  self._background_clip_path.contains_points(extent.get_points())
113
153
  )
114
154
 
115
- def _maybe_remove_label(self, label) -> None:
155
+ def _add_label_to_rtree(self, label, extent=None):
156
+ extent = extent or label.get_window_extent(
157
+ renderer=self.fig.canvas.get_renderer()
158
+ )
159
+ self.labels.append(label)
160
+ self._labels_rtree.insert(
161
+ 0, np.array((extent.x0, extent.y0, extent.x1, extent.y1))
162
+ )
163
+
164
+ def _maybe_remove_label(
165
+ self, label, remove_on_collision=True, remove_on_clipped=True
166
+ ) -> bool:
167
+ """Returns true if the label is removed, else false"""
116
168
  extent = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
117
169
 
118
170
  if any([np.isnan(c) for c in (extent.x0, extent.y0, extent.x1, extent.y1)]):
119
171
  label.remove()
120
- return
172
+ return True
121
173
 
122
- if any(
123
- [
124
- self._is_clipped(extent),
125
- self.hide_colliding_labels and self._is_label_collision(extent),
126
- ]
174
+ if remove_on_clipped and self._is_clipped(extent):
175
+ label.remove()
176
+ return True
177
+
178
+ if remove_on_collision and (
179
+ self._is_label_collision(extent)
180
+ or self._is_object_collision(extent)
181
+ # or self._is_star_collision(extent)
127
182
  ):
128
183
  label.remove()
129
- return
184
+ return True
130
185
 
131
- self.labels.append(label)
132
- self._labels_rtree.insert(
133
- 0, np.array((extent.x0, extent.y0, extent.x1, extent.y1))
134
- )
186
+ return False
135
187
 
136
188
  def _add_legend_handle_marker(self, label: str, style: MarkerStyle):
137
189
  if label is not None and label not in self._legend_handles:
138
190
  s = style.matplot_kwargs()
139
- s["markersize"] = self.style.legend.symbol_size * self._size_multiplier
191
+ s["markersize"] = self.style.legend.symbol_size * self.scale
140
192
  self._legend_handles[label] = Line2D(
141
193
  [],
142
194
  [],
@@ -146,12 +198,176 @@ class BasePlot(ABC):
146
198
  label=label,
147
199
  )
148
200
 
201
+ def _collision_score(self, label) -> int:
202
+ config = {
203
+ "labels": 1.0, # always fail
204
+ "stars": 0.5,
205
+ "constellations": 0.8,
206
+ "anchors": [
207
+ ("bottom right", 0),
208
+ ("top right", 0.2),
209
+ ("top left", 0.5),
210
+ ],
211
+ "on_fail": "plot",
212
+ }
213
+ extent = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
214
+
215
+ if any(
216
+ [np.isnan(c) for c in (extent.x0, extent.y0, extent.x1, extent.y1)]
217
+ ) or self._is_clipped(extent):
218
+ return 1
219
+
220
+ x_labels = (
221
+ len(
222
+ list(
223
+ self._labels_rtree.intersection(
224
+ (extent.x0, extent.y0, extent.x1, extent.y1)
225
+ )
226
+ )
227
+ )
228
+ * config["labels"]
229
+ )
230
+
231
+ if x_labels >= 1:
232
+ return 1
233
+
234
+ x_constellations = (
235
+ len(
236
+ list(
237
+ self._constellations_rtree.intersection(
238
+ (extent.x0, extent.y0, extent.x1, extent.y1)
239
+ )
240
+ )
241
+ )
242
+ * config["constellations"]
243
+ )
244
+
245
+ if x_constellations >= 1:
246
+ return 1
247
+
248
+ x_stars = (
249
+ len(
250
+ list(
251
+ self._stars_rtree.intersection(
252
+ (extent.x0, extent.y0, extent.x1, extent.y1)
253
+ )
254
+ )
255
+ )
256
+ * config["stars"]
257
+ )
258
+ if x_stars >= 1:
259
+ return 1
260
+
261
+ return sum([x_labels, x_constellations, x_stars]) / 3
262
+
263
+ def _text_experimental(
264
+ self,
265
+ ra: float,
266
+ dec: float,
267
+ text: str,
268
+ hide_on_collision: bool = True,
269
+ auto_anchor: bool = True,
270
+ *args,
271
+ **kwargs,
272
+ ) -> None:
273
+ if not text:
274
+ return
275
+
276
+ x, y = self._prepare_coords(ra, dec)
277
+ kwargs["path_effects"] = kwargs.get("path_effects", [self.text_border])
278
+ clip_on = kwargs.get("clip_on") or True
279
+
280
+ def plot_text(**kwargs):
281
+ label = self.ax.annotate(
282
+ text,
283
+ (x, y),
284
+ *args,
285
+ **kwargs,
286
+ **self._plot_kwargs(),
287
+ )
288
+ if clip_on:
289
+ label.set_clip_on(True)
290
+ label.set_clip_path(self._background_clip_path)
291
+ return label
292
+
293
+ def add_label(label):
294
+ extent = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
295
+ self.labels.append(label)
296
+ self._labels_rtree.insert(
297
+ 0, np.array((extent.x0, extent.y0, extent.x1, extent.y1))
298
+ )
299
+
300
+ label = plot_text(**kwargs)
301
+
302
+ if not clip_on:
303
+ add_label(label)
304
+ return
305
+
306
+ if not hide_on_collision and not auto_anchor:
307
+ add_label(label)
308
+ return
309
+
310
+ # removed = self._maybe_remove_label(label)
311
+ collision = self._collision_score(label)
312
+
313
+ if collision == 0:
314
+ add_label(label)
315
+ return
316
+
317
+ label.remove()
318
+
319
+ collision_scores = []
320
+ original_va = kwargs.pop("va", None)
321
+ original_ha = kwargs.pop("ha", None)
322
+ original_offset_x, original_offset_y = kwargs.pop("xytext", (0, 0))
323
+ anchor_fallbacks = self.style.text_anchor_fallbacks
324
+ for i, a in enumerate(anchor_fallbacks):
325
+ d = AnchorPointEnum.from_str(a).as_matplot()
326
+ va, ha = d["va"], d["ha"]
327
+ offset_x, offset_y = original_offset_x, original_offset_y
328
+ if original_ha != ha:
329
+ offset_x *= -1
330
+
331
+ if original_va != va:
332
+ offset_y *= -1
333
+
334
+ if ha == "center":
335
+ offset_x = 0
336
+ offset_y = 0
337
+
338
+ pt_kwargs = dict(**kwargs, va=va, ha=ha, xytext=(offset_x, offset_y))
339
+ label = plot_text(**pt_kwargs)
340
+
341
+ # if not hide_on_collision and i == len(anchor_fallbacks) - 1:
342
+ # break
343
+
344
+ collision = self._collision_score(label)
345
+ if collision == 0:
346
+ add_label(label)
347
+ return
348
+
349
+ if collision < 1:
350
+ collision_scores.append((collision, pt_kwargs))
351
+
352
+ label.remove()
353
+ # removed = self._maybe_remove_label(label)
354
+ # if not removed:
355
+ # break
356
+ if len(collision_scores) > 0:
357
+ best = sorted(collision_scores, key=lambda c: c[0])[0]
358
+ # return
359
+ if best[0] < 1:
360
+ label = plot_text(**best[1])
361
+ add_label(label)
362
+
149
363
  def _text(
150
364
  self,
151
365
  ra: float,
152
366
  dec: float,
153
367
  text: str,
154
368
  hide_on_collision: bool = True,
369
+ force: bool = False,
370
+ clip_on: bool = True,
155
371
  *args,
156
372
  **kwargs,
157
373
  ) -> None:
@@ -160,21 +376,59 @@ class BasePlot(ABC):
160
376
 
161
377
  x, y = self._prepare_coords(ra, dec)
162
378
  kwargs["path_effects"] = kwargs.get("path_effects", [self.text_border])
163
- label = self.ax.annotate(
164
- text,
165
- (x, y),
166
- *args,
167
- **kwargs,
168
- **self._plot_kwargs(),
379
+
380
+ def plot_text(**kwargs):
381
+ label = self.ax.annotate(
382
+ text,
383
+ (x, y),
384
+ *args,
385
+ **kwargs,
386
+ **self._plot_kwargs(),
387
+ )
388
+ if clip_on:
389
+ label.set_clip_on(True)
390
+ label.set_clip_path(self._background_clip_path)
391
+ return label
392
+
393
+ label = plot_text(**kwargs)
394
+
395
+ if force:
396
+ return
397
+
398
+ removed = self._maybe_remove_label(
399
+ label, remove_on_collision=hide_on_collision, remove_on_clipped=clip_on
169
400
  )
170
- if kwargs.get("clip_on") is False:
401
+
402
+ if not removed:
403
+ self._add_label_to_rtree(label)
171
404
  return
172
405
 
173
- label.set_clip_on(True)
174
- label.set_clip_path(self._background_clip_path)
406
+ original_va = kwargs.pop("va", None)
407
+ original_ha = kwargs.pop("ha", None)
408
+ original_offset_x, original_offset_y = kwargs.pop("xytext", (0, 0))
409
+ anchor_fallbacks = self.style.text_anchor_fallbacks
410
+ for i, a in enumerate(anchor_fallbacks):
411
+ d = AnchorPointEnum.from_str(a).as_matplot()
412
+ va, ha = d["va"], d["ha"]
413
+ offset_x, offset_y = original_offset_x, original_offset_y
414
+ if original_ha != ha:
415
+ offset_x *= -1
416
+
417
+ if original_va != va:
418
+ offset_y *= -1
419
+
420
+ if ha == "center":
421
+ offset_x = 0
422
+ offset_y = 0
423
+
424
+ label = plot_text(**kwargs, va=va, ha=ha, xytext=(offset_x, offset_y))
425
+ removed = self._maybe_remove_label(
426
+ label, remove_on_collision=hide_on_collision, remove_on_clipped=clip_on
427
+ )
175
428
 
176
- if hide_on_collision:
177
- self._maybe_remove_label(label)
429
+ if not removed:
430
+ self._add_label_to_rtree(label)
431
+ break
178
432
 
179
433
  @use_style(LabelStyle)
180
434
  def text(
@@ -184,6 +438,8 @@ class BasePlot(ABC):
184
438
  dec: float,
185
439
  style: LabelStyle = None,
186
440
  hide_on_collision: bool = True,
441
+ *args,
442
+ **kwargs,
187
443
  ):
188
444
  """
189
445
  Plots text
@@ -195,15 +451,27 @@ class BasePlot(ABC):
195
451
  style: Styling of the text
196
452
  hide_on_collision: If True, then the text will not be plotted if it collides with another label
197
453
  """
454
+ if not self.in_bounds(ra, dec):
455
+ return
456
+
198
457
  style = style or LabelStyle()
458
+
459
+ if style.offset_x == "auto":
460
+ style.offset_x = 0
461
+
462
+ if style.offset_y == "auto":
463
+ style.offset_y = 0
464
+
199
465
  self._text(
200
466
  ra,
201
467
  dec,
202
468
  text,
203
- **style.matplot_kwargs(self._size_multiplier),
469
+ **style.matplot_kwargs(self.scale),
204
470
  hide_on_collision=hide_on_collision,
205
- xytext=(style.offset_x, style.offset_y),
206
- textcoords="offset pixels",
471
+ xycoords="data",
472
+ xytext=(style.offset_x * self.scale, style.offset_y * self.scale),
473
+ textcoords="offset points",
474
+ **kwargs,
207
475
  )
208
476
 
209
477
  @property
@@ -222,7 +490,7 @@ class BasePlot(ABC):
222
490
  text: Title text to plot
223
491
  style: Styling of the title. If None, then the plot's style (specified when creating the plot) will be used
224
492
  """
225
- style_kwargs = style.matplot_kwargs(self._size_multiplier)
493
+ style_kwargs = style.matplot_kwargs(self.scale)
226
494
  style_kwargs.pop("linespacing", None)
227
495
  style_kwargs["pad"] = style.line_spacing
228
496
  self.ax.set_title(text, **style_kwargs)
@@ -265,7 +533,7 @@ class BasePlot(ABC):
265
533
 
266
534
  self._legend = self.ax.legend(
267
535
  handles=self._legend_handles.values(),
268
- **style.matplot_kwargs(size_multiplier=self._size_multiplier),
536
+ **style.matplot_kwargs(self.scale),
269
537
  **bbox_kwargs,
270
538
  ).set_zorder(
271
539
  # zorder is not a valid kwarg to legend(), so we have to set it afterwards
@@ -317,6 +585,7 @@ class BasePlot(ABC):
317
585
  label: Optional[str] = None,
318
586
  legend_label: str = None,
319
587
  skip_bounds_check: bool = False,
588
+ **kwargs,
320
589
  ) -> None:
321
590
  """Plots a marker
322
591
 
@@ -335,18 +604,36 @@ class BasePlot(ABC):
335
604
 
336
605
  x, y = self._prepare_coords(ra, dec)
337
606
 
338
- self.ax.plot(
607
+ self.ax.scatter(
339
608
  x,
340
609
  y,
341
- **style.marker.matplot_kwargs(size_multiplier=self._size_multiplier),
610
+ **style.marker.matplot_scatter_kwargs(self.scale),
342
611
  **self._plot_kwargs(),
343
- linestyle="None",
344
612
  clip_on=True,
345
613
  clip_path=self._background_clip_path,
614
+ gid=kwargs.get("gid_marker") or "marker",
346
615
  )
347
616
 
348
617
  if label:
349
- self.text(label, ra, dec, style.label)
618
+ label_style = style.label
619
+ if label_style.offset_x == "auto" or label_style.offset_y == "auto":
620
+ marker_size = ((style.marker.size / self.scale) ** 2) * (
621
+ self.scale**2
622
+ )
623
+
624
+ label_style = label_style.offset_from_marker(
625
+ marker_symbol=style.marker.symbol,
626
+ marker_size=marker_size,
627
+ scale=self.scale,
628
+ )
629
+ self.text(
630
+ label,
631
+ ra,
632
+ dec,
633
+ label_style,
634
+ hide_on_collision=self.hide_colliding_labels,
635
+ gid=kwargs.get("gid_label") or "marker-label",
636
+ )
350
637
 
351
638
  if legend_label is not None:
352
639
  self._add_legend_handle_marker(legend_label, style.marker)
@@ -386,18 +673,23 @@ class BasePlot(ABC):
386
673
  (p.ra, p.dec),
387
674
  p.apparent_size,
388
675
  polygon_style,
676
+ gid="planet-marker",
389
677
  )
390
678
  self._add_legend_handle_marker(legend_label, style.marker)
391
679
 
392
680
  if label:
393
- self.text(label.upper(), p.ra, p.dec, style.label)
681
+ self.text(
682
+ label.upper(), p.ra, p.dec, style.label, gid="planet-label"
683
+ )
394
684
  else:
395
685
  self.marker(
396
686
  ra=p.ra,
397
687
  dec=p.dec,
398
- label=label.upper() if label else None,
399
688
  style=style,
689
+ label=label.upper() if label else None,
400
690
  legend_label=legend_label,
691
+ gid_marker="planet-marker",
692
+ gid_label="planet-label",
401
693
  )
402
694
 
403
695
  @use_style(ObjectStyle, "sun")
@@ -438,21 +730,24 @@ class BasePlot(ABC):
438
730
  (s.ra, s.dec),
439
731
  s.apparent_size,
440
732
  style=polygon_style,
733
+ gid="sun-marker",
441
734
  )
442
735
 
443
736
  style.marker.symbol = MarkerSymbolEnum.CIRCLE
444
737
  self._add_legend_handle_marker(legend_label, style.marker)
445
738
 
446
739
  if label:
447
- self.text(label, s.ra, s.dec, style.label)
740
+ self.text(label, s.ra, s.dec, style.label, gid="sun-label")
448
741
 
449
742
  else:
450
743
  self.marker(
451
744
  ra=s.ra,
452
745
  dec=s.dec,
453
- label=label,
454
746
  style=style,
747
+ label=label,
455
748
  legend_label=legend_label,
749
+ gid_marker="sun-marker",
750
+ gid_label="sun-label",
456
751
  )
457
752
 
458
753
  @abstractmethod
@@ -491,7 +786,7 @@ class BasePlot(ABC):
491
786
  patch = patches.Polygon(
492
787
  points,
493
788
  # closed=False, # needs to be false for circles at poles?
494
- **style.matplot_kwargs(size_multiplier=self._size_multiplier),
789
+ **style.matplot_kwargs(self.scale),
495
790
  **kwargs,
496
791
  clip_on=True,
497
792
  clip_path=self._background_clip_path,
@@ -504,6 +799,7 @@ class BasePlot(ABC):
504
799
  style: PolygonStyle,
505
800
  points: list = None,
506
801
  geometry: Polygon = None,
802
+ **kwargs,
507
803
  ):
508
804
  """
509
805
  Plots a polygon.
@@ -522,7 +818,7 @@ class BasePlot(ABC):
522
818
  points = list(zip(*geometry.exterior.coords.xy))
523
819
 
524
820
  _points = [(ra * 15, dec) for ra, dec in points]
525
- self._polygon(_points, style)
821
+ self._polygon(_points, style, gid=kwargs.get("gid") or "polygon")
526
822
 
527
823
  @use_style(PolygonStyle)
528
824
  def rectangle(
@@ -532,7 +828,6 @@ class BasePlot(ABC):
532
828
  width_degrees: float,
533
829
  style: PolygonStyle,
534
830
  angle: float = 0,
535
- *args,
536
831
  **kwargs,
537
832
  ):
538
833
  """Plots a rectangle
@@ -550,7 +845,7 @@ class BasePlot(ABC):
550
845
  width_degrees,
551
846
  angle,
552
847
  )
553
- self._polygon(points, style)
848
+ self._polygon(points, style, gid=kwargs.get("gid") or "polygon")
554
849
 
555
850
  @use_style(PolygonStyle)
556
851
  def ellipse(
@@ -563,6 +858,7 @@ class BasePlot(ABC):
563
858
  num_pts: int = 100,
564
859
  start_angle: int = 0,
565
860
  end_angle: int = 360,
861
+ **kwargs,
566
862
  ):
567
863
  """Plots an ellipse
568
864
 
@@ -584,7 +880,7 @@ class BasePlot(ABC):
584
880
  start_angle,
585
881
  end_angle,
586
882
  )
587
- self._polygon(points, style)
883
+ self._polygon(points, style, gid=kwargs.get("gid") or "polygon")
588
884
 
589
885
  @use_style(PolygonStyle)
590
886
  def circle(
@@ -593,6 +889,7 @@ class BasePlot(ABC):
593
889
  radius_degrees: float,
594
890
  style: PolygonStyle,
595
891
  num_pts: int = 100,
892
+ **kwargs,
596
893
  ):
597
894
  """Plots a circle
598
895
 
@@ -609,10 +906,11 @@ class BasePlot(ABC):
609
906
  style=style,
610
907
  angle=0,
611
908
  num_pts=num_pts,
909
+ gid=kwargs.get("gid") or "polygon",
612
910
  )
613
911
 
614
912
  @use_style(LineStyle)
615
- def line(self, coordinates: list[tuple[float, float]], style: LineStyle):
913
+ def line(self, coordinates: list[tuple[float, float]], style: LineStyle, **kwargs):
616
914
  """Plots a line
617
915
 
618
916
  Args:
@@ -626,7 +924,8 @@ class BasePlot(ABC):
626
924
  y,
627
925
  clip_on=True,
628
926
  clip_path=self._background_clip_path,
629
- **style.matplot_kwargs(self._size_multiplier),
927
+ gid=kwargs.get("gid") or "line",
928
+ **style.matplot_kwargs(self.scale),
630
929
  **self._plot_kwargs(),
631
930
  )
632
931
 
@@ -680,21 +979,24 @@ class BasePlot(ABC):
680
979
  (m.ra, m.dec),
681
980
  m.apparent_size,
682
981
  style=polygon_style,
982
+ gid="moon-marker",
683
983
  )
684
984
 
685
985
  style.marker.symbol = MarkerSymbolEnum.CIRCLE
686
986
  self._add_legend_handle_marker(legend_label, style.marker)
687
987
 
688
988
  if label:
689
- self.text(label, m.ra, m.dec, style.label)
989
+ self.text(label, m.ra, m.dec, style.label, gid="moon-label")
690
990
 
691
991
  else:
692
992
  self.marker(
693
993
  ra=m.ra,
694
994
  dec=m.dec,
695
- label=label,
696
995
  style=style,
996
+ label=label,
697
997
  legend_label=legend_label,
998
+ gid_marker="moon-marker",
999
+ gid_label="moon-label",
698
1000
  )
699
1001
 
700
1002
  def _moon_with_phase(
@@ -761,6 +1063,7 @@ class BasePlot(ABC):
761
1063
  num_pts=num_pts,
762
1064
  angle=0,
763
1065
  end_angle=180, # plot as a semicircle
1066
+ gid="moon-marker",
764
1067
  )
765
1068
  # Plot right side
766
1069
  self.ellipse(
@@ -771,6 +1074,7 @@ class BasePlot(ABC):
771
1074
  num_pts=num_pts,
772
1075
  angle=180,
773
1076
  end_angle=180, # plot as a semicircle
1077
+ gid="moon-marker",
774
1078
  )
775
1079
  # Plot middle
776
1080
  self.ellipse(
@@ -778,6 +1082,7 @@ class BasePlot(ABC):
778
1082
  radius_degrees * 2,
779
1083
  radius_degrees,
780
1084
  style=middle,
1085
+ gid="moon-marker",
781
1086
  )
782
1087
 
783
1088
  def _fov_circle(
@@ -860,17 +1165,16 @@ class BasePlot(ABC):
860
1165
  y,
861
1166
  dash_capstyle=style.line.dash_capstyle,
862
1167
  clip_path=self._background_clip_path,
863
- **style.line.matplot_kwargs(self._size_multiplier),
1168
+ gid="ecliptic-line",
1169
+ **style.line.matplot_kwargs(self.scale),
864
1170
  **self._plot_kwargs(),
865
1171
  )
866
1172
 
867
- if label:
868
- if len(inbounds) > 4:
869
- label_spacing = int(len(inbounds) / 3) or 1
1173
+ if label and len(inbounds) > 4:
1174
+ label_spacing = int(len(inbounds) / 4)
870
1175
 
871
- for i in range(0, len(inbounds), label_spacing):
872
- ra, dec = inbounds[i]
873
- self.text(label, ra, dec, style.label)
1176
+ for ra, dec in [inbounds[label_spacing], inbounds[label_spacing * 2]]:
1177
+ self.text(label, ra, dec, style.label, gid="ecliptic-label")
874
1178
 
875
1179
  @use_style(PathStyle, "celestial_equator")
876
1180
  def celestial_equator(
@@ -897,11 +1201,18 @@ class BasePlot(ABC):
897
1201
  x,
898
1202
  y,
899
1203
  clip_path=self._background_clip_path,
900
- **style.line.matplot_kwargs(self._size_multiplier),
1204
+ gid="celestial-equator-line",
1205
+ **style.line.matplot_kwargs(self.scale),
901
1206
  **self._plot_kwargs(),
902
1207
  )
903
1208
 
904
1209
  if label:
905
1210
  label_spacing = (self.ra_max - self.ra_min) / 3
906
1211
  for ra in np.arange(self.ra_min, self.ra_max, label_spacing):
907
- self.text(label, ra, 0.25, style.label)
1212
+ self.text(
1213
+ label,
1214
+ ra,
1215
+ 0.25,
1216
+ style.label,
1217
+ gid="celestial-equator-label",
1218
+ )