starplot 0.18.3__py2.py3-none-any.whl → 0.19.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.
@@ -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
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,17 +112,11 @@ 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
@@ -154,127 +145,6 @@ class BasePlot(ABC):
154
145
  coords = self._background_clip_path.get_verts()
155
146
  self._clip_path_polygon = Polygon(coords).buffer(-1 * buffer)
156
147
 
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
148
  def _add_legend_handle_marker(self, label: str, style: MarkerStyle):
279
149
  if label is not None and label not in self._legend_handles:
280
150
  s = style.matplot_kwargs()
@@ -288,235 +158,6 @@ class BasePlot(ABC):
288
158
  label=label,
289
159
  )
290
160
 
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
161
  @property
521
162
  def magnitude_range(self) -> tuple[float, float]:
522
163
  """
@@ -525,64 +166,6 @@ class BasePlot(ABC):
525
166
  mags = [s.magnitude for s in self.objects.stars]
526
167
  return (min(mags), max(mags))
527
168
 
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
169
  @property
587
170
  def objects(self) -> models.ObjectList:
588
171
  """
@@ -637,6 +220,7 @@ class BasePlot(ABC):
637
220
  label: Optional[str] = None,
638
221
  legend_label: str = None,
639
222
  skip_bounds_check: bool = False,
223
+ collision_handler: CollisionHandler = None,
640
224
  **kwargs,
641
225
  ) -> None:
642
226
  """Plots a marker
@@ -648,6 +232,7 @@ class BasePlot(ABC):
648
232
  style: Styling for the marker
649
233
  legend_label: How to label the marker in the legend. If `None`, then the marker will not be added to the legend
650
234
  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.
235
+ 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
236
 
652
237
  """
653
238
 
@@ -700,7 +285,7 @@ class BasePlot(ABC):
700
285
  ra,
701
286
  dec,
702
287
  label_style,
703
- hide_on_collision=self.hide_colliding_labels,
288
+ collision_handler=collision_handler or self.collision_handler,
704
289
  gid=kwargs.get("gid_label") or "marker-label",
705
290
  )
706
291
 
@@ -714,6 +299,7 @@ class BasePlot(ABC):
714
299
  true_size: bool = False,
715
300
  labels: Dict[PlanetName, str] = PLANET_LABELS_DEFAULT,
716
301
  legend_label: str = "Planet",
302
+ collision_handler: CollisionHandler = None,
717
303
  ) -> None:
718
304
  """
719
305
  Plots the planets.
@@ -725,6 +311,7 @@ class BasePlot(ABC):
725
311
  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
312
  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
313
  legend_label: How to label the planets in the legend. If `None`, then the planets will not be added to the legend
314
+ 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
315
  """
729
316
  labels = labels or {}
730
317
  planets = models.Planet.all(
@@ -732,6 +319,7 @@ class BasePlot(ABC):
732
319
  )
733
320
 
734
321
  legend_label = translate(legend_label, self.language)
322
+ handler = collision_handler or self.collision_handler
735
323
 
736
324
  for p in planets:
737
325
  label = labels.get(p.name)
@@ -753,7 +341,12 @@ class BasePlot(ABC):
753
341
 
754
342
  if label:
755
343
  self.text(
756
- label.upper(), p.ra, p.dec, style.label, gid="planet-label"
344
+ label.upper(),
345
+ p.ra,
346
+ p.dec,
347
+ style.label,
348
+ collision_handler=handler,
349
+ gid="planet-label",
757
350
  )
758
351
  else:
759
352
  self.marker(
@@ -762,6 +355,7 @@ class BasePlot(ABC):
762
355
  style=style,
763
356
  label=label.upper() if label else None,
764
357
  legend_label=legend_label,
358
+ collision_handler=handler,
765
359
  gid_marker="planet-marker",
766
360
  gid_label="planet-label",
767
361
  )
@@ -773,6 +367,7 @@ class BasePlot(ABC):
773
367
  true_size: bool = False,
774
368
  label: str = "Sun",
775
369
  legend_label: str = "Sun",
370
+ collision_handler: CollisionHandler = None,
776
371
  ) -> None:
777
372
  """
778
373
  Plots the Sun.
@@ -784,6 +379,7 @@ class BasePlot(ABC):
784
379
  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
380
  label: How the Sun will be labeled on the plot
786
381
  legend_label: How the sun will be labeled in the legend
382
+ 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
383
  """
788
384
  s = models.Sun.get(
789
385
  dt=self.observer.dt,
@@ -794,6 +390,7 @@ class BasePlot(ABC):
794
390
  label = translate(label, self.language)
795
391
  legend_label = translate(legend_label, self.language)
796
392
  s.name = label or s.name
393
+ handler = collision_handler or self.collision_handler
797
394
 
798
395
  if not self.in_bounds(s.ra, s.dec):
799
396
  return
@@ -817,7 +414,14 @@ class BasePlot(ABC):
817
414
  self._add_legend_handle_marker(legend_label, style.marker)
818
415
 
819
416
  if label:
820
- self.text(label, s.ra, s.dec, style.label, gid="sun-label")
417
+ self.text(
418
+ label,
419
+ s.ra,
420
+ s.dec,
421
+ style.label,
422
+ collision_handler=handler,
423
+ gid="sun-label",
424
+ )
821
425
 
822
426
  else:
823
427
  self.marker(
@@ -826,6 +430,7 @@ class BasePlot(ABC):
826
430
  style=style,
827
431
  label=label,
828
432
  legend_label=legend_label,
433
+ collision_handler=handler,
829
434
  gid_marker="sun-marker",
830
435
  gid_label="sun-label",
831
436
  )
@@ -1053,6 +658,7 @@ class BasePlot(ABC):
1053
658
  show_phase: bool = False,
1054
659
  label: str = "Moon",
1055
660
  legend_label: str = "Moon",
661
+ collision_handler: CollisionHandler = None,
1056
662
  ) -> None:
1057
663
  """
1058
664
  Plots the Moon.
@@ -1063,7 +669,9 @@ class BasePlot(ABC):
1063
669
  style: Styling of the Moon. If None, then the plot's style (specified when creating the plot) will be used
1064
670
  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
671
  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
672
+ label: How the Moon will be labeled on the plot
673
+ legend_label: How the Moon will be labeled in the legend
674
+ 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
675
  """
1068
676
  m = models.Moon.get(
1069
677
  dt=self.observer.dt,
@@ -1074,6 +682,7 @@ class BasePlot(ABC):
1074
682
  label = translate(label, self.language)
1075
683
  legend_label = translate(legend_label, self.language)
1076
684
  m.name = label or m.name
685
+ handler = collision_handler or self.collision_handler
1077
686
 
1078
687
  if not self.in_bounds(m.ra, m.dec):
1079
688
  return
@@ -1107,7 +716,14 @@ class BasePlot(ABC):
1107
716
  self._add_legend_handle_marker(legend_label, style.marker)
1108
717
 
1109
718
  if label:
1110
- self.text(label, m.ra, m.dec, style.label, gid="moon-label")
719
+ self.text(
720
+ label,
721
+ m.ra,
722
+ m.dec,
723
+ style.label,
724
+ collision_handler=handler,
725
+ gid="moon-label",
726
+ )
1111
727
 
1112
728
  else:
1113
729
  self.marker(
@@ -1116,6 +732,7 @@ class BasePlot(ABC):
1116
732
  style=style,
1117
733
  label=label,
1118
734
  legend_label=legend_label,
735
+ collision_handler=handler,
1119
736
  gid_marker="moon-marker",
1120
737
  gid_label="moon-label",
1121
738
  )
@@ -1238,12 +855,18 @@ class BasePlot(ABC):
1238
855
  )
1239
856
 
1240
857
  @use_style(PathStyle, "ecliptic")
1241
- def ecliptic(self, style: PathStyle = None, label: str = "ECLIPTIC"):
858
+ def ecliptic(
859
+ self,
860
+ style: PathStyle = None,
861
+ label: str = "ECLIPTIC",
862
+ collision_handler: CollisionHandler = None,
863
+ ):
1242
864
  """Plots the ecliptic
1243
865
 
1244
866
  Args:
1245
867
  style: Styling of the ecliptic. If None, then the plot's style will be used
1246
868
  label: How the ecliptic will be labeled on the plot
869
+ 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
870
  """
1248
871
  x = []
1249
872
  y = []
@@ -1272,11 +895,21 @@ class BasePlot(ABC):
1272
895
  label_spacing = int(len(inbounds) / 4)
1273
896
 
1274
897
  for ra, dec in [inbounds[label_spacing], inbounds[label_spacing * 2]]:
1275
- self.text(label, ra, dec, style.label, gid="ecliptic-label")
898
+ self.text(
899
+ label,
900
+ ra,
901
+ dec,
902
+ style.label,
903
+ collision_handler=collision_handler or self.collision_handler,
904
+ gid="ecliptic-label",
905
+ )
1276
906
 
1277
907
  @use_style(PathStyle, "celestial_equator")
1278
908
  def celestial_equator(
1279
- self, style: PathStyle = None, label: str = "CELESTIAL EQUATOR"
909
+ self,
910
+ style: PathStyle = None,
911
+ label: str = "CELESTIAL EQUATOR",
912
+ collision_handler: CollisionHandler = None,
1280
913
  ):
1281
914
  """
1282
915
  Plots the celestial equator
@@ -1284,6 +917,7 @@ class BasePlot(ABC):
1284
917
  Args:
1285
918
  style: Styling of the celestial equator. If None, then the plot's style will be used
1286
919
  label: How the celestial equator will be labeled on the plot
920
+ 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
921
  """
1288
922
  x = []
1289
923
  y = []
@@ -1314,5 +948,6 @@ class BasePlot(ABC):
1314
948
  ra,
1315
949
  0.25,
1316
950
  style.label,
951
+ collision_handler=collision_handler or self.collision_handler,
1317
952
  gid="celestial-equator-label",
1318
953
  )