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
starplot/plots/map.py ADDED
@@ -0,0 +1,515 @@
1
+ import math
2
+ from typing import Callable
3
+ from functools import cache
4
+
5
+ from cartopy import crs as ccrs
6
+ from matplotlib import pyplot as plt
7
+ from matplotlib import patches, ticker
8
+ from matplotlib.ticker import FuncFormatter, FixedLocator
9
+ from shapely import Polygon
10
+ from skyfield.api import wgs84
11
+ import numpy as np
12
+
13
+ from starplot.coordinates import CoordinateSystem
14
+ from starplot import geometry
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
+ LegendPlotterMixin,
24
+ GradientBackgroundMixin,
25
+ ArrowPlotterMixin,
26
+ )
27
+ from starplot.plotters.text import CollisionHandler
28
+ from starplot.projections import StereoNorth, StereoSouth, ProjectionBase
29
+ from starplot.styles import (
30
+ ObjectStyle,
31
+ PlotStyle,
32
+ PathStyle,
33
+ GradientDirection,
34
+ extensions,
35
+ )
36
+ from starplot.styles.helpers import use_style
37
+ from starplot.utils import lon_to_ra, ra_to_lon
38
+
39
+
40
+ class MapPlot(
41
+ BasePlot,
42
+ ExtentMaskMixin,
43
+ StarPlotterMixin,
44
+ DsoPlotterMixin,
45
+ MilkyWayPlotterMixin,
46
+ ConstellationPlotterMixin,
47
+ LegendPlotterMixin,
48
+ GradientBackgroundMixin,
49
+ ArrowPlotterMixin,
50
+ ):
51
+ """Creates a new map plot.
52
+
53
+ Args:
54
+ projection: [Projection](/reference-mapplot/#projections) of the map
55
+ ra_min: Minimum right ascension of the map's extent, in degrees (0...360)
56
+ ra_max: Maximum right ascension of the map's extent, in degrees (0...360)
57
+ dec_min: Minimum declination of the map's extent, in degrees (-90...90)
58
+ dec_max: Maximum declination of the map's extent, in degrees (-90...90)
59
+ observer: Observer instance which specifies a time and place. Defaults to an observer at epoch J2000
60
+ ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
61
+ style: Styling for the plot (colors, sizes, fonts, etc). If `None`, it defaults to `PlotStyle()`
62
+ resolution: Size (in pixels) of largest dimension of the map
63
+ point_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for point labels.
64
+ area_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for area labels.
65
+ path_label_handler: Default [CollisionHandler][starplot.CollisionHandler] for path labels.
66
+ clip_path: An optional Shapely Polygon that specifies the clip path of the plot -- only objects inside the polygon will be plotted. If `None` (the default), then the clip path will be the extent of the map you specified with the RA/DEC parameters.
67
+ 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.
68
+ autoscale: If True, then the scale will be set automatically based on resolution.
69
+ suppress_warnings: If True (the default), then all warnings will be suppressed
70
+
71
+ Returns:
72
+ MapPlot: A new instance of a MapPlot
73
+
74
+ """
75
+
76
+ _coordinate_system = CoordinateSystem.RA_DEC
77
+ _gradient_direction = GradientDirection.LINEAR
78
+
79
+ def __init__(
80
+ self,
81
+ projection: ProjectionBase,
82
+ ra_min: float = 0,
83
+ ra_max: float = 360,
84
+ dec_min: float = -90,
85
+ dec_max: float = 90,
86
+ observer: Observer = None,
87
+ ephemeris: str = "de421.bsp",
88
+ style: PlotStyle = None,
89
+ resolution: int = 4096,
90
+ point_label_handler: CollisionHandler = None,
91
+ area_label_handler: CollisionHandler = None,
92
+ path_label_handler: CollisionHandler = None,
93
+ clip_path: Polygon = None,
94
+ scale: float = 1.0,
95
+ autoscale: bool = False,
96
+ suppress_warnings: bool = True,
97
+ *args,
98
+ **kwargs,
99
+ ) -> "MapPlot":
100
+ observer = observer or Observer.at_epoch(2000)
101
+ style = style or PlotStyle().extend(extensions.MAP)
102
+
103
+ super().__init__(
104
+ observer,
105
+ ephemeris,
106
+ style,
107
+ resolution,
108
+ point_label_handler=point_label_handler,
109
+ area_label_handler=area_label_handler,
110
+ path_label_handler=path_label_handler,
111
+ scale=scale,
112
+ autoscale=autoscale,
113
+ suppress_warnings=suppress_warnings,
114
+ *args,
115
+ **kwargs,
116
+ )
117
+ self.logger.debug("Creating MapPlot...")
118
+
119
+ if ra_min > ra_max:
120
+ raise ValueError("ra_min must be less than ra_max")
121
+ if dec_min > dec_max:
122
+ raise ValueError("dec_min must be less than dec_max")
123
+ if dec_min < -90 or dec_max > 90:
124
+ raise ValueError("Declination out of range (must be -90...90)")
125
+
126
+ self.projection = projection
127
+ self.ra_min = ra_min
128
+ self.ra_max = ra_max
129
+ self.dec_min = dec_min
130
+ self.dec_max = dec_max
131
+
132
+ self.clip_path = clip_path
133
+
134
+ self._geodetic = ccrs.Geodetic()
135
+ self._plate_carree = ccrs.PlateCarree()
136
+ self._crs = ccrs.CRS(
137
+ proj4_params=[
138
+ ("proj", "latlong"),
139
+ ("axis", "wnu"), # invert
140
+ ("a", "6378137"),
141
+ ],
142
+ globe=ccrs.Globe(ellipse="sphere", flattening=0),
143
+ )
144
+ self._init_plot()
145
+
146
+ def _plot_kwargs(self) -> dict:
147
+ return dict(transform=self._crs)
148
+
149
+ @cache
150
+ def in_bounds(self, ra: float, dec: float) -> bool:
151
+ """Determine if a coordinate is within the bounds of the plot.
152
+
153
+ Args:
154
+ ra: Right ascension, in degrees (0...360)
155
+ dec: Declination, in degrees (-90...90)
156
+
157
+ Returns:
158
+ True if the coordinate is in bounds, otherwise False
159
+ """
160
+ # TODO : try using pyproj transformer directly
161
+ x, y = self._proj.transform_point(ra, dec, self._crs)
162
+ data_to_axes = self.ax.transData + self.ax.transAxes.inverted()
163
+ x_axes, y_axes = data_to_axes.transform((x, y))
164
+ return 0 <= x_axes <= 1 and 0 <= y_axes <= 1
165
+
166
+ def _in_bounds_xy(self, x: float, y: float) -> bool:
167
+ return self.in_bounds(x, y)
168
+
169
+ def _polygon(self, points, style, **kwargs):
170
+ super()._polygon(points, style, transform=self._crs, **kwargs)
171
+
172
+ def _latlon_bounds(self):
173
+ # convert the RA/DEC bounds to lat/lon bounds
174
+ return [
175
+ -1 * self.ra_min,
176
+ -1 * self.ra_max,
177
+ self.dec_min,
178
+ self.dec_max,
179
+ ]
180
+
181
+ def _adjust_radec_minmax(self):
182
+ if self._is_global_extent():
183
+ return
184
+
185
+ # adjust declination to match extent
186
+ extent = self.ax.get_extent(crs=self._plate_carree)
187
+ self.dec_min = extent[2]
188
+ self.dec_max = extent[3]
189
+
190
+ # adjust the RA min/max if the DEC bounds is near the poles
191
+ if (
192
+ isinstance(self.projection, StereoNorth)
193
+ or isinstance(self.projection, StereoSouth)
194
+ ) and (self.dec_max > 80 or self.dec_min < -80):
195
+ self.ra_min = 0
196
+ self.ra_max = 360
197
+
198
+ elif self.ra_max < 360:
199
+ # adjust right ascension to match extent
200
+ ra_min = extent[1] * -1
201
+ ra_max = extent[0] * -1
202
+
203
+ if ra_min < 0 or ra_max < 0:
204
+ ra_min += 360
205
+ ra_max += 360
206
+
207
+ self.ra_min = ra_min
208
+ self.ra_max = ra_max
209
+
210
+ else:
211
+ self.ra_min = lon_to_ra(extent[1]) * 15
212
+ self.ra_max = lon_to_ra(extent[0]) * 15 + 360
213
+
214
+ self.logger.debug(
215
+ f"Extent = RA ({self.ra_min:.2f}, {self.ra_max:.2f}) DEC ({self.dec_min:.2f}, {self.dec_max:.2f})"
216
+ )
217
+
218
+ @use_style(ObjectStyle, "zenith")
219
+ def zenith(
220
+ self,
221
+ style: ObjectStyle = None,
222
+ label: str = None,
223
+ legend_label: str = "Zenith",
224
+ ):
225
+ """
226
+ Plots a marker for the zenith (requires `lat`, `lon`, and `dt` to be defined when creating the plot)
227
+
228
+ Args:
229
+ style: Style of the zenith marker. If None, then the plot's style definition will be used.
230
+ label: Label for the zenith
231
+ legend_label: Label in the legend
232
+ """
233
+ if self.observer is None:
234
+ raise ValueError("observer is required for plotting the zenith")
235
+
236
+ geographic = wgs84.latlon(
237
+ latitude_degrees=self.observer.lat, longitude_degrees=self.observer.lon
238
+ )
239
+ observer = geographic.at(self.observer.timescale)
240
+ zenith = observer.from_altaz(alt_degrees=90, az_degrees=0)
241
+ ra, dec, _ = zenith.radec()
242
+
243
+ self.marker(
244
+ ra=ra.hours * 15,
245
+ dec=dec.degrees,
246
+ style=style,
247
+ label=label,
248
+ legend_label=legend_label,
249
+ )
250
+
251
+ @use_style(PathStyle, "horizon")
252
+ def horizon(
253
+ self,
254
+ style: PathStyle = None,
255
+ labels: list = ["N", "E", "S", "W"],
256
+ ):
257
+ """
258
+ 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)
259
+
260
+ Args:
261
+ style: Style of the horizon path. If None, then the plot's style definition will be used.
262
+ labels: List of labels for cardinal directions. **NOTE: labels should be in the order: North, East, South, West.**
263
+ """
264
+ if self.observer is None:
265
+ raise ValueError("observer is required for plotting the horizon")
266
+
267
+ geographic = wgs84.latlon(
268
+ latitude_degrees=self.observer.lat, longitude_degrees=self.observer.lon
269
+ )
270
+ observer = geographic.at(self.observer.timescale)
271
+ zenith = observer.from_altaz(alt_degrees=90, az_degrees=0)
272
+ ra, dec, _ = zenith.radec()
273
+
274
+ polygon = geometry.ellipse(
275
+ center=(ra.hours * 15, dec.degrees),
276
+ height_degrees=180,
277
+ width_degrees=180,
278
+ num_pts=100,
279
+ )
280
+ points = list(zip(*polygon.exterior.coords.xy))
281
+ x = []
282
+ y = []
283
+
284
+ for ra, dec in points:
285
+ x0, y0 = self._prepare_coords(ra, dec)
286
+ x.append(x0)
287
+ y.append(y0)
288
+
289
+ style_kwargs = {}
290
+ style_kwargs["clip_on"] = True
291
+ style_kwargs["clip_path"] = self._background_clip_path
292
+ self.ax.plot(
293
+ x,
294
+ y,
295
+ dash_capstyle=style.line.dash_capstyle,
296
+ **style.line.matplot_kwargs(self.scale),
297
+ **style_kwargs,
298
+ **self._plot_kwargs(),
299
+ )
300
+
301
+ if not labels:
302
+ return
303
+
304
+ north = observer.from_altaz(alt_degrees=0, az_degrees=0)
305
+ east = observer.from_altaz(alt_degrees=0, az_degrees=90)
306
+ south = observer.from_altaz(alt_degrees=0, az_degrees=180)
307
+ west = observer.from_altaz(alt_degrees=0, az_degrees=270)
308
+
309
+ cardinal_directions = [north, east, south, west]
310
+
311
+ text_kwargs = dict(
312
+ **style.label.matplot_kwargs(self.scale),
313
+ xytext=(
314
+ style.label.offset_x * self.scale,
315
+ style.label.offset_y * self.scale,
316
+ ),
317
+ textcoords="offset points",
318
+ path_effects=[],
319
+ clip_on=True,
320
+ )
321
+
322
+ for i, position in enumerate(cardinal_directions):
323
+ ra, dec, _ = position.radec()
324
+ x, y = self._prepare_coords(ra.hours * 15, dec.degrees)
325
+ self._text(x, y, labels[i], **text_kwargs)
326
+
327
+ @use_style(PathStyle, "gridlines")
328
+ def gridlines(
329
+ self,
330
+ style: PathStyle = None,
331
+ labels: bool = True,
332
+ ra_locations: list[float] = None,
333
+ dec_locations: list[float] = None,
334
+ ra_formatter_fn: Callable[[float], str] = None,
335
+ dec_formatter_fn: Callable[[float], str] = None,
336
+ tick_marks: bool = False,
337
+ ra_tick_locations: list[float] = None,
338
+ dec_tick_locations: list[float] = None,
339
+ ):
340
+ """Plots gridlines
341
+
342
+ Args:
343
+ style: Styling of the gridlines. If None, then the plot's style (specified when creating the plot) will be used
344
+ labels: If True, then labels for each gridline will be plotted on the outside of the axes.
345
+ ra_locations: List of Right Ascension locations for the gridlines (in degrees, 0...360). Defaults to every 15 degrees.
346
+ dec_locations: List of Declination locations for the gridlines (in degrees, -90...90). Defaults to every 10 degrees.
347
+ ra_formatter_fn: Callable for creating labels of right ascension gridlines
348
+ dec_formatter_fn: Callable for creating labels of declination gridlines
349
+ tick_marks: If True, then tick marks will be plotted outside the axis. **Only supported for rectangular projections (e.g. Mercator, Miller)**
350
+ ra_tick_locations: List of Right Ascension locations for the tick marks (in degrees, 0...260)
351
+ dec_tick_locations: List of Declination locations for the tick marks (in degrees, -90...90)
352
+ """
353
+
354
+ ra_formatter_fn_default = lambda r: f"{math.floor(r)}h" # noqa: E731
355
+ dec_formatter_fn_default = lambda d: f"{round(d)}\u00b0 " # noqa: E731
356
+
357
+ ra_formatter_fn = ra_formatter_fn or ra_formatter_fn_default
358
+ dec_formatter_fn = dec_formatter_fn or dec_formatter_fn_default
359
+
360
+ def ra_formatter(x, pos) -> str:
361
+ ra = lon_to_ra(x)
362
+ return ra_formatter_fn(ra)
363
+
364
+ def dec_formatter(x, pos) -> str:
365
+ return dec_formatter_fn(x)
366
+
367
+ ra_locations = ra_locations or [x for x in range(0, 360, 15)]
368
+ dec_locations = dec_locations or [d for d in range(-80, 90, 10)]
369
+
370
+ line_style_kwargs = style.line.matplot_kwargs(self.scale)
371
+ gridlines = self.ax.gridlines(
372
+ draw_labels=labels,
373
+ x_inline=False,
374
+ y_inline=False,
375
+ rotate_labels=False,
376
+ xpadding=12,
377
+ ypadding=12,
378
+ clip_on=True,
379
+ clip_path=self._background_clip_path,
380
+ gid="gridlines",
381
+ **line_style_kwargs,
382
+ )
383
+ gridlines.set_zorder(style.line.zorder)
384
+
385
+ if labels:
386
+ self._axis_labels = True
387
+
388
+ label_style_kwargs = style.label.matplot_kwargs(self.scale)
389
+ label_style_kwargs.pop("va")
390
+ label_style_kwargs.pop("ha")
391
+
392
+ if self.dec_max > 75 or self.dec_min < -75:
393
+ # if the extent is near the poles, then plot the RA gridlines again
394
+ # because cartopy does not extend lines to poles
395
+ for ra in ra_locations:
396
+ self.ax.plot(
397
+ (ra, ra),
398
+ (-90, 90),
399
+ gid="gridlines",
400
+ **line_style_kwargs,
401
+ **self._plot_kwargs(),
402
+ )
403
+
404
+ gridlines.xlocator = FixedLocator([ra_to_lon(r / 15) for r in ra_locations])
405
+ gridlines.xformatter = FuncFormatter(ra_formatter)
406
+ gridlines.xlabel_style = label_style_kwargs
407
+
408
+ gridlines.ylocator = FixedLocator(dec_locations)
409
+ gridlines.yformatter = FuncFormatter(dec_formatter)
410
+ gridlines.ylabel_style = label_style_kwargs
411
+
412
+ if tick_marks:
413
+ self._tick_marks(style, ra_tick_locations, dec_tick_locations)
414
+
415
+ def _tick_marks(self, style, ra_tick_locations=None, dec_tick_locations=None):
416
+ def in_axes(ra):
417
+ return self.in_bounds(ra, (self.dec_max + self.dec_min) / 2)
418
+
419
+ xticks = ra_tick_locations or [x for x in np.arange(0, 360, 1.875)]
420
+ yticks = dec_tick_locations or [x for x in np.arange(-90, 90, 1)]
421
+
422
+ inbound_xticks = [ra_to_lon(ra / 15) for ra in xticks if in_axes(ra)]
423
+ self.ax.set_xticks(inbound_xticks, crs=self._plate_carree)
424
+ self.ax.xaxis.set_major_formatter(ticker.NullFormatter())
425
+
426
+ inbound_yticks = [y for y in yticks if y < self.dec_max and y > self.dec_min]
427
+ self.ax.set_yticks(inbound_yticks, crs=self._plate_carree)
428
+ self.ax.yaxis.set_major_formatter(ticker.NullFormatter())
429
+
430
+ self.ax.tick_params(
431
+ which="major",
432
+ width=1,
433
+ length=8,
434
+ color=style.label.font_color.as_hex(),
435
+ top=True,
436
+ right=True,
437
+ )
438
+
439
+ def _set_extent(self):
440
+ bounds = self._latlon_bounds()
441
+ if self._is_global_extent():
442
+ # this cartopy function works better for setting global extents
443
+ self.ax.set_global()
444
+ else:
445
+ self.ax.set_extent(bounds, crs=self._plate_carree)
446
+
447
+ def _init_plot(self):
448
+ self.fig = plt.figure(
449
+ figsize=(self.figure_size, self.figure_size),
450
+ facecolor=self.style.figure_background_color.as_hex(),
451
+ # layout="constrained",
452
+ dpi=DPI,
453
+ )
454
+
455
+ self._proj = self.projection.crs
456
+ self.ax = self.fig.add_subplot(1, 1, 1, projection=self._proj)
457
+ self.fig.subplots_adjust(left=0, right=1, top=1, bottom=0)
458
+
459
+ self._set_extent()
460
+ self._adjust_radec_minmax()
461
+
462
+ self.logger.debug(f"Projection = {self.projection.__class__.__name__.upper()}")
463
+
464
+ self._fit_to_ax()
465
+ self._plot_background_clip_path()
466
+
467
+ def _ax_to_radec(self, x, y):
468
+ trans = self.ax.transAxes + self.ax.transData.inverted()
469
+ x_projected, y_projected = trans.transform((x, y)) # axes to data
470
+ x_ra, y_ra = self._crs.transform_point(x_projected, y_projected, self._proj)
471
+ return (x_ra + 360), y_ra
472
+
473
+ def _plot_background_clip_path(self):
474
+ if self.style.has_gradient_background():
475
+ background_color = "#ffffff00"
476
+ self._plot_gradient_background(self.style.background_color)
477
+ else:
478
+ background_color = self.style.background_color.as_hex()
479
+
480
+ def to_axes(points):
481
+ ax_points = []
482
+
483
+ for ra, dec in points:
484
+ x, y = self._proj.transform_point(ra, dec, self._crs)
485
+ data_to_axes = self.ax.transData + self.ax.transAxes.inverted()
486
+ x_axes, y_axes = data_to_axes.transform((x, y))
487
+ ax_points.append([x_axes, y_axes])
488
+ return ax_points
489
+
490
+ if self.clip_path is not None:
491
+ points = list(zip(*self.clip_path.exterior.coords.xy))
492
+ self._background_clip_path = patches.Polygon(
493
+ to_axes(points),
494
+ facecolor=background_color,
495
+ fill=True,
496
+ zorder=-2_000,
497
+ transform=self.ax.transAxes,
498
+ )
499
+ else:
500
+ # draw patch in axes coords, which are easier to work with
501
+ # in cases like this cause they go from 0...1 in all plots
502
+ self._background_clip_path = patches.Rectangle(
503
+ (0, 0),
504
+ width=1,
505
+ height=1,
506
+ facecolor=background_color,
507
+ linewidth=0,
508
+ fill=True,
509
+ zorder=-2_000,
510
+ transform=self.ax.transAxes,
511
+ )
512
+
513
+ self.ax.set_facecolor(background_color)
514
+ self.ax.add_patch(self._background_clip_path)
515
+ self._update_clip_path_polygon()