starplot 0.20.5__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.
- starplot/__init__.py +59 -0
- starplot/callables.py +176 -0
- starplot/cli.py +29 -0
- starplot/config.py +80 -0
- starplot/coordinates.py +7 -0
- starplot/data/__init__.py +22 -0
- starplot/data/catalogs.py +356 -0
- starplot/data/constellations.py +71 -0
- starplot/data/db.py +33 -0
- starplot/data/dsos.py +85 -0
- starplot/data/ecliptic.py +201 -0
- starplot/data/library/constellation_names.parquet +0 -0
- starplot/data/library/dso_names.parquet +0 -0
- starplot/data/library/readme.md +1 -0
- starplot/data/library/star_designations.parquet +0 -0
- starplot/data/stars.py +79 -0
- starplot/data/translations.py +408 -0
- starplot/data/utils.py +35 -0
- starplot/geometry.py +319 -0
- starplot/mixins.py +360 -0
- starplot/models/__init__.py +21 -0
- starplot/models/base.py +77 -0
- starplot/models/comet.py +302 -0
- starplot/models/constellation.py +151 -0
- starplot/models/dso.py +317 -0
- starplot/models/milky_way.py +30 -0
- starplot/models/moon.py +130 -0
- starplot/models/objects.py +29 -0
- starplot/models/observer.py +125 -0
- starplot/models/optics.py +342 -0
- starplot/models/planet.py +137 -0
- starplot/models/satellite.py +138 -0
- starplot/models/star.py +253 -0
- starplot/models/sun.py +62 -0
- starplot/plots/__init__.py +7 -0
- starplot/plots/base.py +1023 -0
- starplot/plots/galaxy.py +372 -0
- starplot/plots/horizon.py +543 -0
- starplot/plots/map.py +515 -0
- starplot/plots/optic.py +469 -0
- starplot/plots/zenith.py +217 -0
- starplot/plotters/__init__.py +9 -0
- starplot/plotters/arrow.py +174 -0
- starplot/plotters/constellations.py +298 -0
- starplot/plotters/debug.py +21 -0
- starplot/plotters/dsos.py +294 -0
- starplot/plotters/experimental.py +722 -0
- starplot/plotters/gradients.py +153 -0
- starplot/plotters/legend.py +253 -0
- starplot/plotters/milkyway.py +51 -0
- starplot/plotters/stars.py +319 -0
- starplot/plotters/text.py +802 -0
- starplot/profile.py +16 -0
- starplot/projections.py +184 -0
- starplot/styles/__init__.py +6 -0
- starplot/styles/base.py +1344 -0
- starplot/styles/ext/antique.yml +175 -0
- starplot/styles/ext/blue_dark.yml +163 -0
- starplot/styles/ext/blue_gold.yml +147 -0
- starplot/styles/ext/blue_light.yml +123 -0
- starplot/styles/ext/blue_medium.yml +142 -0
- starplot/styles/ext/blue_night.yml +185 -0
- starplot/styles/ext/cb_wong.yml +124 -0
- starplot/styles/ext/color_print.yml +111 -0
- starplot/styles/ext/gradient_presets.yml +158 -0
- starplot/styles/ext/grayscale.yml +94 -0
- starplot/styles/ext/grayscale_dark.yml +136 -0
- starplot/styles/ext/map.yml +12 -0
- starplot/styles/ext/nord.yml +158 -0
- starplot/styles/ext/optic.yml +20 -0
- starplot/styles/ext/publication.yml +8 -0
- starplot/styles/extensions.py +129 -0
- starplot/styles/fonts-library/gfs-didot/DESCRIPTION.en_us.html +9 -0
- starplot/styles/fonts-library/gfs-didot/GFSDidot-Regular.ttf +0 -0
- starplot/styles/fonts-library/gfs-didot/METADATA.pb +16 -0
- starplot/styles/fonts-library/gfs-didot/OFL.txt +94 -0
- starplot/styles/fonts-library/inter/Inter-Bold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-BoldItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraBold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraLight.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-ExtraLightItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Italic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Light.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-LightItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-Regular.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-SemiBold.ttf +0 -0
- starplot/styles/fonts-library/inter/Inter-SemiBoldItalic.ttf +0 -0
- starplot/styles/fonts-library/inter/LICENSE.txt +92 -0
- starplot/styles/fonts.py +15 -0
- starplot/styles/helpers.py +93 -0
- starplot/styles/markers.py +308 -0
- starplot/utils.py +169 -0
- starplot/warnings.py +21 -0
- starplot-0.20.5.dist-info/METADATA +146 -0
- starplot-0.20.5.dist-info/RECORD +98 -0
- starplot-0.20.5.dist-info/WHEEL +4 -0
- starplot-0.20.5.dist-info/entry_points.txt +3 -0
- starplot-0.20.5.dist-info/licenses/LICENSE +21 -0
starplot/plots/base.py
ADDED
|
@@ -0,0 +1,1023 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Dict, Union, Optional
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from matplotlib import patches
|
|
7
|
+
from matplotlib import pyplot as plt, patheffects
|
|
8
|
+
from matplotlib.axes import Axes
|
|
9
|
+
from matplotlib.figure import Figure
|
|
10
|
+
from matplotlib.lines import Line2D
|
|
11
|
+
from shapely import Polygon, LineString
|
|
12
|
+
|
|
13
|
+
from starplot.coordinates import CoordinateSystem
|
|
14
|
+
from starplot import models, warnings
|
|
15
|
+
from starplot import geometry as _geometry
|
|
16
|
+
from starplot.config import settings as StarplotSettings, SvgTextType
|
|
17
|
+
from starplot.data import load, ecliptic
|
|
18
|
+
from starplot.data.translations import translate
|
|
19
|
+
from starplot.models.planet import PlanetName, PLANET_LABELS_DEFAULT
|
|
20
|
+
from starplot.models.moon import MoonPhase
|
|
21
|
+
from starplot.models.optics import Optic, Camera
|
|
22
|
+
from starplot.models.observer import Observer
|
|
23
|
+
from starplot.styles import (
|
|
24
|
+
PlotStyle,
|
|
25
|
+
MarkerStyle,
|
|
26
|
+
ObjectStyle,
|
|
27
|
+
LabelStyle,
|
|
28
|
+
MarkerSymbolEnum,
|
|
29
|
+
PathStyle,
|
|
30
|
+
PolygonStyle,
|
|
31
|
+
GradientDirection,
|
|
32
|
+
fonts,
|
|
33
|
+
AnchorPointEnum,
|
|
34
|
+
)
|
|
35
|
+
from starplot.plotters.debug import DebugPlotterMixin
|
|
36
|
+
from starplot.plotters.text import TextPlotterMixin, CollisionHandler
|
|
37
|
+
from starplot.styles.helpers import use_style
|
|
38
|
+
from starplot.profile import profile
|
|
39
|
+
|
|
40
|
+
LOGGER = logging.getLogger("starplot")
|
|
41
|
+
LOG_HANDLER = logging.StreamHandler()
|
|
42
|
+
LOG_FORMATTER = logging.Formatter(
|
|
43
|
+
"\033[1;34m%(name)s\033[0m:[%(levelname)s]: %(message)s"
|
|
44
|
+
)
|
|
45
|
+
LOG_HANDLER.setFormatter(LOG_FORMATTER)
|
|
46
|
+
LOGGER.addHandler(LOG_HANDLER)
|
|
47
|
+
|
|
48
|
+
DEFAULT_RESOLUTION = 4096
|
|
49
|
+
|
|
50
|
+
DPI = 100
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BasePlot(DebugPlotterMixin, TextPlotterMixin, ABC):
|
|
54
|
+
_coordinate_system = CoordinateSystem.RA_DEC
|
|
55
|
+
_gradient_direction: GradientDirection = GradientDirection.LINEAR
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
observer: Observer = None,
|
|
60
|
+
ephemeris: str = "de421.bsp",
|
|
61
|
+
style: PlotStyle = None,
|
|
62
|
+
resolution: int = 4096,
|
|
63
|
+
point_label_handler: CollisionHandler = None,
|
|
64
|
+
area_label_handler: CollisionHandler = None,
|
|
65
|
+
path_label_handler: CollisionHandler = None,
|
|
66
|
+
scale: float = 1.0,
|
|
67
|
+
autoscale: bool = False,
|
|
68
|
+
suppress_warnings: bool = True,
|
|
69
|
+
*args,
|
|
70
|
+
**kwargs,
|
|
71
|
+
):
|
|
72
|
+
super().__init__(*args, **kwargs)
|
|
73
|
+
|
|
74
|
+
self._clip_path_polygon: Polygon = None # clip path in display coordinates
|
|
75
|
+
|
|
76
|
+
self.ax: Axes = None
|
|
77
|
+
"""
|
|
78
|
+
The underlying [Matplotlib axes](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html#matplotlib.axes.Axes) that everything is plotted on.
|
|
79
|
+
|
|
80
|
+
**Important**: Most Starplot plotting functions also specify a transform based on the plot's projection when plotting things on the Matplotlib Axes instance, so use this property at your own risk!
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
self.fig: Figure = None
|
|
84
|
+
"""
|
|
85
|
+
The underlying [Matplotlib figure](https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.html#matplotlib.figure.Figure) that the axes is drawn on.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
if StarplotSettings.svg_text_type == SvgTextType.PATH:
|
|
89
|
+
plt.rcParams["svg.fonttype"] = "path"
|
|
90
|
+
else:
|
|
91
|
+
plt.rcParams["svg.fonttype"] = "none"
|
|
92
|
+
|
|
93
|
+
px = 1 / DPI # pixel in inches
|
|
94
|
+
self.pixels_per_point = DPI / 72
|
|
95
|
+
self.dpi = DPI
|
|
96
|
+
|
|
97
|
+
self.language = StarplotSettings.language
|
|
98
|
+
|
|
99
|
+
self.style = style or PlotStyle()
|
|
100
|
+
"""The plot's style."""
|
|
101
|
+
|
|
102
|
+
self.figure_size = resolution * px
|
|
103
|
+
self.resolution = resolution
|
|
104
|
+
|
|
105
|
+
self.point_label_handler = point_label_handler or CollisionHandler(
|
|
106
|
+
attempts=10,
|
|
107
|
+
anchor_fallbacks=[
|
|
108
|
+
AnchorPointEnum.BOTTOM_RIGHT,
|
|
109
|
+
AnchorPointEnum.TOP_LEFT,
|
|
110
|
+
AnchorPointEnum.TOP_RIGHT,
|
|
111
|
+
AnchorPointEnum.BOTTOM_LEFT,
|
|
112
|
+
AnchorPointEnum.BOTTOM_CENTER,
|
|
113
|
+
AnchorPointEnum.TOP_CENTER,
|
|
114
|
+
AnchorPointEnum.RIGHT_CENTER,
|
|
115
|
+
AnchorPointEnum.LEFT_CENTER,
|
|
116
|
+
],
|
|
117
|
+
)
|
|
118
|
+
"""Default [collision handler][starplot.CollisionHandler] for point labels."""
|
|
119
|
+
|
|
120
|
+
self.area_label_handler = area_label_handler or CollisionHandler(
|
|
121
|
+
allow_constellation_line_collisions=True
|
|
122
|
+
)
|
|
123
|
+
"""Default [collision handler][starplot.CollisionHandler] for area labels."""
|
|
124
|
+
|
|
125
|
+
self.path_label_handler = path_label_handler or CollisionHandler(
|
|
126
|
+
allow_constellation_line_collisions=True
|
|
127
|
+
)
|
|
128
|
+
"""Default [collision handler][starplot.CollisionHandler] for path labels."""
|
|
129
|
+
|
|
130
|
+
self.scale = scale
|
|
131
|
+
self.autoscale = autoscale
|
|
132
|
+
if self.autoscale:
|
|
133
|
+
self.scale = self.resolution / DEFAULT_RESOLUTION
|
|
134
|
+
|
|
135
|
+
self.scale *= 1.28
|
|
136
|
+
|
|
137
|
+
if suppress_warnings:
|
|
138
|
+
warnings.suppress()
|
|
139
|
+
|
|
140
|
+
self.observer = observer or Observer()
|
|
141
|
+
self.ephemeris_name = ephemeris
|
|
142
|
+
self.ephemeris = load(ephemeris)
|
|
143
|
+
self.earth = self.ephemeris["earth"]
|
|
144
|
+
|
|
145
|
+
self._background_clip_path = None
|
|
146
|
+
|
|
147
|
+
self._legend = None
|
|
148
|
+
self._legend_handles = {}
|
|
149
|
+
|
|
150
|
+
self.debug = StarplotSettings.debug or bool(kwargs.get("debug"))
|
|
151
|
+
self.debug_text = StarplotSettings.debug or bool(kwargs.get("debug_text"))
|
|
152
|
+
self.log_level = logging.DEBUG if self.debug else logging.ERROR
|
|
153
|
+
self.logger = LOGGER
|
|
154
|
+
self.logger.setLevel(self.log_level)
|
|
155
|
+
|
|
156
|
+
self.text_border = patheffects.withStroke(
|
|
157
|
+
linewidth=self.style.text_border_width * self.scale,
|
|
158
|
+
foreground=self.style.text_border_color.as_hex(),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
self._objects = models.ObjectList()
|
|
162
|
+
self._labeled_stars = []
|
|
163
|
+
fonts.load()
|
|
164
|
+
|
|
165
|
+
def _plot_kwargs(self) -> dict:
|
|
166
|
+
return {}
|
|
167
|
+
|
|
168
|
+
def _prepare_coords(self, ra, dec) -> tuple[float, float]:
|
|
169
|
+
return ra, dec
|
|
170
|
+
|
|
171
|
+
def _prepare_coords_many(self, coordinates: list, epoch_year: float = 2000) -> list:
|
|
172
|
+
return coordinates
|
|
173
|
+
|
|
174
|
+
def _update_clip_path_polygon(self, buffer=8):
|
|
175
|
+
self.fig.draw_without_rendering()
|
|
176
|
+
coords = self._background_clip_path.get_verts()
|
|
177
|
+
self._clip_path_polygon = Polygon(coords).buffer(-1 * buffer)
|
|
178
|
+
|
|
179
|
+
# if self.debug_text:
|
|
180
|
+
# patch = patches.Polygon(
|
|
181
|
+
# Polygon(coords).buffer(-1 * buffer).exterior.coords,
|
|
182
|
+
# fill=False,
|
|
183
|
+
# facecolor="none",
|
|
184
|
+
# edgecolor="red",
|
|
185
|
+
# linewidth=4,
|
|
186
|
+
# zorder=5_000,
|
|
187
|
+
# transform=None,
|
|
188
|
+
# )
|
|
189
|
+
# self.ax.add_patch(patch)
|
|
190
|
+
|
|
191
|
+
def _add_legend_handle_marker(self, label: str, style: MarkerStyle):
|
|
192
|
+
if label is not None and label not in self._legend_handles:
|
|
193
|
+
s = style.matplot_kwargs()
|
|
194
|
+
s["markersize"] = self.style.legend.symbol_size * self.scale
|
|
195
|
+
self._legend_handles[label] = Line2D(
|
|
196
|
+
[],
|
|
197
|
+
[],
|
|
198
|
+
**s,
|
|
199
|
+
**self._plot_kwargs(),
|
|
200
|
+
linestyle="None",
|
|
201
|
+
label=label,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def _fit_to_ax(self) -> None:
|
|
205
|
+
self.fig.draw_without_rendering()
|
|
206
|
+
bbox = self.ax.get_window_extent().transformed(
|
|
207
|
+
self.fig.dpi_scale_trans.inverted()
|
|
208
|
+
)
|
|
209
|
+
width, height = bbox.width, bbox.height
|
|
210
|
+
self.fig.set_size_inches(width, height)
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def magnitude_range(self) -> tuple[float, float]:
|
|
214
|
+
"""
|
|
215
|
+
Range of magnitude for all plotted stars, as a tuple (min, max)
|
|
216
|
+
"""
|
|
217
|
+
mags = [s.magnitude for s in self.objects.stars]
|
|
218
|
+
return (min(mags), max(mags))
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def objects(self) -> models.ObjectList:
|
|
222
|
+
"""
|
|
223
|
+
Returns an [`ObjectList`][starplot.models.ObjectList] that contains various lists of sky objects that have been plotted.
|
|
224
|
+
"""
|
|
225
|
+
return self._objects
|
|
226
|
+
|
|
227
|
+
@use_style(LabelStyle, "title")
|
|
228
|
+
def title(self, text: str, style: LabelStyle = None):
|
|
229
|
+
"""
|
|
230
|
+
Plots a title at the top of the plot
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
text: Title text to plot
|
|
234
|
+
style: Styling of the title. If None, then the plot's style (specified when creating the plot) will be used
|
|
235
|
+
"""
|
|
236
|
+
style_kwargs = style.matplot_kwargs(self.scale)
|
|
237
|
+
style_kwargs.pop("linespacing", None)
|
|
238
|
+
style_kwargs["pad"] = style.line_spacing
|
|
239
|
+
self.ax.set_title(text, **style_kwargs)
|
|
240
|
+
|
|
241
|
+
def close_fig(self) -> None:
|
|
242
|
+
"""Closes the underlying matplotlib figure."""
|
|
243
|
+
if self.fig:
|
|
244
|
+
plt.close(self.fig)
|
|
245
|
+
|
|
246
|
+
@profile
|
|
247
|
+
def export(self, filename: str, padding: float = 0, **kwargs):
|
|
248
|
+
"""Exports the plot to an image file.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
filename: Filename of exported file (the format will be inferred from the extension)
|
|
252
|
+
padding: Padding (in inches) around the image
|
|
253
|
+
**kwargs: Any keyword arguments to pass through to matplotlib's `savefig` method
|
|
254
|
+
|
|
255
|
+
"""
|
|
256
|
+
self.logger.debug("Exporting...")
|
|
257
|
+
self.fig.savefig(
|
|
258
|
+
filename,
|
|
259
|
+
bbox_inches="tight",
|
|
260
|
+
pad_inches=padding * self.scale,
|
|
261
|
+
dpi=DPI,
|
|
262
|
+
**kwargs,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
@use_style(ObjectStyle)
|
|
266
|
+
def marker(
|
|
267
|
+
self,
|
|
268
|
+
ra: float,
|
|
269
|
+
dec: float,
|
|
270
|
+
style: Union[dict, ObjectStyle],
|
|
271
|
+
label: Optional[str] = None,
|
|
272
|
+
legend_label: str = None,
|
|
273
|
+
skip_bounds_check: bool = False,
|
|
274
|
+
collision_handler: CollisionHandler = None,
|
|
275
|
+
**kwargs,
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Plots a marker
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
ra: Right ascension of the marker
|
|
281
|
+
dec: Declination of the marker
|
|
282
|
+
label: Label for the marker
|
|
283
|
+
style: Styling for the marker
|
|
284
|
+
legend_label: How to label the marker in the legend. If `None`, then the marker will not be added to the legend
|
|
285
|
+
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.
|
|
286
|
+
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.
|
|
287
|
+
|
|
288
|
+
"""
|
|
289
|
+
|
|
290
|
+
if not skip_bounds_check and not self.in_bounds(ra, dec):
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
# Plot marker
|
|
294
|
+
x, y = self._prepare_coords(ra, dec)
|
|
295
|
+
style_kwargs = style.marker.matplot_scatter_kwargs(self.scale)
|
|
296
|
+
result = self.ax.scatter(
|
|
297
|
+
x,
|
|
298
|
+
y,
|
|
299
|
+
**style_kwargs,
|
|
300
|
+
**self._plot_kwargs(),
|
|
301
|
+
clip_on=True,
|
|
302
|
+
clip_path=self._background_clip_path,
|
|
303
|
+
gid=kwargs.get("gid_marker") or "marker",
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Add to spatial index
|
|
307
|
+
data_xy = self._proj.transform_point(x, y, self._crs)
|
|
308
|
+
display_x, display_y = self.ax.transData.transform(data_xy)
|
|
309
|
+
if display_x > 0 and display_y > 0:
|
|
310
|
+
radius = style_kwargs.get("s", 1) ** 0.5 / 5
|
|
311
|
+
bbox = np.array(
|
|
312
|
+
(
|
|
313
|
+
display_x - radius,
|
|
314
|
+
display_y - radius,
|
|
315
|
+
display_x + radius,
|
|
316
|
+
display_y + radius,
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
self._markers_rtree.insert(0, bbox, None)
|
|
320
|
+
|
|
321
|
+
# Plot label
|
|
322
|
+
if label:
|
|
323
|
+
label_style = style.label
|
|
324
|
+
if label_style.offset_x == "auto" or label_style.offset_y == "auto":
|
|
325
|
+
marker_size = ((style.marker.size / self.scale) ** 2) * (
|
|
326
|
+
self.scale**2
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
label_style = label_style.offset_from_marker(
|
|
330
|
+
marker_symbol=style.marker.symbol,
|
|
331
|
+
marker_size=marker_size,
|
|
332
|
+
scale=self.scale,
|
|
333
|
+
)
|
|
334
|
+
self.text(
|
|
335
|
+
label,
|
|
336
|
+
ra,
|
|
337
|
+
dec,
|
|
338
|
+
label_style,
|
|
339
|
+
collision_handler=collision_handler or self.point_label_handler,
|
|
340
|
+
gid=kwargs.get("gid_label") or "marker-label",
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
if legend_label is not None:
|
|
344
|
+
self._legend_handles[legend_label] = result
|
|
345
|
+
|
|
346
|
+
@use_style(ObjectStyle, "planets")
|
|
347
|
+
def planets(
|
|
348
|
+
self,
|
|
349
|
+
style: ObjectStyle = None,
|
|
350
|
+
true_size: bool = False,
|
|
351
|
+
labels: Dict[PlanetName, str] = PLANET_LABELS_DEFAULT,
|
|
352
|
+
legend_label: str = "Planet",
|
|
353
|
+
collision_handler: CollisionHandler = None,
|
|
354
|
+
) -> None:
|
|
355
|
+
"""
|
|
356
|
+
Plots the planets, at their _apparent_ RA/DEC (based on the observer you defined).
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
style: Styling of the planets. If None, then the plot's style (specified when creating the plot) will be used
|
|
360
|
+
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.
|
|
361
|
+
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])
|
|
362
|
+
legend_label: How to label the planets in the legend. If `None`, then the planets will not be added to the legend
|
|
363
|
+
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.
|
|
364
|
+
"""
|
|
365
|
+
labels = labels or {}
|
|
366
|
+
planets = models.Planet.all(self.observer, self.ephemeris_name)
|
|
367
|
+
|
|
368
|
+
legend_label = translate(legend_label, self.language)
|
|
369
|
+
handler = collision_handler or self.point_label_handler
|
|
370
|
+
|
|
371
|
+
for p in planets:
|
|
372
|
+
label = labels.get(p.name)
|
|
373
|
+
label = translate(label, self.language)
|
|
374
|
+
|
|
375
|
+
if self.in_bounds(p.ra, p.dec):
|
|
376
|
+
self._objects.planets.append(p)
|
|
377
|
+
|
|
378
|
+
if true_size:
|
|
379
|
+
polygon_style = style.marker.to_polygon_style()
|
|
380
|
+
polygon_style.edge_color = None
|
|
381
|
+
self.circle(
|
|
382
|
+
center=(p.ra, p.dec),
|
|
383
|
+
radius_degrees=p.apparent_size / 2,
|
|
384
|
+
style=polygon_style,
|
|
385
|
+
gid="planet-marker",
|
|
386
|
+
)
|
|
387
|
+
self._add_legend_handle_marker(legend_label, style.marker)
|
|
388
|
+
|
|
389
|
+
if label:
|
|
390
|
+
self.text(
|
|
391
|
+
label.upper(),
|
|
392
|
+
p.ra,
|
|
393
|
+
p.dec,
|
|
394
|
+
style.label,
|
|
395
|
+
collision_handler=handler,
|
|
396
|
+
gid="planet-label",
|
|
397
|
+
)
|
|
398
|
+
else:
|
|
399
|
+
self.marker(
|
|
400
|
+
ra=p.ra,
|
|
401
|
+
dec=p.dec,
|
|
402
|
+
style=style,
|
|
403
|
+
label=label.upper() if label else None,
|
|
404
|
+
legend_label=legend_label,
|
|
405
|
+
collision_handler=handler,
|
|
406
|
+
gid_marker="planet-marker",
|
|
407
|
+
gid_label="planet-label",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
@use_style(ObjectStyle, "sun")
|
|
411
|
+
def sun(
|
|
412
|
+
self,
|
|
413
|
+
style: ObjectStyle = None,
|
|
414
|
+
true_size: bool = False,
|
|
415
|
+
label: str = "Sun",
|
|
416
|
+
legend_label: str = "Sun",
|
|
417
|
+
collision_handler: CollisionHandler = None,
|
|
418
|
+
) -> None:
|
|
419
|
+
"""
|
|
420
|
+
Plots the Sun, at its _apparent_ RA/DEC (based on the observer you defined).
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
style: Styling of the Sun. If None, then the plot's style (specified when creating the plot) will be used
|
|
424
|
+
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.
|
|
425
|
+
label: How the Sun will be labeled on the plot
|
|
426
|
+
legend_label: How the sun will be labeled in the legend
|
|
427
|
+
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.
|
|
428
|
+
"""
|
|
429
|
+
s = models.Sun.get(
|
|
430
|
+
observer=self.observer,
|
|
431
|
+
ephemeris=self.ephemeris_name,
|
|
432
|
+
)
|
|
433
|
+
label = translate(label, self.language)
|
|
434
|
+
legend_label = translate(legend_label, self.language)
|
|
435
|
+
s.name = label or s.name
|
|
436
|
+
handler = collision_handler or self.point_label_handler
|
|
437
|
+
|
|
438
|
+
if not self.in_bounds(s.ra, s.dec):
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
self._objects.sun = s
|
|
442
|
+
|
|
443
|
+
if true_size:
|
|
444
|
+
polygon_style = style.marker.to_polygon_style()
|
|
445
|
+
|
|
446
|
+
# hide the edge because it can interfere with the true size
|
|
447
|
+
polygon_style.edge_color = None
|
|
448
|
+
|
|
449
|
+
self.circle(
|
|
450
|
+
center=(s.ra, s.dec),
|
|
451
|
+
radius_degrees=s.apparent_size / 2,
|
|
452
|
+
style=polygon_style,
|
|
453
|
+
gid="sun-marker",
|
|
454
|
+
num_pts=200,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
style.marker.symbol = MarkerSymbolEnum.CIRCLE
|
|
458
|
+
self._add_legend_handle_marker(legend_label, style.marker)
|
|
459
|
+
|
|
460
|
+
if label:
|
|
461
|
+
self.text(
|
|
462
|
+
label,
|
|
463
|
+
s.ra,
|
|
464
|
+
s.dec,
|
|
465
|
+
style.label,
|
|
466
|
+
collision_handler=handler,
|
|
467
|
+
gid="sun-label",
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
else:
|
|
471
|
+
self.marker(
|
|
472
|
+
ra=s.ra,
|
|
473
|
+
dec=s.dec,
|
|
474
|
+
style=style,
|
|
475
|
+
label=label,
|
|
476
|
+
legend_label=legend_label,
|
|
477
|
+
collision_handler=handler,
|
|
478
|
+
gid_marker="sun-marker",
|
|
479
|
+
gid_label="sun-label",
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
@abstractmethod
|
|
483
|
+
def in_bounds(self, ra: float, dec: float) -> bool:
|
|
484
|
+
"""Determine if a coordinate is within the bounds of the plot.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
ra: Right ascension
|
|
488
|
+
dec: Declination
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
bool: True if the coordinate is in bounds, otherwise False
|
|
492
|
+
|
|
493
|
+
"""
|
|
494
|
+
raise NotImplementedError
|
|
495
|
+
|
|
496
|
+
@abstractmethod
|
|
497
|
+
def _in_bounds_xy(self, x: float, y: float) -> bool:
|
|
498
|
+
"""
|
|
499
|
+
Determine if a data / projected coordinate is within the non-clipped bounds of the plot.
|
|
500
|
+
|
|
501
|
+
This should be extremely precise.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
x: X coordinate
|
|
505
|
+
y: Y coordinate
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
bool: True if the coordinate is in bounds, otherwise False
|
|
509
|
+
"""
|
|
510
|
+
raise NotImplementedError
|
|
511
|
+
|
|
512
|
+
def _polygon(self, points: list, style: PolygonStyle, **kwargs):
|
|
513
|
+
# points = [self._prepare_coords(*p) for p in points]
|
|
514
|
+
points = self._prepare_coords_many(points)
|
|
515
|
+
patch = patches.Polygon(
|
|
516
|
+
points,
|
|
517
|
+
# closed=False, # needs to be false for circles at poles?
|
|
518
|
+
**style.matplot_kwargs(self.scale),
|
|
519
|
+
**kwargs,
|
|
520
|
+
# clip_on=True,
|
|
521
|
+
# clip_path=self._background_clip_path,
|
|
522
|
+
)
|
|
523
|
+
self.ax.add_patch(patch)
|
|
524
|
+
# Need to set clip path AFTER adding patch
|
|
525
|
+
patch.set_clip_on(True)
|
|
526
|
+
patch.set_clip_path(self._background_clip_path)
|
|
527
|
+
|
|
528
|
+
@use_style(PolygonStyle)
|
|
529
|
+
def polygon(
|
|
530
|
+
self,
|
|
531
|
+
style: PolygonStyle,
|
|
532
|
+
points: list = None,
|
|
533
|
+
geometry: Polygon = None,
|
|
534
|
+
legend_label: str = None,
|
|
535
|
+
**kwargs,
|
|
536
|
+
):
|
|
537
|
+
"""
|
|
538
|
+
Plots a polygon.
|
|
539
|
+
|
|
540
|
+
Must pass in either `points` **or** `geometry` (but not both).
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
style: Style of polygon
|
|
544
|
+
points: List of polygon points `[(ra, dec), ...]` - **must be in counterclockwise order**
|
|
545
|
+
geometry: A shapely Polygon. If this value is passed, then the `points` kwarg will be ignored.
|
|
546
|
+
legend_label: Label for this object in the legend
|
|
547
|
+
|
|
548
|
+
"""
|
|
549
|
+
if points is None and geometry is None:
|
|
550
|
+
raise ValueError("Must pass points or geometry when plotting polygons.")
|
|
551
|
+
|
|
552
|
+
if geometry is not None:
|
|
553
|
+
points = list(zip(*geometry.exterior.coords.xy))
|
|
554
|
+
|
|
555
|
+
self._polygon(points, style, gid=kwargs.get("gid") or "polygon", **kwargs)
|
|
556
|
+
|
|
557
|
+
if legend_label is not None:
|
|
558
|
+
self._add_legend_handle_marker(
|
|
559
|
+
legend_label,
|
|
560
|
+
style=style.to_marker_style(symbol=MarkerSymbolEnum.SQUARE),
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
@use_style(PolygonStyle)
|
|
564
|
+
def rectangle(
|
|
565
|
+
self,
|
|
566
|
+
center: tuple,
|
|
567
|
+
height_degrees: float,
|
|
568
|
+
width_degrees: float,
|
|
569
|
+
style: PolygonStyle,
|
|
570
|
+
angle: float = 0,
|
|
571
|
+
legend_label: str = None,
|
|
572
|
+
**kwargs,
|
|
573
|
+
):
|
|
574
|
+
"""Plots a rectangle
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
center: Center of rectangle (ra, dec)
|
|
578
|
+
height_degrees: Height of rectangle (degrees)
|
|
579
|
+
width_degrees: Width of rectangle (degrees)
|
|
580
|
+
style: Style of rectangle
|
|
581
|
+
angle: Angle of rotation clockwise (degrees)
|
|
582
|
+
legend_label: Label for this object in the legend
|
|
583
|
+
"""
|
|
584
|
+
polygon = _geometry.rectangle(
|
|
585
|
+
center,
|
|
586
|
+
height_degrees,
|
|
587
|
+
width_degrees,
|
|
588
|
+
angle,
|
|
589
|
+
)
|
|
590
|
+
points = list(zip(*polygon.exterior.coords.xy))
|
|
591
|
+
self._polygon(points, style, gid=kwargs.get("gid") or "polygon")
|
|
592
|
+
|
|
593
|
+
if legend_label is not None:
|
|
594
|
+
self._add_legend_handle_marker(
|
|
595
|
+
legend_label,
|
|
596
|
+
style=style.to_marker_style(symbol=MarkerSymbolEnum.SQUARE),
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
@use_style(PolygonStyle)
|
|
600
|
+
def ellipse(
|
|
601
|
+
self,
|
|
602
|
+
center: tuple,
|
|
603
|
+
height_degrees: float,
|
|
604
|
+
width_degrees: float,
|
|
605
|
+
style: PolygonStyle,
|
|
606
|
+
angle: float = 0,
|
|
607
|
+
num_pts: int = 100,
|
|
608
|
+
start_angle: int = 0,
|
|
609
|
+
end_angle: int = 360,
|
|
610
|
+
legend_label: str = None,
|
|
611
|
+
**kwargs,
|
|
612
|
+
):
|
|
613
|
+
"""Plots an ellipse
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
center: Center of ellipse (ra, dec)
|
|
617
|
+
height_degrees: Height of ellipse (degrees)
|
|
618
|
+
width_degrees: Width of ellipse (degrees)
|
|
619
|
+
style: Style of ellipse
|
|
620
|
+
angle: Angle of rotation clockwise (degrees)
|
|
621
|
+
num_pts: Number of points to calculate for the ellipse polygon
|
|
622
|
+
start_angle: Angle to start at
|
|
623
|
+
end_angle: Angle to end at
|
|
624
|
+
legend_label: Label for this object in the legend
|
|
625
|
+
"""
|
|
626
|
+
|
|
627
|
+
polygon = _geometry.ellipse(
|
|
628
|
+
center,
|
|
629
|
+
height_degrees,
|
|
630
|
+
width_degrees,
|
|
631
|
+
angle,
|
|
632
|
+
num_pts,
|
|
633
|
+
start_angle,
|
|
634
|
+
end_angle,
|
|
635
|
+
)
|
|
636
|
+
points = list(zip(*polygon.exterior.coords.xy))
|
|
637
|
+
self._polygon(points, style, gid=kwargs.get("gid") or "polygon")
|
|
638
|
+
|
|
639
|
+
if legend_label is not None:
|
|
640
|
+
self._add_legend_handle_marker(
|
|
641
|
+
legend_label,
|
|
642
|
+
style=style.to_marker_style(symbol=MarkerSymbolEnum.ELLIPSE),
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
@use_style(PolygonStyle)
|
|
646
|
+
def circle(
|
|
647
|
+
self,
|
|
648
|
+
center: tuple,
|
|
649
|
+
radius_degrees: float,
|
|
650
|
+
style: PolygonStyle,
|
|
651
|
+
num_pts: int = 100,
|
|
652
|
+
legend_label: str = None,
|
|
653
|
+
**kwargs,
|
|
654
|
+
):
|
|
655
|
+
"""Plots a circle
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
center: Center of circle (ra, dec)
|
|
659
|
+
radius_degrees: Radius of circle (degrees)
|
|
660
|
+
style: Style of circle
|
|
661
|
+
num_pts: Number of points to calculate for the circle polygon
|
|
662
|
+
legend_label: Label for this object in the legend
|
|
663
|
+
"""
|
|
664
|
+
self.ellipse(
|
|
665
|
+
center,
|
|
666
|
+
radius_degrees * 2,
|
|
667
|
+
radius_degrees * 2,
|
|
668
|
+
style=style,
|
|
669
|
+
angle=0,
|
|
670
|
+
num_pts=num_pts,
|
|
671
|
+
gid=kwargs.get("gid") or "polygon",
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
if legend_label is not None:
|
|
675
|
+
self._add_legend_handle_marker(
|
|
676
|
+
legend_label,
|
|
677
|
+
style=style.to_marker_style(symbol=MarkerSymbolEnum.CIRCLE),
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
@use_style(ObjectStyle, "moon")
|
|
681
|
+
def moon(
|
|
682
|
+
self,
|
|
683
|
+
style: ObjectStyle = None,
|
|
684
|
+
true_size: bool = False,
|
|
685
|
+
show_phase: bool = False,
|
|
686
|
+
label: str = "Moon",
|
|
687
|
+
legend_label: str = "Moon",
|
|
688
|
+
collision_handler: CollisionHandler = None,
|
|
689
|
+
) -> None:
|
|
690
|
+
"""
|
|
691
|
+
Plots the Moon, at its _apparent_ RA/DEC (based on the observer you defined).
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
style: Styling of the Moon. If None, then the plot's style (specified when creating the plot) will be used
|
|
695
|
+
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.
|
|
696
|
+
show_phase: If True, and if `true_size = True`, then the phase of the moon will be illustrated. The dark side of the moon will be colored with the marker's `edge_color`.
|
|
697
|
+
label: How the Moon will be labeled on the plot
|
|
698
|
+
legend_label: How the Moon will be labeled in the legend
|
|
699
|
+
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.
|
|
700
|
+
"""
|
|
701
|
+
m = models.Moon.get(
|
|
702
|
+
observer=self.observer,
|
|
703
|
+
ephemeris=self.ephemeris_name,
|
|
704
|
+
)
|
|
705
|
+
label = translate(label, self.language)
|
|
706
|
+
legend_label = translate(legend_label, self.language)
|
|
707
|
+
m.name = label or m.name
|
|
708
|
+
handler = collision_handler or self.point_label_handler
|
|
709
|
+
|
|
710
|
+
if not self.in_bounds(m.ra, m.dec):
|
|
711
|
+
return
|
|
712
|
+
|
|
713
|
+
self._objects.moon = m
|
|
714
|
+
|
|
715
|
+
if true_size:
|
|
716
|
+
# convert to PolygonStyle because we'll plot the true size as a polygon
|
|
717
|
+
polygon_style = style.marker.to_polygon_style()
|
|
718
|
+
|
|
719
|
+
# hide the edge because it can interfere with the true size
|
|
720
|
+
polygon_style.edge_color = None
|
|
721
|
+
|
|
722
|
+
if show_phase:
|
|
723
|
+
self._moon_with_phase(
|
|
724
|
+
moon_phase=m.phase_description,
|
|
725
|
+
center=(m.ra, m.dec),
|
|
726
|
+
radius_degrees=m.apparent_size / 2,
|
|
727
|
+
style=polygon_style,
|
|
728
|
+
dark_side_color=style.marker.edge_color,
|
|
729
|
+
)
|
|
730
|
+
else:
|
|
731
|
+
self.circle(
|
|
732
|
+
center=(m.ra, m.dec),
|
|
733
|
+
radius_degrees=m.apparent_size / 2,
|
|
734
|
+
style=polygon_style,
|
|
735
|
+
gid="moon-marker",
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
style.marker.symbol = MarkerSymbolEnum.CIRCLE
|
|
739
|
+
self._add_legend_handle_marker(legend_label, style.marker)
|
|
740
|
+
|
|
741
|
+
if label:
|
|
742
|
+
self.text(
|
|
743
|
+
label,
|
|
744
|
+
m.ra,
|
|
745
|
+
m.dec,
|
|
746
|
+
style.label,
|
|
747
|
+
collision_handler=handler,
|
|
748
|
+
gid="moon-label",
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
else:
|
|
752
|
+
self.marker(
|
|
753
|
+
ra=m.ra,
|
|
754
|
+
dec=m.dec,
|
|
755
|
+
style=style,
|
|
756
|
+
label=label,
|
|
757
|
+
legend_label=legend_label,
|
|
758
|
+
collision_handler=handler,
|
|
759
|
+
gid_marker="moon-marker",
|
|
760
|
+
gid_label="moon-label",
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
def _moon_with_phase(
|
|
764
|
+
self,
|
|
765
|
+
moon_phase: MoonPhase,
|
|
766
|
+
center: tuple,
|
|
767
|
+
radius_degrees: float,
|
|
768
|
+
style: PolygonStyle,
|
|
769
|
+
dark_side_color: str,
|
|
770
|
+
num_pts: int = 200,
|
|
771
|
+
):
|
|
772
|
+
"""
|
|
773
|
+
Plots the (approximate) moon phase by drawing two half circles and one ellipse in the center,
|
|
774
|
+
and then determining the color of each of the three shapes by the moon phase.
|
|
775
|
+
"""
|
|
776
|
+
illuminated_color = style.fill_color
|
|
777
|
+
|
|
778
|
+
ellipse_b_radius_degrees = np.abs(
|
|
779
|
+
radius_degrees * (2 * self._objects.moon.illumination - 1)
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
left = style.copy()
|
|
783
|
+
right = style.copy()
|
|
784
|
+
middle = style.copy()
|
|
785
|
+
|
|
786
|
+
if moon_phase == MoonPhase.WAXING_CRESCENT:
|
|
787
|
+
left.fill_color = illuminated_color
|
|
788
|
+
middle.fill_color = dark_side_color
|
|
789
|
+
right.fill_color = dark_side_color
|
|
790
|
+
|
|
791
|
+
elif moon_phase == MoonPhase.FIRST_QUARTER:
|
|
792
|
+
left.fill_color = illuminated_color
|
|
793
|
+
middle.alpha = 0
|
|
794
|
+
right.fill_color = dark_side_color
|
|
795
|
+
|
|
796
|
+
elif moon_phase == MoonPhase.WAXING_GIBBOUS:
|
|
797
|
+
left.fill_color = illuminated_color
|
|
798
|
+
middle.fill_color = illuminated_color
|
|
799
|
+
right.fill_color = dark_side_color
|
|
800
|
+
|
|
801
|
+
elif moon_phase == MoonPhase.FULL_MOON:
|
|
802
|
+
left.fill_color = middle.fill_color = right.fill_color = illuminated_color
|
|
803
|
+
|
|
804
|
+
elif moon_phase == MoonPhase.WANING_GIBBOUS:
|
|
805
|
+
left.fill_color = dark_side_color
|
|
806
|
+
middle.fill_color = illuminated_color
|
|
807
|
+
right.fill_color = illuminated_color
|
|
808
|
+
|
|
809
|
+
elif moon_phase == MoonPhase.LAST_QUARTER:
|
|
810
|
+
left.fill_color = dark_side_color
|
|
811
|
+
middle.alpha = 0
|
|
812
|
+
right.fill_color = illuminated_color
|
|
813
|
+
|
|
814
|
+
elif moon_phase == MoonPhase.WANING_CRESCENT:
|
|
815
|
+
left.fill_color = dark_side_color
|
|
816
|
+
middle.fill_color = dark_side_color
|
|
817
|
+
right.fill_color = illuminated_color
|
|
818
|
+
|
|
819
|
+
else:
|
|
820
|
+
left.fill_color = middle.fill_color = right.fill_color = dark_side_color
|
|
821
|
+
|
|
822
|
+
# Plot left side
|
|
823
|
+
self.ellipse(
|
|
824
|
+
center,
|
|
825
|
+
height_degrees=radius_degrees * 2,
|
|
826
|
+
width_degrees=radius_degrees * 2,
|
|
827
|
+
style=left,
|
|
828
|
+
num_pts=num_pts,
|
|
829
|
+
angle=0,
|
|
830
|
+
end_angle=180, # plot as a semicircle
|
|
831
|
+
gid="moon-marker",
|
|
832
|
+
)
|
|
833
|
+
# Plot right side
|
|
834
|
+
self.ellipse(
|
|
835
|
+
center,
|
|
836
|
+
height_degrees=radius_degrees * 2,
|
|
837
|
+
width_degrees=radius_degrees * 2,
|
|
838
|
+
style=right,
|
|
839
|
+
num_pts=num_pts,
|
|
840
|
+
angle=180,
|
|
841
|
+
end_angle=180, # plot as a semicircle
|
|
842
|
+
gid="moon-marker",
|
|
843
|
+
)
|
|
844
|
+
# Plot middle
|
|
845
|
+
self.ellipse(
|
|
846
|
+
center,
|
|
847
|
+
height_degrees=radius_degrees * 2,
|
|
848
|
+
width_degrees=ellipse_b_radius_degrees * 2,
|
|
849
|
+
num_pts=num_pts,
|
|
850
|
+
style=middle,
|
|
851
|
+
gid="moon-marker",
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
@use_style(PolygonStyle, "optic_fov")
|
|
855
|
+
def optic_fov(
|
|
856
|
+
self,
|
|
857
|
+
ra: float,
|
|
858
|
+
dec: float,
|
|
859
|
+
optic: Optic,
|
|
860
|
+
style: PolygonStyle = None,
|
|
861
|
+
):
|
|
862
|
+
"""Draws a polygon representing the field of view for an optic, centered at a specific point.
|
|
863
|
+
|
|
864
|
+
Args:
|
|
865
|
+
ra: Right ascension of the center of view
|
|
866
|
+
dec: Declination of the center of view
|
|
867
|
+
optic: Instance of an [Optic][starplot.models.Optic]
|
|
868
|
+
style: style of the polygon
|
|
869
|
+
"""
|
|
870
|
+
if isinstance(optic, Camera):
|
|
871
|
+
self.rectangle(
|
|
872
|
+
center=(ra, dec),
|
|
873
|
+
height_degrees=optic.true_fov_y,
|
|
874
|
+
width_degrees=optic.true_fov_x,
|
|
875
|
+
angle=optic.rotation,
|
|
876
|
+
style=style,
|
|
877
|
+
)
|
|
878
|
+
else:
|
|
879
|
+
self.circle(
|
|
880
|
+
center=(ra, dec),
|
|
881
|
+
radius_degrees=optic.true_fov / 2,
|
|
882
|
+
style=style,
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
@profile
|
|
886
|
+
@use_style(PathStyle, "ecliptic")
|
|
887
|
+
def ecliptic(
|
|
888
|
+
self,
|
|
889
|
+
style: PathStyle = None,
|
|
890
|
+
label: str = "ECLIPTIC",
|
|
891
|
+
num_labels: int = 1,
|
|
892
|
+
collision_handler: CollisionHandler = None,
|
|
893
|
+
):
|
|
894
|
+
"""Plots the ecliptic
|
|
895
|
+
|
|
896
|
+
Args:
|
|
897
|
+
style: Styling of the ecliptic. If None, then the plot's style will be used
|
|
898
|
+
label: How the ecliptic will be labeled on the plot
|
|
899
|
+
num_labels: Max number of labels to plot along the line
|
|
900
|
+
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 plot's `path_label_handler` will be used.
|
|
901
|
+
"""
|
|
902
|
+
x = []
|
|
903
|
+
y = []
|
|
904
|
+
inbounds = []
|
|
905
|
+
|
|
906
|
+
label = translate(label, self.language)
|
|
907
|
+
|
|
908
|
+
for ra, dec in ecliptic.RA_DECS:
|
|
909
|
+
x0, y0 = self._prepare_coords(ra * 15, dec)
|
|
910
|
+
x.append(x0)
|
|
911
|
+
y.append(y0)
|
|
912
|
+
if self.in_bounds(ra * 15, dec):
|
|
913
|
+
inbounds.append((ra * 15, dec))
|
|
914
|
+
|
|
915
|
+
coords = [(ra * 15, dec) for ra, dec in ecliptic.RA_DECS]
|
|
916
|
+
|
|
917
|
+
self.line(
|
|
918
|
+
style=style,
|
|
919
|
+
label=label.upper(),
|
|
920
|
+
num_labels=num_labels,
|
|
921
|
+
collision_handler=collision_handler,
|
|
922
|
+
coordinates=coords,
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
@profile
|
|
926
|
+
@use_style(PathStyle, "celestial_equator")
|
|
927
|
+
def celestial_equator(
|
|
928
|
+
self,
|
|
929
|
+
style: PathStyle = None,
|
|
930
|
+
label: str = "CELESTIAL EQUATOR",
|
|
931
|
+
num_labels: int = 1,
|
|
932
|
+
collision_handler: CollisionHandler = None,
|
|
933
|
+
):
|
|
934
|
+
"""
|
|
935
|
+
Plots the celestial equator
|
|
936
|
+
|
|
937
|
+
Args:
|
|
938
|
+
style: Styling of the celestial equator. If None, then the plot's style will be used
|
|
939
|
+
label: How the celestial equator will be labeled on the plot
|
|
940
|
+
num_labels: Max number of labels to plot along the line
|
|
941
|
+
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 plot's `path_label_handler` will be used.
|
|
942
|
+
"""
|
|
943
|
+
label = translate(label, self.language)
|
|
944
|
+
coords = [(ra, 0) for ra in range(0, 361)]
|
|
945
|
+
self.line(
|
|
946
|
+
style=style,
|
|
947
|
+
label=label.upper(),
|
|
948
|
+
num_labels=num_labels,
|
|
949
|
+
collision_handler=collision_handler,
|
|
950
|
+
coordinates=coords,
|
|
951
|
+
gid="celestial-equator",
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
@use_style(PathStyle)
|
|
955
|
+
def line(
|
|
956
|
+
self,
|
|
957
|
+
coordinates: list[tuple[float, float]] = None,
|
|
958
|
+
geometry: LineString = None,
|
|
959
|
+
style: PathStyle = None,
|
|
960
|
+
label: str = None,
|
|
961
|
+
num_labels: int = 2,
|
|
962
|
+
collision_handler: CollisionHandler = None,
|
|
963
|
+
**kwargs,
|
|
964
|
+
):
|
|
965
|
+
"""Plots a line, with optional labels. Either coordinates OR geometry must be specified.
|
|
966
|
+
|
|
967
|
+
Args:
|
|
968
|
+
|
|
969
|
+
coordinates: List of coordinates, e.g. `[(ra, dec), (ra, dec)]`
|
|
970
|
+
geometry: A shapely LineString. If this value is passed, then the `coordinates` kwarg will be ignored.
|
|
971
|
+
style: Style of the line
|
|
972
|
+
label: Label for the line
|
|
973
|
+
num_labels: Number of labels to plot along the line
|
|
974
|
+
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 plot's `path_label_handler` will be used.
|
|
975
|
+
|
|
976
|
+
"""
|
|
977
|
+
|
|
978
|
+
if coordinates is None and geometry is None:
|
|
979
|
+
raise ValueError("Must pass coordinates or geometry when plotting lines.")
|
|
980
|
+
|
|
981
|
+
coords = geometry.coords if geometry is not None else coordinates
|
|
982
|
+
prepared_coords = [self._prepare_coords(*p) for p in coords]
|
|
983
|
+
x, y = zip(*prepared_coords)
|
|
984
|
+
|
|
985
|
+
gid = kwargs.get("gid") or "line"
|
|
986
|
+
|
|
987
|
+
self.ax.plot(
|
|
988
|
+
x,
|
|
989
|
+
y,
|
|
990
|
+
clip_on=True,
|
|
991
|
+
clip_path=self._background_clip_path,
|
|
992
|
+
dash_capstyle=style.line.dash_capstyle,
|
|
993
|
+
gid=gid,
|
|
994
|
+
**style.line.matplot_kwargs(self.scale),
|
|
995
|
+
**self._plot_kwargs(),
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
if not label:
|
|
999
|
+
return
|
|
1000
|
+
|
|
1001
|
+
prepared_coords = [
|
|
1002
|
+
(x, y) for x, y in prepared_coords if self._in_bounds_xy(x, y)
|
|
1003
|
+
]
|
|
1004
|
+
|
|
1005
|
+
if not prepared_coords:
|
|
1006
|
+
return
|
|
1007
|
+
|
|
1008
|
+
x, y = zip(*prepared_coords)
|
|
1009
|
+
|
|
1010
|
+
collision_handler = collision_handler or self.path_label_handler
|
|
1011
|
+
|
|
1012
|
+
self._text_line(
|
|
1013
|
+
x,
|
|
1014
|
+
y,
|
|
1015
|
+
label,
|
|
1016
|
+
num_labels=num_labels,
|
|
1017
|
+
collision_handler=collision_handler,
|
|
1018
|
+
min_spacing=0.65,
|
|
1019
|
+
**style.label.matplot_kwargs(self.scale),
|
|
1020
|
+
**self._plot_kwargs(),
|
|
1021
|
+
clip_path=self._background_clip_path,
|
|
1022
|
+
gid=gid,
|
|
1023
|
+
)
|