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/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()
|