starplot 0.18.3__py2.py3-none-any.whl → 0.19.2__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.
Files changed (38) hide show
  1. starplot/__init__.py +33 -27
  2. starplot/config.py +11 -0
  3. starplot/data/__init__.py +3 -5
  4. starplot/data/catalogs.py +24 -12
  5. starplot/data/constellations.py +1 -0
  6. starplot/data/db.py +1 -7
  7. starplot/geod.py +3 -4
  8. starplot/geometry.py +17 -1
  9. starplot/mixins.py +11 -0
  10. starplot/models/__init__.py +3 -1
  11. starplot/models/constellation.py +20 -3
  12. starplot/models/milky_way.py +30 -0
  13. starplot/models/moon.py +1 -1
  14. starplot/models/observer.py +11 -2
  15. starplot/models/planet.py +1 -1
  16. starplot/models/sun.py +1 -1
  17. starplot/plots/__init__.py +6 -0
  18. starplot/{base.py → plots/base.py} +107 -456
  19. starplot/{horizon.py → plots/horizon.py} +12 -10
  20. starplot/{map.py → plots/map.py} +11 -7
  21. starplot/{optic.py → plots/optic.py} +21 -30
  22. starplot/{zenith.py → plots/zenith.py} +37 -8
  23. starplot/plotters/__init__.py +9 -7
  24. starplot/plotters/arrow.py +1 -1
  25. starplot/plotters/constellations.py +46 -61
  26. starplot/plotters/dsos.py +33 -16
  27. starplot/plotters/experimental.py +0 -1
  28. starplot/plotters/milkyway.py +15 -6
  29. starplot/plotters/stars.py +19 -36
  30. starplot/plotters/text.py +524 -0
  31. starplot/styles/__init__.py +4 -4
  32. starplot/styles/base.py +1 -13
  33. {starplot-0.18.3.dist-info → starplot-0.19.2.dist-info}/METADATA +2 -1
  34. {starplot-0.18.3.dist-info → starplot-0.19.2.dist-info}/RECORD +37 -35
  35. starplot/data/library/sky.db +0 -0
  36. {starplot-0.18.3.dist-info → starplot-0.19.2.dist-info}/WHEEL +0 -0
  37. {starplot-0.18.3.dist-info → starplot-0.19.2.dist-info}/entry_points.txt +0 -0
  38. {starplot-0.18.3.dist-info → starplot-0.19.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,14 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from typing import Dict, Union, Optional
3
- from random import randrange
4
3
  import logging
5
4
 
6
5
  import numpy as np
7
- import rtree
8
6
  from matplotlib import patches
9
7
  from matplotlib import pyplot as plt, patheffects
10
8
  from matplotlib.axes import Axes
11
9
  from matplotlib.figure import Figure
12
10
  from matplotlib.lines import Line2D
13
- from shapely import Polygon, Point
11
+ from shapely import Polygon, LineString
14
12
 
15
13
  from starplot.coordinates import CoordinateSystem
16
14
  from starplot import geod, models, warnings
@@ -22,7 +20,6 @@ from starplot.models.moon import MoonPhase
22
20
  from starplot.models.optics import Optic, Camera
23
21
  from starplot.models.observer import Observer
24
22
  from starplot.styles import (
25
- AnchorPointEnum,
26
23
  PlotStyle,
27
24
  MarkerStyle,
28
25
  ObjectStyle,
@@ -34,11 +31,8 @@ from starplot.styles import (
34
31
  GradientDirection,
35
32
  fonts,
36
33
  )
34
+ from starplot.plotters.text import TextPlotterMixin, CollisionHandler
37
35
  from starplot.styles.helpers import use_style
38
- from starplot.geometry import (
39
- unwrap_polygon_360,
40
- random_point_in_polygon_at_distance,
41
- )
42
36
  from starplot.profile import profile
43
37
 
44
38
  LOGGER = logging.getLogger("starplot")
@@ -49,14 +43,12 @@ LOG_FORMATTER = logging.Formatter(
49
43
  LOG_HANDLER.setFormatter(LOG_FORMATTER)
50
44
  LOGGER.addHandler(LOG_HANDLER)
51
45
 
52
- DEFAULT_STYLE = PlotStyle()
53
-
54
46
  DEFAULT_RESOLUTION = 4096
55
47
 
56
48
  DPI = 100
57
49
 
58
50
 
59
- class BasePlot(ABC):
51
+ class BasePlot(TextPlotterMixin, ABC):
60
52
  _background_clip_path = None
61
53
  _clip_path_polygon: Polygon = None # clip path in display coordinates
62
54
  _coordinate_system = CoordinateSystem.RA_DEC
@@ -79,19 +71,24 @@ class BasePlot(ABC):
79
71
  The plot's style.
80
72
  """
81
73
 
74
+ collision_handler: CollisionHandler
75
+ """Default [collision handler][starplot.CollisionHandler] for the plot."""
76
+
82
77
  def __init__(
83
78
  self,
84
- observer: Observer = Observer(),
79
+ observer: Observer = None,
85
80
  ephemeris: str = "de421.bsp",
86
- style: PlotStyle = DEFAULT_STYLE,
81
+ style: PlotStyle = None,
87
82
  resolution: int = 4096,
88
- hide_colliding_labels: bool = True,
83
+ collision_handler: CollisionHandler = None,
89
84
  scale: float = 1.0,
90
85
  autoscale: bool = False,
91
86
  suppress_warnings: bool = True,
92
87
  *args,
93
88
  **kwargs,
94
89
  ):
90
+ super().__init__(*args, **kwargs)
91
+
95
92
  if StarplotSettings.svg_text_type == SvgTextType.PATH:
96
93
  plt.rcParams["svg.fonttype"] = "path"
97
94
  else:
@@ -102,10 +99,10 @@ class BasePlot(ABC):
102
99
 
103
100
  self.language = StarplotSettings.language
104
101
 
105
- self.style = style
102
+ self.style = style or PlotStyle()
106
103
  self.figure_size = resolution * px
107
104
  self.resolution = resolution
108
- self.hide_colliding_labels = hide_colliding_labels
105
+ self.collision_handler = collision_handler or CollisionHandler()
109
106
 
110
107
  self.scale = scale
111
108
  self.autoscale = autoscale
@@ -115,23 +112,19 @@ class BasePlot(ABC):
115
112
  if suppress_warnings:
116
113
  warnings.suppress()
117
114
 
118
- self.observer = observer
115
+ self.observer = observer or Observer()
119
116
  self._ephemeris_name = ephemeris
120
117
  self.ephemeris = load(ephemeris)
121
118
  self.earth = self.ephemeris["earth"]
122
119
 
123
- self.labels = []
124
- self._labels_rtree = rtree.index.Index()
125
- self._constellations_rtree = rtree.index.Index()
126
- self._stars_rtree = rtree.index.Index()
127
- self._markers_rtree = rtree.index.Index()
128
-
129
120
  self._background_clip_path = None
130
121
 
131
122
  self._legend = None
132
123
  self._legend_handles = {}
133
124
 
134
- self.log_level = logging.DEBUG if kwargs.get("debug") else logging.ERROR
125
+ self.debug = StarplotSettings.debug or bool(kwargs.get("debug"))
126
+ self.debug_text = StarplotSettings.debug or bool(kwargs.get("debug_text"))
127
+ self.log_level = logging.DEBUG if self.debug else logging.ERROR
135
128
  self.logger = LOGGER
136
129
  self.logger.setLevel(self.log_level)
137
130
 
@@ -154,127 +147,6 @@ class BasePlot(ABC):
154
147
  coords = self._background_clip_path.get_verts()
155
148
  self._clip_path_polygon = Polygon(coords).buffer(-1 * buffer)
156
149
 
157
- def _is_label_collision(self, bbox) -> bool:
158
- ix = list(self._labels_rtree.intersection(bbox))
159
- return len(ix) > 0
160
-
161
- def _is_constellation_collision(self, bbox) -> bool:
162
- ix = list(self._constellations_rtree.intersection(bbox))
163
- return len(ix) > 0
164
-
165
- def _is_star_collision(self, bbox) -> bool:
166
- ix = list(self._stars_rtree.intersection(bbox))
167
- return len(ix) > 0
168
-
169
- def _is_marker_collision(self, bbox) -> bool:
170
- ix = list(self._markers_rtree.intersection(bbox))
171
- return len(ix) > 0
172
-
173
- def _is_clipped(self, points) -> bool:
174
- p = self._clip_path_polygon
175
-
176
- for x, y in points:
177
- if not p.contains(Point(x, y)):
178
- return True
179
-
180
- return False
181
-
182
- def _add_label_to_rtree(self, label, extent=None):
183
- extent = extent or label.get_window_extent(
184
- renderer=self.fig.canvas.get_renderer()
185
- )
186
- self.labels.append(label)
187
- self._labels_rtree.insert(
188
- 0, np.array((extent.x0 - 1, extent.y0 - 1, extent.x1 + 1, extent.y1 + 1))
189
- )
190
-
191
- def _is_open_space(
192
- self,
193
- bbox: tuple[float, float, float, float],
194
- padding=0,
195
- avoid_clipped=True,
196
- avoid_label_collisions=True,
197
- avoid_marker_collisions=True,
198
- avoid_constellation_collision=True,
199
- ) -> bool:
200
- """
201
- Returns true if the boox covers an open space (i.e. no collisions)
202
-
203
- Args:
204
- bbox: 4-element tuple of lower left and upper right coordinates
205
- """
206
- x0, y0, x1, y1 = bbox
207
- points = [(x0, y0), (x1, y1)]
208
- bbox = (
209
- x0 - padding,
210
- y0 - padding,
211
- x1 + padding,
212
- y1 + padding,
213
- )
214
-
215
- if any([np.isnan(c) for c in (x0, y0, x1, y1)]):
216
- return False
217
-
218
- if avoid_clipped and self._is_clipped(points):
219
- return False
220
-
221
- if avoid_label_collisions and self._is_label_collision(bbox):
222
- return False
223
-
224
- if avoid_marker_collisions and (
225
- self._is_star_collision(bbox) or self._is_marker_collision(bbox)
226
- ):
227
- return False
228
-
229
- if avoid_constellation_collision and self._is_constellation_collision(bbox):
230
- return False
231
-
232
- return True
233
-
234
- def _get_label_bbox(self, label):
235
- extent = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
236
- return (extent.x0, extent.y0, extent.x1, extent.y1)
237
-
238
- def _maybe_remove_label(
239
- self,
240
- label,
241
- remove_on_collision=True,
242
- remove_on_clipped=True,
243
- remove_on_constellation_collision=True,
244
- padding=0,
245
- ) -> bool:
246
- """Returns true if the label is removed, else false"""
247
- extent = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
248
- bbox = (
249
- extent.x0 - padding,
250
- extent.y0 - padding,
251
- extent.x1 + padding,
252
- extent.y1 + padding,
253
- )
254
- points = [(extent.x0, extent.y0), (extent.x1, extent.y1)]
255
-
256
- if any([np.isnan(c) for c in (extent.x0, extent.y0, extent.x1, extent.y1)]):
257
- label.remove()
258
- return True
259
-
260
- if remove_on_clipped and self._is_clipped(points):
261
- label.remove()
262
- return True
263
-
264
- if remove_on_collision and (
265
- self._is_label_collision(bbox)
266
- or self._is_star_collision(bbox)
267
- or self._is_marker_collision(bbox)
268
- ):
269
- label.remove()
270
- return True
271
-
272
- if remove_on_constellation_collision and self._is_constellation_collision(bbox):
273
- label.remove()
274
- return True
275
-
276
- return False
277
-
278
150
  def _add_legend_handle_marker(self, label: str, style: MarkerStyle):
279
151
  if label is not None and label not in self._legend_handles:
280
152
  s = style.matplot_kwargs()
@@ -288,235 +160,6 @@ class BasePlot(ABC):
288
160
  label=label,
289
161
  )
290
162
 
291
- def _collision_score(self, label) -> int:
292
- config = {
293
- "labels": 1.0, # always fail
294
- "stars": 0.5,
295
- "constellations": 0.8,
296
- "anchors": [
297
- ("bottom right", 0),
298
- ("top right", 0.2),
299
- ("top left", 0.5),
300
- ],
301
- "on_fail": "plot",
302
- }
303
- extent = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
304
-
305
- if any(
306
- [np.isnan(c) for c in (extent.x0, extent.y0, extent.x1, extent.y1)]
307
- ) or self._is_clipped(extent):
308
- return 1
309
-
310
- x_labels = (
311
- len(
312
- list(
313
- self._labels_rtree.intersection(
314
- (extent.x0, extent.y0, extent.x1, extent.y1)
315
- )
316
- )
317
- )
318
- * config["labels"]
319
- )
320
-
321
- if x_labels >= 1:
322
- return 1
323
-
324
- x_constellations = (
325
- len(
326
- list(
327
- self._constellations_rtree.intersection(
328
- (extent.x0, extent.y0, extent.x1, extent.y1)
329
- )
330
- )
331
- )
332
- * config["constellations"]
333
- )
334
-
335
- if x_constellations >= 1:
336
- return 1
337
-
338
- x_stars = (
339
- len(
340
- list(
341
- self._stars_rtree.intersection(
342
- (extent.x0, extent.y0, extent.x1, extent.y1)
343
- )
344
- )
345
- )
346
- * config["stars"]
347
- )
348
- if x_stars >= 1:
349
- return 1
350
-
351
- return sum([x_labels, x_constellations, x_stars]) / 3
352
-
353
- def _text(self, x, y, text, **kwargs):
354
- label = self.ax.annotate(
355
- text,
356
- (x, y),
357
- **kwargs,
358
- **self._plot_kwargs(),
359
- )
360
- if kwargs.get("clip_on"):
361
- label.set_clip_on(True)
362
- label.set_clip_path(self._background_clip_path)
363
- return label
364
-
365
- def _text_point(
366
- self,
367
- ra: float,
368
- dec: float,
369
- text: str,
370
- hide_on_collision: bool = True,
371
- force: bool = False,
372
- clip_on: bool = True,
373
- **kwargs,
374
- ):
375
- if not text:
376
- return None
377
-
378
- x, y = self._prepare_coords(ra, dec)
379
-
380
- if StarplotSettings.svg_text_type == SvgTextType.PATH:
381
- kwargs["path_effects"] = kwargs.get("path_effects", [self.text_border])
382
-
383
- remove_on_constellation_collision = kwargs.pop(
384
- "remove_on_constellation_collision", True
385
- )
386
-
387
- original_va = kwargs.pop("va", None)
388
- original_ha = kwargs.pop("ha", None)
389
- original_offset_x, original_offset_y = kwargs.pop("xytext", (0, 0))
390
-
391
- anchors = [(original_va, original_ha)]
392
- for a in self.style.text_anchor_fallbacks:
393
- d = AnchorPointEnum.from_str(a).as_matplot()
394
- anchors.append((d["va"], d["ha"]))
395
-
396
- for va, ha in anchors:
397
- offset_x, offset_y = original_offset_x, original_offset_y
398
- if original_ha != ha:
399
- offset_x *= -1
400
-
401
- if original_va != va:
402
- offset_y *= -1
403
-
404
- if ha == "center":
405
- offset_x = 0
406
- offset_y = 0
407
-
408
- label = self._text(
409
- x, y, text, **kwargs, va=va, ha=ha, xytext=(offset_x, offset_y)
410
- )
411
- removed = self._maybe_remove_label(
412
- label,
413
- remove_on_collision=hide_on_collision,
414
- remove_on_clipped=clip_on,
415
- remove_on_constellation_collision=remove_on_constellation_collision,
416
- )
417
-
418
- if force or not removed:
419
- self._add_label_to_rtree(label)
420
- return label
421
-
422
- def _text_area(
423
- self,
424
- ra: float,
425
- dec: float,
426
- text: str,
427
- area,
428
- hide_on_collision: bool = True,
429
- force: bool = False,
430
- clip_on: bool = True,
431
- settings: dict = None,
432
- **kwargs,
433
- ) -> None:
434
- kwargs["va"] = "center"
435
- kwargs["ha"] = "center"
436
-
437
- if StarplotSettings.svg_text_type == SvgTextType.PATH:
438
- kwargs["path_effects"] = kwargs.get("path_effects", [self.text_border])
439
-
440
- avoid_constellation_lines = settings.get("avoid_constellation_lines", False)
441
- padding = settings.get("label_padding", 3)
442
- settings.get("buffer", 0.1)
443
- max_distance = settings.get("max_distance", 300)
444
- distance_step_size = settings.get("distance_step_size", 1)
445
- point_iterations = settings.get("point_generation_max_iterations", 500)
446
- random_seed = settings.get("seed")
447
-
448
- attempts = 0
449
- height = None
450
- width = None
451
- bbox = None
452
- areas = (
453
- [p for p in area.geoms] if "MultiPolygon" == str(area.geom_type) else [area]
454
- )
455
- new_areas = []
456
- origin = Point(ra, dec)
457
-
458
- for a in areas:
459
- unwrapped = unwrap_polygon_360(a)
460
- # new_buffer = unwrapped.area / 10 * -1 * buffer * self.scale
461
- # new_buffer = -1 * buffer * self.scale
462
- # new_poly = unwrapped.buffer(new_buffer)
463
- new_areas.append(unwrapped)
464
-
465
- for d in range(0, max_distance, distance_step_size):
466
- distance = d / 20
467
- poly = randrange(len(new_areas))
468
- point = random_point_in_polygon_at_distance(
469
- new_areas[poly],
470
- origin_point=origin,
471
- distance=distance,
472
- max_iterations=point_iterations,
473
- seed=random_seed,
474
- )
475
-
476
- if point is None:
477
- continue
478
-
479
- x, y = self._prepare_coords(point.x, point.y)
480
-
481
- if height and width:
482
- data_xy = self._proj.transform_point(x, y, self._crs)
483
- display_x, display_y = self.ax.transData.transform(data_xy)
484
- bbox = (
485
- display_x - width / 2,
486
- display_y - height / 2,
487
- display_x + width / 2,
488
- display_y + height / 2,
489
- )
490
- label = None
491
-
492
- else:
493
- label = self._text(x, y, text, **kwargs)
494
- bbox = self._get_label_bbox(label)
495
- height = bbox[3] - bbox[1]
496
- width = bbox[2] - bbox[0]
497
-
498
- is_open = self._is_open_space(
499
- bbox,
500
- padding=padding,
501
- avoid_clipped=clip_on,
502
- avoid_constellation_collision=avoid_constellation_lines,
503
- avoid_marker_collisions=hide_on_collision,
504
- avoid_label_collisions=hide_on_collision,
505
- )
506
-
507
- # # TODO : remove label if not fully inside area?
508
-
509
- attempts += 1
510
-
511
- if is_open and label is None:
512
- label = self._text(x, y, text, **kwargs)
513
-
514
- if is_open:
515
- self._add_label_to_rtree(label)
516
- return label
517
- elif label is not None:
518
- label.remove()
519
-
520
163
  @property
521
164
  def magnitude_range(self) -> tuple[float, float]:
522
165
  """
@@ -525,64 +168,6 @@ class BasePlot(ABC):
525
168
  mags = [s.magnitude for s in self.objects.stars]
526
169
  return (min(mags), max(mags))
527
170
 
528
- @use_style(LabelStyle)
529
- def text(
530
- self,
531
- text: str,
532
- ra: float,
533
- dec: float,
534
- style: LabelStyle = None,
535
- hide_on_collision: bool = True,
536
- **kwargs,
537
- ):
538
- """
539
- Plots text
540
-
541
- Args:
542
- text: Text to plot
543
- ra: Right ascension of text (0...360)
544
- dec: Declination of text (-90...90)
545
- style: Styling of the text
546
- hide_on_collision: If True, then the text will not be plotted if it collides with another label
547
- """
548
- if not text:
549
- return
550
-
551
- style = style or LabelStyle()
552
-
553
- if style.offset_x == "auto":
554
- style.offset_x = 0
555
-
556
- if style.offset_y == "auto":
557
- style.offset_y = 0
558
-
559
- if kwargs.get("area"):
560
- return self._text_area(
561
- ra,
562
- dec,
563
- text,
564
- **style.matplot_kwargs(self.scale),
565
- area=kwargs.pop("area"),
566
- hide_on_collision=hide_on_collision,
567
- xycoords="data",
568
- xytext=(style.offset_x * self.scale, style.offset_y * self.scale),
569
- textcoords="offset points",
570
- settings=kwargs.pop("auto_adjust_settings"),
571
- **kwargs,
572
- )
573
- else:
574
- return self._text_point(
575
- ra,
576
- dec,
577
- text,
578
- **style.matplot_kwargs(self.scale),
579
- hide_on_collision=hide_on_collision,
580
- xycoords="data",
581
- xytext=(style.offset_x * self.scale, style.offset_y * self.scale),
582
- textcoords="offset points",
583
- **kwargs,
584
- )
585
-
586
171
  @property
587
172
  def objects(self) -> models.ObjectList:
588
173
  """
@@ -637,6 +222,7 @@ class BasePlot(ABC):
637
222
  label: Optional[str] = None,
638
223
  legend_label: str = None,
639
224
  skip_bounds_check: bool = False,
225
+ collision_handler: CollisionHandler = None,
640
226
  **kwargs,
641
227
  ) -> None:
642
228
  """Plots a marker
@@ -648,6 +234,7 @@ class BasePlot(ABC):
648
234
  style: Styling for the marker
649
235
  legend_label: How to label the marker in the legend. If `None`, then the marker will not be added to the legend
650
236
  skip_bounds_check: If True, then don't check the marker coordinates to ensure they're within the bounds of the plot. If you're plotting many markers, setting this to True can speed up plotting time.
237
+ collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on label collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
651
238
 
652
239
  """
653
240
 
@@ -700,7 +287,7 @@ class BasePlot(ABC):
700
287
  ra,
701
288
  dec,
702
289
  label_style,
703
- hide_on_collision=self.hide_colliding_labels,
290
+ collision_handler=collision_handler or self.collision_handler,
704
291
  gid=kwargs.get("gid_label") or "marker-label",
705
292
  )
706
293
 
@@ -714,6 +301,7 @@ class BasePlot(ABC):
714
301
  true_size: bool = False,
715
302
  labels: Dict[PlanetName, str] = PLANET_LABELS_DEFAULT,
716
303
  legend_label: str = "Planet",
304
+ collision_handler: CollisionHandler = None,
717
305
  ) -> None:
718
306
  """
719
307
  Plots the planets.
@@ -725,6 +313,7 @@ class BasePlot(ABC):
725
313
  true_size: If True, then each planet's true apparent size in the sky will be plotted. If False, then the style's marker size will be used.
726
314
  labels: How the planets will be labeled on the plot and legend. If not specified, then the planet's name will be used (see [`Planet`][starplot.models.planet.PlanetName])
727
315
  legend_label: How to label the planets in the legend. If `None`, then the planets will not be added to the legend
316
+ collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on label collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
728
317
  """
729
318
  labels = labels or {}
730
319
  planets = models.Planet.all(
@@ -732,6 +321,7 @@ class BasePlot(ABC):
732
321
  )
733
322
 
734
323
  legend_label = translate(legend_label, self.language)
324
+ handler = collision_handler or self.collision_handler
735
325
 
736
326
  for p in planets:
737
327
  label = labels.get(p.name)
@@ -744,16 +334,21 @@ class BasePlot(ABC):
744
334
  polygon_style = style.marker.to_polygon_style()
745
335
  polygon_style.edge_color = None
746
336
  self.circle(
747
- (p.ra, p.dec),
748
- p.apparent_size,
749
- polygon_style,
337
+ center=(p.ra, p.dec),
338
+ radius_degrees=p.apparent_size / 2,
339
+ style=polygon_style,
750
340
  gid="planet-marker",
751
341
  )
752
342
  self._add_legend_handle_marker(legend_label, style.marker)
753
343
 
754
344
  if label:
755
345
  self.text(
756
- label.upper(), p.ra, p.dec, style.label, gid="planet-label"
346
+ label.upper(),
347
+ p.ra,
348
+ p.dec,
349
+ style.label,
350
+ collision_handler=handler,
351
+ gid="planet-label",
757
352
  )
758
353
  else:
759
354
  self.marker(
@@ -762,6 +357,7 @@ class BasePlot(ABC):
762
357
  style=style,
763
358
  label=label.upper() if label else None,
764
359
  legend_label=legend_label,
360
+ collision_handler=handler,
765
361
  gid_marker="planet-marker",
766
362
  gid_label="planet-label",
767
363
  )
@@ -773,6 +369,7 @@ class BasePlot(ABC):
773
369
  true_size: bool = False,
774
370
  label: str = "Sun",
775
371
  legend_label: str = "Sun",
372
+ collision_handler: CollisionHandler = None,
776
373
  ) -> None:
777
374
  """
778
375
  Plots the Sun.
@@ -784,6 +381,7 @@ class BasePlot(ABC):
784
381
  true_size: If True, then the Sun's true apparent size in the sky will be plotted as a circle (the marker style's symbol will be ignored). If False, then the style's marker size will be used.
785
382
  label: How the Sun will be labeled on the plot
786
383
  legend_label: How the sun will be labeled in the legend
384
+ collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on label collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
787
385
  """
788
386
  s = models.Sun.get(
789
387
  dt=self.observer.dt,
@@ -794,6 +392,7 @@ class BasePlot(ABC):
794
392
  label = translate(label, self.language)
795
393
  legend_label = translate(legend_label, self.language)
796
394
  s.name = label or s.name
395
+ handler = collision_handler or self.collision_handler
797
396
 
798
397
  if not self.in_bounds(s.ra, s.dec):
799
398
  return
@@ -807,17 +406,25 @@ class BasePlot(ABC):
807
406
  polygon_style.edge_color = None
808
407
 
809
408
  self.circle(
810
- (s.ra, s.dec),
811
- s.apparent_size,
409
+ center=(s.ra, s.dec),
410
+ radius_degrees=s.apparent_size / 2,
812
411
  style=polygon_style,
813
412
  gid="sun-marker",
413
+ num_pts=200,
814
414
  )
815
415
 
816
416
  style.marker.symbol = MarkerSymbolEnum.CIRCLE
817
417
  self._add_legend_handle_marker(legend_label, style.marker)
818
418
 
819
419
  if label:
820
- self.text(label, s.ra, s.dec, style.label, gid="sun-label")
420
+ self.text(
421
+ label,
422
+ s.ra,
423
+ s.dec,
424
+ style.label,
425
+ collision_handler=handler,
426
+ gid="sun-label",
427
+ )
821
428
 
822
429
  else:
823
430
  self.marker(
@@ -826,6 +433,7 @@ class BasePlot(ABC):
826
433
  style=style,
827
434
  label=label,
828
435
  legend_label=legend_label,
436
+ collision_handler=handler,
829
437
  gid_marker="sun-marker",
830
438
  gid_label="sun-label",
831
439
  )
@@ -1026,14 +634,27 @@ class BasePlot(ABC):
1026
634
  )
1027
635
 
1028
636
  @use_style(LineStyle)
1029
- def line(self, coordinates: list[tuple[float, float]], style: LineStyle, **kwargs):
637
+ def line(
638
+ self,
639
+ style: LineStyle,
640
+ coordinates: list[tuple[float, float]] = None,
641
+ geometry: LineString = None,
642
+ **kwargs,
643
+ ):
1030
644
  """Plots a line
1031
645
 
1032
646
  Args:
1033
647
  coordinates: List of coordinates, e.g. `[(ra, dec), (ra, dec)]`
648
+ geometry: A shapely LineString. If this value is passed, then the `coordinates` kwarg will be ignored.
1034
649
  style: Style of the line
1035
650
  """
1036
- x, y = zip(*[self._prepare_coords(*p) for p in coordinates])
651
+
652
+ if coordinates is None and geometry is None:
653
+ raise ValueError("Must pass coordinates or geometry when plotting lines.")
654
+
655
+ coords = geometry.coords if geometry is not None else coordinates
656
+
657
+ x, y = zip(*[self._prepare_coords(*p) for p in coords])
1037
658
 
1038
659
  self.ax.plot(
1039
660
  x,
@@ -1053,6 +674,7 @@ class BasePlot(ABC):
1053
674
  show_phase: bool = False,
1054
675
  label: str = "Moon",
1055
676
  legend_label: str = "Moon",
677
+ collision_handler: CollisionHandler = None,
1056
678
  ) -> None:
1057
679
  """
1058
680
  Plots the Moon.
@@ -1063,7 +685,9 @@ class BasePlot(ABC):
1063
685
  style: Styling of the Moon. If None, then the plot's style (specified when creating the plot) will be used
1064
686
  true_size: If True, then the Moon's true apparent size in the sky will be plotted as a circle (the marker style's symbol will be ignored). If False, then the style's marker size will be used.
1065
687
  show_phase: If True, and if `true_size = True`, then the approximate phase of the moon will be illustrated. The dark side of the moon will be colored with the marker's `edge_color`.
1066
- label: How the Moon will be labeled on the plot and legend
688
+ label: How the Moon will be labeled on the plot
689
+ legend_label: How the Moon will be labeled in the legend
690
+ collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on label collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
1067
691
  """
1068
692
  m = models.Moon.get(
1069
693
  dt=self.observer.dt,
@@ -1074,6 +698,7 @@ class BasePlot(ABC):
1074
698
  label = translate(label, self.language)
1075
699
  legend_label = translate(legend_label, self.language)
1076
700
  m.name = label or m.name
701
+ handler = collision_handler or self.collision_handler
1077
702
 
1078
703
  if not self.in_bounds(m.ra, m.dec):
1079
704
  return
@@ -1089,9 +714,9 @@ class BasePlot(ABC):
1089
714
 
1090
715
  if show_phase:
1091
716
  self._moon_with_phase(
1092
- m.phase_description,
1093
- (m.ra, m.dec),
1094
- m.apparent_size,
717
+ moon_phase=m.phase_description,
718
+ center=(m.ra, m.dec),
719
+ radius_degrees=m.apparent_size / 2,
1095
720
  style=polygon_style,
1096
721
  dark_side_color=style.marker.edge_color,
1097
722
  )
@@ -1107,7 +732,14 @@ class BasePlot(ABC):
1107
732
  self._add_legend_handle_marker(legend_label, style.marker)
1108
733
 
1109
734
  if label:
1110
- self.text(label, m.ra, m.dec, style.label, gid="moon-label")
735
+ self.text(
736
+ label,
737
+ m.ra,
738
+ m.dec,
739
+ style.label,
740
+ collision_handler=handler,
741
+ gid="moon-label",
742
+ )
1111
743
 
1112
744
  else:
1113
745
  self.marker(
@@ -1116,6 +748,7 @@ class BasePlot(ABC):
1116
748
  style=style,
1117
749
  label=label,
1118
750
  legend_label=legend_label,
751
+ collision_handler=handler,
1119
752
  gid_marker="moon-marker",
1120
753
  gid_label="moon-label",
1121
754
  )
@@ -1178,8 +811,8 @@ class BasePlot(ABC):
1178
811
  # Plot left side
1179
812
  self.ellipse(
1180
813
  center,
1181
- radius_degrees * 2,
1182
- radius_degrees * 2,
814
+ height_degrees=radius_degrees * 2,
815
+ width_degrees=radius_degrees * 2,
1183
816
  style=left,
1184
817
  num_pts=num_pts,
1185
818
  angle=0,
@@ -1189,8 +822,8 @@ class BasePlot(ABC):
1189
822
  # Plot right side
1190
823
  self.ellipse(
1191
824
  center,
1192
- radius_degrees * 2,
1193
- radius_degrees * 2,
825
+ height_degrees=radius_degrees * 2,
826
+ width_degrees=radius_degrees * 2,
1194
827
  style=right,
1195
828
  num_pts=num_pts,
1196
829
  angle=180,
@@ -1200,8 +833,8 @@ class BasePlot(ABC):
1200
833
  # Plot middle
1201
834
  self.ellipse(
1202
835
  center,
1203
- radius_degrees * 2,
1204
- radius_degrees,
836
+ height_degrees=radius_degrees * 2,
837
+ width_degrees=radius_degrees,
1205
838
  style=middle,
1206
839
  gid="moon-marker",
1207
840
  )
@@ -1238,12 +871,18 @@ class BasePlot(ABC):
1238
871
  )
1239
872
 
1240
873
  @use_style(PathStyle, "ecliptic")
1241
- def ecliptic(self, style: PathStyle = None, label: str = "ECLIPTIC"):
874
+ def ecliptic(
875
+ self,
876
+ style: PathStyle = None,
877
+ label: str = "ECLIPTIC",
878
+ collision_handler: CollisionHandler = None,
879
+ ):
1242
880
  """Plots the ecliptic
1243
881
 
1244
882
  Args:
1245
883
  style: Styling of the ecliptic. If None, then the plot's style will be used
1246
884
  label: How the ecliptic will be labeled on the plot
885
+ collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on label collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
1247
886
  """
1248
887
  x = []
1249
888
  y = []
@@ -1272,11 +911,21 @@ class BasePlot(ABC):
1272
911
  label_spacing = int(len(inbounds) / 4)
1273
912
 
1274
913
  for ra, dec in [inbounds[label_spacing], inbounds[label_spacing * 2]]:
1275
- self.text(label, ra, dec, style.label, gid="ecliptic-label")
914
+ self.text(
915
+ label,
916
+ ra,
917
+ dec,
918
+ style.label,
919
+ collision_handler=collision_handler or self.collision_handler,
920
+ gid="ecliptic-label",
921
+ )
1276
922
 
1277
923
  @use_style(PathStyle, "celestial_equator")
1278
924
  def celestial_equator(
1279
- self, style: PathStyle = None, label: str = "CELESTIAL EQUATOR"
925
+ self,
926
+ style: PathStyle = None,
927
+ label: str = "CELESTIAL EQUATOR",
928
+ collision_handler: CollisionHandler = None,
1280
929
  ):
1281
930
  """
1282
931
  Plots the celestial equator
@@ -1284,6 +933,7 @@ class BasePlot(ABC):
1284
933
  Args:
1285
934
  style: Styling of the celestial equator. If None, then the plot's style will be used
1286
935
  label: How the celestial equator will be labeled on the plot
936
+ collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on label collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
1287
937
  """
1288
938
  x = []
1289
939
  y = []
@@ -1314,5 +964,6 @@ class BasePlot(ABC):
1314
964
  ra,
1315
965
  0.25,
1316
966
  style.label,
967
+ collision_handler=collision_handler or self.collision_handler,
1317
968
  gid="celestial-equator-label",
1318
969
  )