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,372 @@
1
+ from functools import cache
2
+ from typing import Callable
3
+
4
+ import pandas as pd
5
+ import numpy as np
6
+ import astropy.units as u
7
+ from astropy.coordinates import SkyCoord
8
+ from cartopy import crs as ccrs
9
+ from matplotlib import pyplot as plt, patches
10
+ from matplotlib.ticker import FixedLocator, FuncFormatter
11
+ from skyfield.api import Star as SkyfieldStar
12
+ from skyfield.framelib import galactic_frame
13
+
14
+ from starplot.coordinates import CoordinateSystem
15
+ from starplot.plots.base import BasePlot, DPI
16
+ from starplot.mixins import ExtentMaskMixin
17
+ from starplot.models.observer import Observer
18
+ from starplot.plotters import (
19
+ ConstellationPlotterMixin,
20
+ StarPlotterMixin,
21
+ DsoPlotterMixin,
22
+ MilkyWayPlotterMixin,
23
+ GradientBackgroundMixin,
24
+ LegendPlotterMixin,
25
+ ArrowPlotterMixin,
26
+ )
27
+ from starplot.plotters.text import CollisionHandler
28
+ from starplot.styles import (
29
+ PlotStyle,
30
+ extensions,
31
+ use_style,
32
+ PathStyle,
33
+ GradientDirection,
34
+ )
35
+ from starplot.profile import profile
36
+
37
+
38
+ class GalaxyPlot(
39
+ BasePlot,
40
+ ExtentMaskMixin,
41
+ ConstellationPlotterMixin,
42
+ StarPlotterMixin,
43
+ DsoPlotterMixin,
44
+ MilkyWayPlotterMixin,
45
+ LegendPlotterMixin,
46
+ GradientBackgroundMixin,
47
+ ArrowPlotterMixin,
48
+ ):
49
+ """Creates a new galaxy plot.
50
+
51
+ Args:
52
+ center_lon: Central galactic longitude of the Mollweide projection
53
+ observer: Observer instance which specifies a time and place. Defaults to an observer at epoch J2000
54
+ ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
55
+ style: Styling for the plot (colors, sizes, fonts, etc). If `None`, it defaults to `PlotStyle()` with the MAP extension
56
+ resolution: Size (in pixels) of largest dimension of the map
57
+ point_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for point labels.
58
+ area_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for area labels.
59
+ path_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for path labels.
60
+ scale: Scaling factor that will be applied to all relevant sizes in styles (e.g. font size, marker size, line widths, etc). For example, if you want to make everything 2x bigger, then set scale to 2.
61
+ autoscale: If True, then the scale will be automatically set based on resolution
62
+ suppress_warnings: If True (the default), then all warnings will be suppressed
63
+
64
+ Returns:
65
+ GalaxyPlot: A new instance of a GalaxyPlot
66
+
67
+ """
68
+
69
+ _coordinate_system = CoordinateSystem.RA_DEC
70
+ _gradient_direction = GradientDirection.MOLLWEIDE
71
+
72
+ def __init__(
73
+ self,
74
+ center_lon: float = 0,
75
+ observer: Observer = None,
76
+ ephemeris: str = "de421.bsp",
77
+ style: PlotStyle = None,
78
+ resolution: int = 4096,
79
+ point_label_handler: CollisionHandler = None,
80
+ area_label_handler: CollisionHandler = None,
81
+ path_label_handler: CollisionHandler = None,
82
+ scale: float = 1.0,
83
+ autoscale: bool = False,
84
+ suppress_warnings: bool = True,
85
+ *args,
86
+ **kwargs,
87
+ ) -> "GalaxyPlot":
88
+ observer = observer or Observer.at_epoch(2000)
89
+ style = style or PlotStyle().extend(extensions.MAP)
90
+
91
+ super().__init__(
92
+ observer,
93
+ ephemeris,
94
+ style,
95
+ resolution,
96
+ point_label_handler=point_label_handler,
97
+ area_label_handler=area_label_handler,
98
+ path_label_handler=path_label_handler,
99
+ scale=scale,
100
+ autoscale=autoscale,
101
+ suppress_warnings=suppress_warnings,
102
+ *args,
103
+ **kwargs,
104
+ )
105
+
106
+ self.center_lon = center_lon
107
+ self.logger.debug("Creating GalaxyPlot...")
108
+ self._geodetic = ccrs.Geodetic()
109
+ self._plate_carree = ccrs.PlateCarree()
110
+
111
+ self._crs = ccrs.CRS(
112
+ proj4_params=[
113
+ ("proj", "latlong"),
114
+ ("axis", "wnu"), # invert
115
+ ("a", "6378137"),
116
+ ],
117
+ globe=ccrs.Globe(ellipse="sphere", flattening=0),
118
+ )
119
+
120
+ self._init_plot()
121
+ self._calc_position()
122
+
123
+ def _prepare_coords(self, ra, dec) -> (float, float):
124
+ """Converts RA/DEC to galactic coordinates (degrees)"""
125
+ if ra > 360:
126
+ ra -= 360
127
+ if ra < 0:
128
+ ra += 360
129
+ point = SkyfieldStar(ra_hours=ra / 15, dec_degrees=dec)
130
+ lat, lon, _ = self.observe(point).frame_latlon(galactic_frame)
131
+
132
+ return lon.degrees, lat.degrees
133
+
134
+ def _prepare_coords_many(
135
+ self, coordinates: list, epoch_year: float = 2000
136
+ ) -> (float, float):
137
+ """Converts RA/DEC to AZ/ALT"""
138
+ df = pd.DataFrame(coordinates, columns=["ra", "dec"])
139
+ df["ra_hours"], df["dec_degrees"] = (df.ra / 15, df.dec)
140
+ df["epoch_year"] = epoch_year
141
+
142
+ sf_star = SkyfieldStar.from_dataframe(df)
143
+ position = self.observe(sf_star)
144
+ lat, lon, _ = position.frame_latlon(galactic_frame)
145
+ df["x"], df["y"] = (lon.degrees, lat.degrees)
146
+
147
+ return list(zip(df["x"], df["y"]))
148
+
149
+ def _prepare_star_coords(self, df, limit_by_altaz=True):
150
+ stars_position = self.observe(SkyfieldStar.from_dataframe(df))
151
+ lat, lon, _ = stars_position.frame_latlon(galactic_frame)
152
+ df["x"], df["y"] = (lon.degrees, lat.degrees)
153
+ return df
154
+
155
+ def _plot_kwargs(self) -> dict:
156
+ return dict(transform=self._crs)
157
+
158
+ @cache
159
+ def in_bounds(self, ra, dec) -> bool:
160
+ """Determine if a coordinate is within the bounds of the plot.
161
+
162
+ Args:
163
+ ra: Right ascension, in hours (0...24)
164
+ dec: Declination, in degrees (-90...90)
165
+
166
+ Returns:
167
+ True if the coordinate is in bounds, otherwise False
168
+ """
169
+ lon, lat = self._prepare_coords(ra, dec)
170
+ return self.in_bounds_lonlat(lon, lat)
171
+
172
+ def in_bounds_lonlat(self, lon, lat) -> bool:
173
+ """Determine if a galactic coordinate is within the bounds of the plot.
174
+
175
+ Args:
176
+ lon: Galactic longitude in degrees (0...360)
177
+ lat: Galactic latitude in degrees (-90...90)
178
+
179
+ Returns:
180
+ True if the coordinate is in bounds, otherwise False
181
+ """
182
+ x, y = self._proj.transform_point(lon, lat, self._crs)
183
+ data_to_axes = self.ax.transData + self.ax.transAxes.inverted()
184
+ x_axes, y_axes = data_to_axes.transform((x, y))
185
+ return 0 <= x_axes <= 1 and 0 <= y_axes <= 1
186
+
187
+ def _in_bounds_xy(self, x: float, y: float) -> bool:
188
+ return self.in_bounds_lonlat(x, y)
189
+
190
+ def _polygon(self, points, style, **kwargs):
191
+ super()._polygon(points, style, transform=self._crs, **kwargs)
192
+
193
+ def _calc_position(self):
194
+ self.location = self.ephemeris["earth"]
195
+ self.observe = self.location.at(self.observer.timescale).observe
196
+
197
+ self.ra_min = 0
198
+ self.ra_max = 360
199
+ self.dec_min = -90
200
+ self.dec_max = 90
201
+
202
+ self.logger.debug(
203
+ f"Extent = RA ({self.ra_min:.2f}, {self.ra_max:.2f}) DEC ({self.dec_min:.2f}, {self.dec_max:.2f})"
204
+ )
205
+
206
+ @profile
207
+ @use_style(PathStyle, "galactic_equator")
208
+ def galactic_equator(
209
+ self,
210
+ style: PathStyle = None,
211
+ label: str = "GALACTIC EQUATOR",
212
+ num_labels: int = 1,
213
+ collision_handler: CollisionHandler = None,
214
+ ):
215
+ """
216
+ Plots the galactic equator
217
+
218
+ Args:
219
+ style: Styling of the galactic equator. If None, then the plot's style will be used
220
+ label: How the galactic equator will be labeled on the plot
221
+ num_labels: Max number of labels to plot along the line
222
+ 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.
223
+ """
224
+ lons = np.array([ra for ra in range(0, 361)]) # galactic longitudes
225
+ lats = np.array([0] * 361) # galactic latitudes
226
+
227
+ coords = SkyCoord(l=lons * u.deg, b=lats * u.deg, frame="galactic")
228
+ coords_eq = coords.icrs
229
+
230
+ ra_values = coords_eq.ra.degree
231
+ dec_values = coords_eq.dec.degree
232
+
233
+ radec = list(zip(ra_values, dec_values))
234
+
235
+ self.line(
236
+ label=label,
237
+ num_labels=num_labels,
238
+ collision_handler=collision_handler,
239
+ style=style,
240
+ coordinates=radec,
241
+ )
242
+
243
+ @use_style(PathStyle, "gridlines")
244
+ def gridlines(
245
+ self,
246
+ style: PathStyle = None,
247
+ show_labels: list = ["left", "right", "bottom"],
248
+ lon_locations: list[float] = None,
249
+ lat_locations: list[float] = None,
250
+ lon_formatter_fn: Callable[[float], str] = None,
251
+ lat_formatter_fn: Callable[[float], str] = None,
252
+ inline: bool = True,
253
+ ):
254
+ """
255
+ Plots gridlines
256
+
257
+ Args:
258
+ style: Styling of the gridlines. If None, then the plot's style (specified when creating the plot) will be used
259
+ show_labels: List of locations where labels should be shown (options: "left", "right", "top", "bottom")
260
+ az_locations: List of azimuth locations for the gridlines (in degrees, 0...360). Defaults to every 15 degrees
261
+ alt_locations: List of altitude locations for the gridlines (in degrees, -90...90). Defaults to every 10 degrees.
262
+ az_formatter_fn: Callable for creating labels of azimuth gridlines
263
+ alt_formatter_fn: Callable for creating labels of altitude gridlines
264
+ divider_line: If True, then a divider line will be plotted below the azimuth labels on the bottom of the plot (this is helpful when also plotting the horizon)
265
+ show_ticks: If True, then tick marks will be plotted on the horizon path for every `tick_step` degree that is not also a degree label
266
+ tick_step: Step size for tick marks
267
+ """
268
+ lon_formatter_fn_default = lambda lon: f"{round(lon)}\u00b0 " # noqa: E731
269
+ lat_formatter_fn_default = lambda lat: f"{round(lat)}\u00b0 " # noqa: E731
270
+
271
+ lon_formatter_fn = lon_formatter_fn or lon_formatter_fn_default
272
+ lat_formatter_fn = lat_formatter_fn or lat_formatter_fn_default
273
+
274
+ def lon_formatter(x, pos) -> str:
275
+ if x < 0:
276
+ x += 360
277
+ return lon_formatter_fn(x)
278
+
279
+ def lat_formatter(x, pos) -> str:
280
+ return lat_formatter_fn(x)
281
+
282
+ x_locations = (
283
+ lon_locations
284
+ if lon_locations is not None
285
+ else [x for x in range(0, 360, 15)]
286
+ )
287
+ x_locations = [x - 180 for x in x_locations]
288
+ y_locations = (
289
+ lat_locations
290
+ if lat_locations is not None
291
+ else [y for y in range(-90, 90, 10)]
292
+ )
293
+
294
+ label_style_kwargs = style.label.matplot_kwargs(self.scale)
295
+ label_style_kwargs.pop("va")
296
+ label_style_kwargs.pop("ha")
297
+
298
+ line_style_kwargs = style.line.matplot_kwargs(self.scale)
299
+ gridlines = self.ax.gridlines(
300
+ draw_labels=show_labels,
301
+ x_inline=inline,
302
+ y_inline=inline,
303
+ rotate_labels=False,
304
+ # xpadding=12,
305
+ # ypadding=12,
306
+ gid="gridlines",
307
+ xlocs=FixedLocator(x_locations),
308
+ xformatter=FuncFormatter(lon_formatter),
309
+ xlabel_style=label_style_kwargs,
310
+ ylocs=FixedLocator(y_locations),
311
+ ylabel_style=label_style_kwargs,
312
+ yformatter=FuncFormatter(lat_formatter),
313
+ **line_style_kwargs,
314
+ )
315
+ gridlines.set_zorder(style.line.zorder)
316
+
317
+ @cache
318
+ def _to_ax(self, az: float, alt: float) -> tuple[float, float]:
319
+ """Converts az/alt to axes coordinates"""
320
+ x, y = self._proj.transform_point(az, alt, self._crs)
321
+ data_to_axes = self.ax.transData + self.ax.transAxes.inverted()
322
+ x_axes, y_axes = data_to_axes.transform((x, y))
323
+ return x_axes, y_axes
324
+
325
+ @cache
326
+ def _ax_to_azalt(self, x: float, y: float) -> tuple[float, float]:
327
+ trans = self.ax.transAxes + self.ax.transData.inverted()
328
+ x_projected, y_projected = trans.transform((x, y)) # axes to data
329
+ az, alt = self._crs.transform_point(x_projected, y_projected, self._proj)
330
+ return float(az), float(alt)
331
+
332
+ def _plot_background_clip_path(self):
333
+ if self.style.has_gradient_background():
334
+ background_color = "#ffffff00"
335
+ self._plot_gradient_background(self.style.background_color)
336
+ else:
337
+ background_color = self.style.background_color.as_hex()
338
+
339
+ self._background_clip_path = patches.Rectangle(
340
+ (0, 0),
341
+ width=1,
342
+ height=1,
343
+ facecolor=background_color,
344
+ linewidth=0,
345
+ fill=True,
346
+ zorder=-3_000,
347
+ transform=self.ax.transAxes,
348
+ )
349
+ self.ax.set_facecolor(background_color)
350
+
351
+ self.ax.add_patch(self._background_clip_path)
352
+ self._update_clip_path_polygon()
353
+
354
+ def _init_plot(self):
355
+ self._proj = ccrs.Mollweide(central_longitude=self.center_lon)
356
+ self._proj.threshold = 100
357
+ self.fig = plt.figure(
358
+ figsize=(self.figure_size, self.figure_size),
359
+ facecolor=self.style.figure_background_color.as_hex(),
360
+ dpi=DPI,
361
+ )
362
+ self.ax = self.fig.add_subplot(1, 1, 1, projection=self._proj)
363
+ self.fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
364
+
365
+ self.ax.xaxis.set_visible(False)
366
+ self.ax.yaxis.set_visible(False)
367
+ self.ax.axis("off")
368
+
369
+ self.ax.set_global()
370
+
371
+ self._fit_to_ax()
372
+ self._plot_background_clip_path()