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.
Files changed (98) hide show
  1. starplot/__init__.py +59 -0
  2. starplot/callables.py +176 -0
  3. starplot/cli.py +29 -0
  4. starplot/config.py +80 -0
  5. starplot/coordinates.py +7 -0
  6. starplot/data/__init__.py +22 -0
  7. starplot/data/catalogs.py +356 -0
  8. starplot/data/constellations.py +71 -0
  9. starplot/data/db.py +33 -0
  10. starplot/data/dsos.py +85 -0
  11. starplot/data/ecliptic.py +201 -0
  12. starplot/data/library/constellation_names.parquet +0 -0
  13. starplot/data/library/dso_names.parquet +0 -0
  14. starplot/data/library/readme.md +1 -0
  15. starplot/data/library/star_designations.parquet +0 -0
  16. starplot/data/stars.py +79 -0
  17. starplot/data/translations.py +408 -0
  18. starplot/data/utils.py +35 -0
  19. starplot/geometry.py +319 -0
  20. starplot/mixins.py +360 -0
  21. starplot/models/__init__.py +21 -0
  22. starplot/models/base.py +77 -0
  23. starplot/models/comet.py +302 -0
  24. starplot/models/constellation.py +151 -0
  25. starplot/models/dso.py +317 -0
  26. starplot/models/milky_way.py +30 -0
  27. starplot/models/moon.py +130 -0
  28. starplot/models/objects.py +29 -0
  29. starplot/models/observer.py +125 -0
  30. starplot/models/optics.py +342 -0
  31. starplot/models/planet.py +137 -0
  32. starplot/models/satellite.py +138 -0
  33. starplot/models/star.py +253 -0
  34. starplot/models/sun.py +62 -0
  35. starplot/plots/__init__.py +7 -0
  36. starplot/plots/base.py +1023 -0
  37. starplot/plots/galaxy.py +372 -0
  38. starplot/plots/horizon.py +543 -0
  39. starplot/plots/map.py +515 -0
  40. starplot/plots/optic.py +469 -0
  41. starplot/plots/zenith.py +217 -0
  42. starplot/plotters/__init__.py +9 -0
  43. starplot/plotters/arrow.py +174 -0
  44. starplot/plotters/constellations.py +298 -0
  45. starplot/plotters/debug.py +21 -0
  46. starplot/plotters/dsos.py +294 -0
  47. starplot/plotters/experimental.py +722 -0
  48. starplot/plotters/gradients.py +153 -0
  49. starplot/plotters/legend.py +253 -0
  50. starplot/plotters/milkyway.py +51 -0
  51. starplot/plotters/stars.py +319 -0
  52. starplot/plotters/text.py +802 -0
  53. starplot/profile.py +16 -0
  54. starplot/projections.py +184 -0
  55. starplot/styles/__init__.py +6 -0
  56. starplot/styles/base.py +1344 -0
  57. starplot/styles/ext/antique.yml +175 -0
  58. starplot/styles/ext/blue_dark.yml +163 -0
  59. starplot/styles/ext/blue_gold.yml +147 -0
  60. starplot/styles/ext/blue_light.yml +123 -0
  61. starplot/styles/ext/blue_medium.yml +142 -0
  62. starplot/styles/ext/blue_night.yml +185 -0
  63. starplot/styles/ext/cb_wong.yml +124 -0
  64. starplot/styles/ext/color_print.yml +111 -0
  65. starplot/styles/ext/gradient_presets.yml +158 -0
  66. starplot/styles/ext/grayscale.yml +94 -0
  67. starplot/styles/ext/grayscale_dark.yml +136 -0
  68. starplot/styles/ext/map.yml +12 -0
  69. starplot/styles/ext/nord.yml +158 -0
  70. starplot/styles/ext/optic.yml +20 -0
  71. starplot/styles/ext/publication.yml +8 -0
  72. starplot/styles/extensions.py +129 -0
  73. starplot/styles/fonts-library/gfs-didot/DESCRIPTION.en_us.html +9 -0
  74. starplot/styles/fonts-library/gfs-didot/GFSDidot-Regular.ttf +0 -0
  75. starplot/styles/fonts-library/gfs-didot/METADATA.pb +16 -0
  76. starplot/styles/fonts-library/gfs-didot/OFL.txt +94 -0
  77. starplot/styles/fonts-library/inter/Inter-Bold.ttf +0 -0
  78. starplot/styles/fonts-library/inter/Inter-BoldItalic.ttf +0 -0
  79. starplot/styles/fonts-library/inter/Inter-ExtraBold.ttf +0 -0
  80. starplot/styles/fonts-library/inter/Inter-ExtraLight.ttf +0 -0
  81. starplot/styles/fonts-library/inter/Inter-ExtraLightItalic.ttf +0 -0
  82. starplot/styles/fonts-library/inter/Inter-Italic.ttf +0 -0
  83. starplot/styles/fonts-library/inter/Inter-Light.ttf +0 -0
  84. starplot/styles/fonts-library/inter/Inter-LightItalic.ttf +0 -0
  85. starplot/styles/fonts-library/inter/Inter-Regular.ttf +0 -0
  86. starplot/styles/fonts-library/inter/Inter-SemiBold.ttf +0 -0
  87. starplot/styles/fonts-library/inter/Inter-SemiBoldItalic.ttf +0 -0
  88. starplot/styles/fonts-library/inter/LICENSE.txt +92 -0
  89. starplot/styles/fonts.py +15 -0
  90. starplot/styles/helpers.py +93 -0
  91. starplot/styles/markers.py +308 -0
  92. starplot/utils.py +169 -0
  93. starplot/warnings.py +21 -0
  94. starplot-0.20.5.dist-info/METADATA +146 -0
  95. starplot-0.20.5.dist-info/RECORD +98 -0
  96. starplot-0.20.5.dist-info/WHEEL +4 -0
  97. starplot-0.20.5.dist-info/entry_points.txt +3 -0
  98. starplot-0.20.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,469 @@
1
+ from typing import Callable
2
+
3
+
4
+ import pandas as pd
5
+ from cartopy import crs as ccrs
6
+ from matplotlib import pyplot as plt, patches, path
7
+ from skyfield.api import Star as SkyfieldStar
8
+
9
+
10
+ from starplot import callables, geometry
11
+ from starplot.coordinates import CoordinateSystem
12
+ from starplot.plots.base import BasePlot, DPI
13
+ from starplot.data.catalogs import Catalog, BIG_SKY_MAG11
14
+ from starplot.mixins import ExtentMaskMixin
15
+ from starplot.models import Star, Optic, Camera
16
+ from starplot.models.observer import Observer
17
+ from starplot.plotters import (
18
+ StarPlotterMixin,
19
+ DsoPlotterMixin,
20
+ GradientBackgroundMixin,
21
+ LegendPlotterMixin,
22
+ )
23
+ from starplot.styles import (
24
+ PlotStyle,
25
+ ObjectStyle,
26
+ LabelStyle,
27
+ extensions,
28
+ use_style,
29
+ ZOrderEnum,
30
+ GradientDirection,
31
+ )
32
+ from starplot.utils import azimuth_to_string
33
+ from starplot.plotters.text import CollisionHandler
34
+
35
+
36
+ class OpticPlot(
37
+ BasePlot,
38
+ ExtentMaskMixin,
39
+ StarPlotterMixin,
40
+ DsoPlotterMixin,
41
+ GradientBackgroundMixin,
42
+ LegendPlotterMixin,
43
+ ):
44
+ """Creates a new optic plot.
45
+
46
+ Args:
47
+ ra: Right ascension of target center, in degrees (0...360)
48
+ dec: Declination of target center, in degrees (-90...90)
49
+ optic: Optic instance that defines optical parameters
50
+ observer: Observer instance which specifies a time and place. Defaults to `Observer()`
51
+ ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
52
+ style: Styling for the plot (colors, sizes, fonts, etc). If `None`, it defaults to `PlotStyle()`
53
+ resolution: Size (in pixels) of largest dimension of the map
54
+ point_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for point labels.
55
+ area_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for area labels.
56
+ path_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for path labels.
57
+ raise_on_below_horizon: If True, then a ValueError will be raised if the target is below the horizon at the observing time/location
58
+ scale: Scaling factor that will be applied to all sizes in styles (e.g. font size, marker size, line widths, etc). For example, if you want to make everything 2x bigger, then set the scale to 2. At `scale=1` and `resolution=4096` (the default), all sizes are optimized visually for a map that covers 1-3 constellations. So, if you're creating a plot of a _larger_ extent, then it'd probably be good to decrease the scale (i.e. make everything smaller) -- and _increase_ the scale if you're plotting a very small area.
59
+ autoscale: If True, then the scale will be set automatically based on resolution.
60
+ suppress_warnings: If True (the default), then all warnings will be suppressed
61
+
62
+ Returns:
63
+ OpticPlot: A new instance of an OpticPlot
64
+
65
+ """
66
+
67
+ _coordinate_system = CoordinateSystem.AZ_ALT
68
+ _gradient_direction = GradientDirection.RADIAL
69
+
70
+ FIELD_OF_VIEW_MAX = 20
71
+
72
+ def __init__(
73
+ self,
74
+ ra: float,
75
+ dec: float,
76
+ optic: Optic,
77
+ observer: Observer = None,
78
+ ephemeris: str = "de421.bsp",
79
+ style: PlotStyle = None,
80
+ resolution: int = 4096,
81
+ point_label_handler: CollisionHandler = None,
82
+ area_label_handler: CollisionHandler = None,
83
+ path_label_handler: CollisionHandler = None,
84
+ raise_on_below_horizon: bool = True,
85
+ scale: float = 1.0,
86
+ autoscale: bool = False,
87
+ suppress_warnings: bool = True,
88
+ *args,
89
+ **kwargs,
90
+ ) -> "OpticPlot":
91
+ observer = observer or Observer()
92
+ style = style or PlotStyle().extend(extensions.OPTIC)
93
+
94
+ super().__init__(
95
+ observer,
96
+ ephemeris,
97
+ style,
98
+ resolution,
99
+ point_label_handler=point_label_handler,
100
+ area_label_handler=area_label_handler,
101
+ path_label_handler=path_label_handler,
102
+ scale=scale,
103
+ autoscale=autoscale,
104
+ suppress_warnings=suppress_warnings,
105
+ *args,
106
+ **kwargs,
107
+ )
108
+ self.logger.debug("Creating OpticPlot...")
109
+
110
+ if isinstance(optic, Camera) and style.has_gradient_background():
111
+ raise ValueError("Gradient backgrounds are not yet supported for cameras.")
112
+
113
+ self.ra = ra
114
+ self.dec = dec
115
+ self.raise_on_below_horizon = raise_on_below_horizon
116
+
117
+ self.optic = optic
118
+ self._crs = ccrs.CRS(
119
+ proj4_params=[
120
+ ("proj", "latlong"),
121
+ ("a", "6378137"),
122
+ ],
123
+ globe=ccrs.Globe(ellipse="sphere", flattening=0),
124
+ )
125
+ if self.optic.true_fov > self.FIELD_OF_VIEW_MAX:
126
+ raise ValueError(
127
+ f"Field of View too big: {self.optic.true_fov} (max = {self.FIELD_OF_VIEW_MAX}). Tip: Use horizon or map plots for wider fields of view."
128
+ )
129
+ self._calc_position()
130
+ self._adjust_radec_minmax()
131
+ self._init_plot()
132
+
133
+ @property
134
+ def alt(self):
135
+ """Altitude of target (degrees)"""
136
+ return self.pos_alt
137
+
138
+ @property
139
+ def az(self):
140
+ """Azimuth of target (degrees)"""
141
+ return self.pos_az
142
+
143
+ def _prepare_coords(self, ra, dec) -> (float, float):
144
+ """Converts RA/DEC to AZ/ALT"""
145
+ return self.observer._apparent(
146
+ obj=SkyfieldStar(ra_hours=ra / 15, dec_degrees=dec),
147
+ ephemeris=self.ephemeris_name,
148
+ )
149
+
150
+ def _prepare_coords_many(
151
+ self, coordinates: list, epoch_year: float = 2000
152
+ ) -> (float, float):
153
+ """Converts RA/DEC to AZ/ALT"""
154
+ df = pd.DataFrame(coordinates, columns=["ra", "dec"])
155
+ df["ra_hours"], df["dec_degrees"] = (df.ra / 15, df.dec)
156
+ df["epoch_year"] = epoch_year
157
+
158
+ sf_star = SkyfieldStar.from_dataframe(df)
159
+
160
+ df["x"], df["y"] = self.observer._apparent(
161
+ obj=sf_star,
162
+ ephemeris=self.ephemeris_name,
163
+ )
164
+
165
+ return list(zip(df["x"], df["y"]))
166
+
167
+ def _plot_kwargs(self) -> dict:
168
+ return dict(transform=self._crs)
169
+
170
+ def in_bounds(self, ra, dec) -> bool:
171
+ """Determine if a coordinate is within the bounds of the plot.
172
+
173
+ Args:
174
+ ra: Right ascension, in degrees (0...360)
175
+ dec: Declination, in degrees (-90...90)
176
+
177
+ Returns:
178
+ True if the coordinate is in bounds, otherwise False
179
+ """
180
+ az, alt = self._prepare_coords(ra, dec)
181
+ return self.in_bounds_altaz(alt, az)
182
+
183
+ def in_bounds_altaz(self, alt, az, scale: float = 1) -> bool:
184
+ """Determine if a coordinate is within the bounds of the plot.
185
+
186
+ Args:
187
+ alt: Altitude angle in degrees (0...90)
188
+ az: Azimuth angle in degrees (0...360)
189
+
190
+ Returns:
191
+ True if the coordinate is in bounds, otherwise False
192
+ """
193
+ x, y = self._proj.transform_point(az, alt, self._crs)
194
+ return self.optic.in_bounds(x, y, scale)
195
+
196
+ def _polygon(self, points, style, **kwargs):
197
+ super()._polygon(points, style, transform=self._crs, **kwargs)
198
+
199
+ def _calc_position(self):
200
+ self.observe = self.observer.observe(self.ephemeris_name)
201
+
202
+ target = SkyfieldStar(ra_hours=self.ra / 15, dec_degrees=self.dec)
203
+ self.pos_az, self.pos_alt = self.observer._apparent(
204
+ obj=target,
205
+ ephemeris=self.ephemeris_name,
206
+ )
207
+ if self.pos_alt < 0 and self.raise_on_below_horizon:
208
+ raise ValueError("Target is below horizon at specified time/location.")
209
+
210
+ def _adjust_radec_minmax(self):
211
+ fov = self.optic.true_fov
212
+ extent = geometry.rectangle(
213
+ center=(self.ra, self.dec),
214
+ height_degrees=fov,
215
+ width_degrees=fov,
216
+ )
217
+ minx, miny, maxx, maxy = extent.bounds
218
+ self.ra_min = minx
219
+ self.ra_max = maxx
220
+ self.dec_min = miny
221
+ self.dec_max = maxy
222
+
223
+ if self.ra_max < 0:
224
+ self.ra_max += 360
225
+ if self.ra_min < 0:
226
+ self.ra_min += 360
227
+
228
+ # handle wrapping
229
+ if self.ra_max < self.ra_min:
230
+ self.ra_max += 360
231
+
232
+ if self.dec > self.dec_max:
233
+ self.dec_max = 90
234
+ self.ra_min = 0
235
+ self.ra_max = 360
236
+
237
+ if self.dec < self.dec_min:
238
+ self.dec_min = -90
239
+ self.ra_min = 0
240
+ self.ra_max = 360
241
+
242
+ self.logger.debug(
243
+ f"Extent = RA ({self.ra_min:.2f}, {self.ra_max:.2f}) DEC ({self.dec_min:.2f}, {self.dec_max:.2f})"
244
+ )
245
+
246
+ def _in_bounds_xy(self, x: float, y: float) -> bool:
247
+ return self.in_bounds_altaz(y, x) # alt = y, az = x
248
+
249
+ def _prepare_star_coords(self, df):
250
+ df["x"], df["y"] = self.observer._apparent(
251
+ obj=SkyfieldStar.from_dataframe(df),
252
+ ephemeris=self.ephemeris_name,
253
+ )
254
+ return df
255
+
256
+ def _scatter_stars(self, ras, decs, sizes, alphas, colors, style=None, **kwargs):
257
+ plotted = super()._scatter_stars(
258
+ ras, decs, sizes, alphas, colors, style, **kwargs
259
+ )
260
+
261
+ if isinstance(self._background_clip_path, patches.Rectangle):
262
+ # convert to generic path to handle possible rotation angle:
263
+ clip_path = path.Path(self._background_clip_path.get_corners())
264
+ plotted.set_clip_path(clip_path, transform=self.ax.transData)
265
+ else:
266
+ plotted.set_clip_path(self._background_clip_path)
267
+
268
+ @use_style(ObjectStyle, "star")
269
+ def stars(
270
+ self,
271
+ where: list = None,
272
+ where_labels: list = None,
273
+ catalog: Catalog = BIG_SKY_MAG11,
274
+ style: ObjectStyle = None,
275
+ size_fn: Callable[[Star], float] = callables.size_by_magnitude_for_optic,
276
+ alpha_fn: Callable[[Star], float] = callables.alpha_by_magnitude,
277
+ color_fn: Callable[[Star], str] = None,
278
+ label_fn: Callable[[Star], str] = Star.get_label,
279
+ legend_label: str = "Star",
280
+ bayer_labels: bool = False,
281
+ flamsteed_labels: bool = False,
282
+ sql: str = None,
283
+ sql_labels: str = None,
284
+ collision_handler: CollisionHandler = None,
285
+ ):
286
+ """
287
+ Plots stars
288
+
289
+ Args:
290
+ where: A list of expressions that determine which stars to plot. See [Selecting Objects](/reference-selecting-objects/) for details.
291
+ where_labels: A list of expressions that determine which stars are labeled on the plot. See [Selecting Objects](/reference-selecting-objects/) for details.
292
+ catalog: The catalog of stars to use -- see [catalogs overview](/data/overview/) for details
293
+ style: If `None`, then the plot's style for stars will be used
294
+ size_fn: Callable for calculating the marker size of each star. If `None`, then the marker style's size will be used.
295
+ alpha_fn: Callable for calculating the alpha value (aka "opacity") of each star. If `None`, then the marker style's alpha will be used.
296
+ color_fn: Callable for calculating the color of each star. If `None`, then the marker style's color will be used.
297
+ label_fn: Callable for determining the label of each star.
298
+ legend_label: Label for stars in the legend. If `None`, then they will not be in the legend.
299
+ bayer_labels: If True, then Bayer labels for stars will be plotted.
300
+ flamsteed_labels: If True, then Flamsteed number labels for stars will be plotted.
301
+ sql: SQL query for selecting stars (table name is `_`). This query will be applied _after_ any filters in the `where` kwarg.
302
+ sql_labels: SQL query for selecting stars that will be labeled (table name is `_`). Applied _after_ any filters in the `where_labels` kwarg.
303
+ collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
304
+ """
305
+ optic_star_multiplier = self.FIELD_OF_VIEW_MAX / self.optic.true_fov
306
+ size_fn_mx = None
307
+
308
+ if size_fn is not None:
309
+
310
+ def size_fn_mx(s):
311
+ return size_fn(s) * optic_star_multiplier * 0.68
312
+
313
+ super().stars(
314
+ where=where,
315
+ where_labels=where_labels,
316
+ catalog=catalog,
317
+ style=style,
318
+ size_fn=size_fn_mx,
319
+ alpha_fn=alpha_fn,
320
+ color_fn=color_fn,
321
+ label_fn=label_fn,
322
+ legend_label=legend_label,
323
+ bayer_labels=bayer_labels,
324
+ flamsteed_labels=flamsteed_labels,
325
+ sql=sql,
326
+ sql_labels=sql_labels,
327
+ collision_handler=collision_handler,
328
+ )
329
+
330
+ @use_style(LabelStyle, "info_text")
331
+ def info(self, style: LabelStyle = None):
332
+ """
333
+ Plots a table with info about the plot, including:
334
+
335
+ - Target's position (alt/az and ra/dec)
336
+ - Observer's position (lat/lon and date/time)
337
+ - Optic details (type, magnification, FOV)
338
+
339
+ Args:
340
+ style: If `None`, then the plot's style for info text will be used
341
+ """
342
+ self.ax.set_xlim(-1.22 * self.optic.xlim, 1.22 * self.optic.xlim)
343
+ self.ax.set_ylim(-1.1 * self.optic.ylim, 1.1 * self.optic.ylim)
344
+ self.optic.transform(
345
+ self.ax
346
+ ) # apply transform again because new xy limits will undo the transform
347
+
348
+ dt_str = (
349
+ self.observer.dt.strftime("%m/%d/%Y @ %H:%M:%S")
350
+ + " "
351
+ + self.observer.dt.tzname()
352
+ )
353
+ font_size = style.font_size * self.scale
354
+
355
+ column_labels = [
356
+ "Target (Alt/Az)",
357
+ "Target (RA/DEC)",
358
+ "Observer Lat, Lon",
359
+ "Observer Date/Time",
360
+ f"Optic - {self.optic.label}",
361
+ ]
362
+ values = [
363
+ f"{self.pos_alt:.0f}\N{DEGREE SIGN} / {self.pos_az:.0f}\N{DEGREE SIGN} ({azimuth_to_string(self.pos_az)})",
364
+ f"{(self.ra / 15):.2f}h / {self.dec:.2f}\N{DEGREE SIGN}",
365
+ f"{self.observer.lat:.2f}\N{DEGREE SIGN}, {self.observer.lon:.2f}\N{DEGREE SIGN}",
366
+ dt_str,
367
+ str(self.optic),
368
+ ]
369
+ widths = [0.15, 0.15, 0.2, 0.2, 0.3]
370
+
371
+ table = self.ax.table(
372
+ cellText=[values],
373
+ cellLoc="center",
374
+ colWidths=widths,
375
+ rowLabels=[None],
376
+ colLabels=column_labels,
377
+ loc="bottom",
378
+ edges="vertical",
379
+ )
380
+ table.auto_set_font_size(False)
381
+ table.set_fontsize(style.font_size)
382
+ table.scale(1, 5)
383
+
384
+ # Apply style to all cells
385
+ for row in [0, 1]:
386
+ for col in range(len(values)):
387
+ table[row, col].set_text_props(**style.matplot_kwargs(self.scale))
388
+
389
+ # Apply some styles only to the header row
390
+ for col in range(len(values)):
391
+ table[0, col].set_text_props(fontweight="heavy", fontsize=font_size * 1.2)
392
+
393
+ def _plot_border(self):
394
+ # since we're using AzimuthalEquidistant projection, the center will always be (0, 0)
395
+ x = 0
396
+ y = 0
397
+
398
+ if self.style.has_gradient_background():
399
+ background_color = "#ffffff00"
400
+ # self._plot_gradient_background(self.style.background_color)
401
+ else:
402
+ background_color = self.style.background_color.as_hex()
403
+
404
+ # Background of Viewable Area
405
+ self._background_clip_path = self.optic.patch(
406
+ x,
407
+ y,
408
+ facecolor=background_color,
409
+ linewidth=0,
410
+ fill=True,
411
+ zorder=ZOrderEnum.LAYER_1,
412
+ )
413
+ self.ax.set_facecolor(background_color)
414
+ self.ax.add_patch(self._background_clip_path)
415
+ self._update_clip_path_polygon(buffer=15)
416
+
417
+ # Inner Border
418
+ # inner_border = self.optic.patch(
419
+ # x,
420
+ # y,
421
+ # linewidth=2 * self.scale,
422
+ # edgecolor=self.style.border_line_color.as_hex(),
423
+ # fill=False,
424
+ # zorder=ZOrderEnum.LAYER_5 + 100,
425
+ # )
426
+ # self.ax.add_patch(inner_border)
427
+
428
+ if self.style.has_gradient_background():
429
+ self._plot_gradient_background(self.style.background_color)
430
+
431
+ # Outer border
432
+ outer_border = self.optic.patch(
433
+ x,
434
+ y,
435
+ padding=0.05,
436
+ linewidth=25 * self.scale,
437
+ edgecolor=self.style.border_bg_color.as_hex(),
438
+ fill=False,
439
+ zorder=ZOrderEnum.LAYER_5,
440
+ )
441
+ self.ax.add_patch(outer_border)
442
+
443
+ def _init_plot(self):
444
+ self._proj = ccrs.AzimuthalEquidistant(
445
+ central_longitude=self.pos_az,
446
+ central_latitude=self.pos_alt,
447
+ )
448
+ self._proj.threshold = 1000
449
+ self.fig = plt.figure(
450
+ figsize=(self.figure_size, self.figure_size),
451
+ facecolor=self.style.figure_background_color.as_hex(),
452
+ # layout="constrained",
453
+ dpi=DPI,
454
+ )
455
+ self.ax = self.fig.add_subplot(1, 1, 1, projection=self._proj)
456
+ self.fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
457
+
458
+ self.ax.xaxis.set_visible(False)
459
+ self.ax.yaxis.set_visible(False)
460
+ self.ax.axis("off")
461
+
462
+ self._fit_to_ax()
463
+ self.ax.set_xlim(-1.06 * self.optic.xlim, 1.06 * self.optic.xlim)
464
+ self.ax.set_ylim(-1.06 * self.optic.ylim, 1.06 * self.optic.ylim)
465
+ self.optic.transform(self.ax)
466
+ self._plot_border()
467
+
468
+ # if self.gradient_preset:
469
+ # self.apply_gradient_background(self.gradient_preset)
@@ -0,0 +1,217 @@
1
+ import numpy as np
2
+ from matplotlib import path, patches
3
+
4
+ from starplot.coordinates import CoordinateSystem
5
+ from starplot.data.translations import translate
6
+ from starplot.plots.map import MapPlot
7
+ from starplot.models.observer import Observer
8
+ from starplot.projections import Stereographic
9
+ from starplot.styles import (
10
+ LabelStyle,
11
+ PlotStyle,
12
+ PathStyle,
13
+ GradientDirection,
14
+ extensions,
15
+ )
16
+ from starplot.styles.helpers import use_style
17
+ from starplot.plotters.text import CollisionHandler
18
+
19
+
20
+ class ZenithPlot(MapPlot):
21
+ """Creates a new zenith plot.
22
+
23
+ Args:
24
+ observer: Observer instance which specifies a time and place. Defaults to `Observer()`
25
+ ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
26
+ style: Styling for the plot (colors, sizes, fonts, etc). If `None`, it defaults to `PlotStyle()`
27
+ resolution: Size (in pixels) of largest dimension of the map
28
+ point_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for point labels.
29
+ area_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for area labels.
30
+ path_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for path labels.
31
+ scale: Scaling factor that will be applied to all sizes in styles (e.g. font size, marker size, line widths, etc). For example, if you want to make everything 2x bigger, then set the scale to 2. At `scale=1` and `resolution=4096` (the default), all sizes are optimized visually for a map that covers 1-3 constellations. So, if you're creating a plot of a _larger_ extent, then it'd probably be good to decrease the scale (i.e. make everything smaller) -- and _increase_ the scale if you're plotting a very small area.
32
+ autoscale: If True, then the scale will be set automatically based on resolution.
33
+ suppress_warnings: If True (the default), then all warnings will be suppressed
34
+
35
+ Returns:
36
+ ZenithPlot: A new instance of a ZenithPlot
37
+
38
+ """
39
+
40
+ _coordinate_system = CoordinateSystem.RA_DEC
41
+ _gradient_direction = GradientDirection.RADIAL
42
+
43
+ def __init__(
44
+ self,
45
+ observer: Observer = None,
46
+ ephemeris: str = "de421.bsp",
47
+ style: PlotStyle = None,
48
+ resolution: int = 4096,
49
+ point_label_handler: CollisionHandler = None,
50
+ area_label_handler: CollisionHandler = None,
51
+ path_label_handler: CollisionHandler = None,
52
+ scale: float = 1.0,
53
+ autoscale: bool = False,
54
+ suppress_warnings: bool = True,
55
+ *args,
56
+ **kwargs,
57
+ ) -> "ZenithPlot":
58
+ observer = observer or Observer()
59
+ projection = Stereographic(
60
+ center_ra=observer.lst,
61
+ center_dec=observer.lat,
62
+ )
63
+ style = style or PlotStyle().extend(extensions.MAP)
64
+
65
+ super().__init__(
66
+ projection,
67
+ 0,
68
+ 360,
69
+ -90,
70
+ 90,
71
+ observer,
72
+ ephemeris,
73
+ style,
74
+ resolution,
75
+ point_label_handler=point_label_handler,
76
+ area_label_handler=area_label_handler,
77
+ path_label_handler=path_label_handler,
78
+ clip_path=None,
79
+ scale=scale,
80
+ autoscale=autoscale,
81
+ suppress_warnings=suppress_warnings,
82
+ *args,
83
+ **kwargs,
84
+ )
85
+
86
+ @use_style(PathStyle, "horizon")
87
+ def horizon(
88
+ self,
89
+ style: PathStyle = None,
90
+ labels: list = ["N", "E", "S", "W"],
91
+ ):
92
+ """
93
+ Draws a [great circle](https://en.wikipedia.org/wiki/Great_circle) representing the horizon for the given `lat`, `lon` at time `dt` (so you must define these when creating the plot to use this function)
94
+
95
+ Args:
96
+ style: Style of the horizon path. If None, then the plot's style definition will be used.
97
+ labels: List of labels for cardinal directions. **NOTE: labels should be in the order: North, East, South, West.**
98
+ """
99
+ if self.observer is None:
100
+ raise ValueError("observer is required for plotting the horizon")
101
+
102
+ """
103
+ For zenith projections, we plot the horizon as a patch to make a more perfect circle
104
+ """
105
+ style_kwargs = style.line.matplot_kwargs(self.scale)
106
+ style_kwargs["clip_on"] = False
107
+ style_kwargs["edgecolor"] = style_kwargs.pop("color")
108
+ patch = patches.Circle(
109
+ (0.50, 0.50),
110
+ radius=0.454,
111
+ facecolor=None,
112
+ fill=False,
113
+ transform=self.ax.transAxes,
114
+ **style_kwargs,
115
+ )
116
+ self.ax.add_patch(patch)
117
+ self._background_clip_path = patch
118
+ self._update_clip_path_polygon(
119
+ buffer=style.line.width / 2 + 2 * style.line.edge_width + 40
120
+ )
121
+
122
+ if not labels:
123
+ return
124
+
125
+ labels = [translate(label, self.language) for label in labels]
126
+
127
+ label_ax_coords = [
128
+ (0.5, 0.95), # north
129
+ (0.045, 0.5), # east
130
+ (0.5, 0.045), # south
131
+ (0.954, 0.5), # west
132
+ ]
133
+ for label, coords in zip(labels, label_ax_coords):
134
+ self.ax.annotate(
135
+ label,
136
+ coords,
137
+ xycoords=self.ax.transAxes,
138
+ clip_on=False,
139
+ **style.label.matplot_kwargs(self.scale),
140
+ )
141
+
142
+ def _adjust_radec_minmax(self):
143
+ self.ra_min = 0
144
+ self.ra_max = 360
145
+ self.dec_min = -90
146
+ self.dec_max = 90
147
+
148
+ def _set_extent(self):
149
+ theta = np.linspace(0, 2 * np.pi, 100)
150
+ center, radius = [0.5, 0.5], 0.45
151
+ verts = np.vstack([np.sin(theta), np.cos(theta)]).T
152
+ circle = path.Path(verts * radius + center)
153
+ extent = self.ax.get_extent(crs=self._proj)
154
+ self.ax.set_extent((p / 3.548 for p in extent), crs=self._proj)
155
+ self.ax.set_boundary(circle, transform=self.ax.transAxes)
156
+
157
+ @use_style(LabelStyle, "info_text")
158
+ def info(self, style: LabelStyle = None):
159
+ """
160
+ Plots info text in the lower left corner, including date/time and lat/lon.
161
+
162
+ Args:
163
+ style: Styling of the info text. If None, then the plot's style definition will be used.
164
+ """
165
+ dt_str = self.dt.strftime("%m/%d/%Y @ %H:%M:%S") + " " + self.dt.tzname()
166
+ info = f"{str(self.observer.lat)}, {str(self.observer.lon)}\n{dt_str}"
167
+ self.ax.text(
168
+ 0.05,
169
+ 0.05,
170
+ info,
171
+ transform=self.ax.transAxes,
172
+ **style.matplot_kwargs(self.scale),
173
+ )
174
+
175
+ def _plot_background_clip_path(self):
176
+ if self.style.has_gradient_background():
177
+ background_color = "#ffffff00"
178
+ self._plot_gradient_background(self.style.background_color)
179
+ else:
180
+ background_color = self.style.background_color.as_hex()
181
+
182
+ self._background_clip_path = patches.Circle(
183
+ (0.50, 0.50),
184
+ radius=0.45,
185
+ fill=True,
186
+ facecolor=background_color,
187
+ # edgecolor=self.style.border_line_color.as_hex(),
188
+ linewidth=0,
189
+ zorder=-2_000,
190
+ transform=self.ax.transAxes,
191
+ )
192
+ self.ax.set_facecolor(background_color)
193
+
194
+ self.ax.add_patch(self._background_clip_path)
195
+ self._update_clip_path_polygon(buffer=20)
196
+
197
+ def _prepare_star_coords(self, df, limit_by_altaz=False):
198
+ # TODO : reconcile this commented code
199
+ # self.location = self.earth + wgs84.latlon(
200
+ # self.observer.lat, self.observer.lon
201
+ # )
202
+ # df["ra_hours"], df["dec_degrees"] = (df.ra / 15, df.dec)
203
+ # stars_apparent = (
204
+ # self.location.at(self.observer.timescale)
205
+ # .observe(SkyfieldStar.from_dataframe(df))
206
+ # .apparent()
207
+ # )
208
+ # # we only need altitude
209
+ # stars_alt, _, _ = stars_apparent.altaz()
210
+ # df["alt"] = stars_alt.degrees
211
+ # df = df[df["alt"] > 0]
212
+
213
+ df["x"], df["y"] = (
214
+ df["ra"],
215
+ df["dec"],
216
+ )
217
+ return df
@@ -0,0 +1,9 @@
1
+ # ruff: noqa: F401,F403
2
+
3
+ from .constellations import ConstellationPlotterMixin
4
+ from .stars import StarPlotterMixin
5
+ from .dsos import DsoPlotterMixin
6
+ from .milkyway import MilkyWayPlotterMixin
7
+ from .legend import LegendPlotterMixin
8
+ from .gradients import GradientBackgroundMixin
9
+ from .arrow import ArrowPlotterMixin