starplot 0.13.0__py2.py3-none-any.whl → 0.15.0__py2.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 (74) hide show
  1. starplot/__init__.py +5 -2
  2. starplot/base.py +311 -197
  3. starplot/cli.py +33 -0
  4. starplot/coordinates.py +6 -0
  5. starplot/data/__init__.py +6 -24
  6. starplot/data/bigsky.py +58 -40
  7. starplot/data/constellation_lines.py +827 -0
  8. starplot/data/constellation_stars.py +1501 -0
  9. starplot/data/constellations.py +600 -27
  10. starplot/data/db.py +17 -0
  11. starplot/data/dsos.py +24 -141
  12. starplot/data/stars.py +45 -24
  13. starplot/geod.py +0 -6
  14. starplot/geometry.py +181 -0
  15. starplot/horizon.py +302 -231
  16. starplot/map.py +100 -463
  17. starplot/mixins.py +75 -14
  18. starplot/models/__init__.py +1 -1
  19. starplot/models/base.py +18 -129
  20. starplot/models/constellation.py +55 -32
  21. starplot/models/dso.py +132 -67
  22. starplot/models/moon.py +57 -78
  23. starplot/models/planet.py +44 -69
  24. starplot/models/star.py +91 -60
  25. starplot/models/sun.py +32 -53
  26. starplot/optic.py +21 -18
  27. starplot/plotters/__init__.py +2 -0
  28. starplot/plotters/constellations.py +342 -0
  29. starplot/plotters/dsos.py +49 -68
  30. starplot/plotters/experimental.py +171 -0
  31. starplot/plotters/milkyway.py +39 -0
  32. starplot/plotters/stars.py +126 -122
  33. starplot/profile.py +16 -0
  34. starplot/settings.py +26 -0
  35. starplot/styles/__init__.py +2 -0
  36. starplot/styles/base.py +56 -34
  37. starplot/styles/ext/antique.yml +11 -9
  38. starplot/styles/ext/blue_dark.yml +8 -10
  39. starplot/styles/ext/blue_gold.yml +135 -0
  40. starplot/styles/ext/blue_light.yml +14 -12
  41. starplot/styles/ext/blue_medium.yml +23 -20
  42. starplot/styles/ext/cb_wong.yml +9 -7
  43. starplot/styles/ext/grayscale.yml +4 -3
  44. starplot/styles/ext/grayscale_dark.yml +7 -5
  45. starplot/styles/ext/map.yml +9 -6
  46. starplot/styles/ext/nord.yml +7 -7
  47. starplot/styles/ext/optic.yml +1 -1
  48. starplot/styles/extensions.py +1 -0
  49. starplot/utils.py +19 -0
  50. starplot/warnings.py +21 -0
  51. {starplot-0.13.0.dist-info → starplot-0.15.0.dist-info}/METADATA +19 -18
  52. starplot-0.15.0.dist-info/RECORD +97 -0
  53. starplot-0.15.0.dist-info/entry_points.txt +3 -0
  54. starplot/data/bayer.py +0 -3499
  55. starplot/data/flamsteed.py +0 -2682
  56. starplot/data/library/constellation_borders_inv.gpkg +0 -0
  57. starplot/data/library/constellation_lines_hips.json +0 -709
  58. starplot/data/library/constellation_lines_inv.gpkg +0 -0
  59. starplot/data/library/constellations.gpkg +0 -0
  60. starplot/data/library/constellations_hip.fab +0 -88
  61. starplot/data/library/milkyway.gpkg +0 -0
  62. starplot/data/library/milkyway_inv.gpkg +0 -0
  63. starplot/data/library/ongc.gpkg.zip +0 -0
  64. starplot/data/library/stars.bigsky.mag11.parquet +0 -0
  65. starplot/data/library/stars.hipparcos.parquet +0 -0
  66. starplot/data/messier.py +0 -111
  67. starplot/data/prep/__init__.py +0 -0
  68. starplot/data/prep/constellations.py +0 -108
  69. starplot/data/prep/dsos.py +0 -299
  70. starplot/data/prep/utils.py +0 -16
  71. starplot/models/geometry.py +0 -44
  72. starplot-0.13.0.dist-info/RECORD +0 -101
  73. {starplot-0.13.0.dist-info → starplot-0.15.0.dist-info}/LICENSE +0 -0
  74. {starplot-0.13.0.dist-info → starplot-0.15.0.dist-info}/WHEEL +0 -0
starplot/horizon.py CHANGED
@@ -1,69 +1,95 @@
1
+ import math
2
+
1
3
  from datetime import datetime
2
- from typing import Callable, Mapping
4
+ from functools import cache
3
5
 
4
6
  import pandas as pd
7
+ import geopandas as gpd
5
8
 
6
9
  from cartopy import crs as ccrs
7
- from matplotlib import pyplot as plt, patches, path
10
+ from matplotlib import pyplot as plt, patches
11
+ from matplotlib.ticker import FixedLocator
8
12
  from skyfield.api import wgs84, Star as SkyfieldStar
9
-
10
- from starplot import callables
13
+ from shapely import Point
14
+ from starplot.coordinates import CoordinateSystem
11
15
  from starplot.base import BasePlot, DPI
12
- from starplot.data.stars import StarCatalog, STAR_NAMES
13
16
  from starplot.mixins import ExtentMaskMixin
14
- from starplot.models import Star
15
- from starplot.plotters import StarPlotterMixin, DsoPlotterMixin
17
+ from starplot.plotters import (
18
+ ConstellationPlotterMixin,
19
+ StarPlotterMixin,
20
+ DsoPlotterMixin,
21
+ MilkyWayPlotterMixin,
22
+ )
16
23
  from starplot.styles import (
17
24
  PlotStyle,
18
- ObjectStyle,
19
25
  extensions,
20
26
  use_style,
21
- ZOrderEnum,
27
+ PathStyle,
22
28
  )
23
29
 
24
30
  pd.options.mode.chained_assignment = None # default='warn'
25
31
 
26
- DEFAULT_OPTIC_STYLE = PlotStyle().extend(extensions.OPTIC)
27
-
28
-
29
- class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
32
+ DEFAULT_HORIZON_STYLE = PlotStyle().extend(extensions.MAP)
33
+
34
+ DEFAULT_HORIZON_LABELS = {
35
+ 0: "N",
36
+ 45: "NE",
37
+ 90: "E",
38
+ 135: "SE",
39
+ 180: "S",
40
+ 225: "SW",
41
+ 270: "W",
42
+ 315: "NW",
43
+ }
44
+
45
+
46
+ class HorizonPlot(
47
+ BasePlot,
48
+ ExtentMaskMixin,
49
+ ConstellationPlotterMixin,
50
+ StarPlotterMixin,
51
+ DsoPlotterMixin,
52
+ MilkyWayPlotterMixin,
53
+ ):
30
54
  """Creates a new horizon plot.
31
55
 
32
56
  Args:
33
- optic: Optic instance that defines optical parameters
34
- ra: Right ascension of target center, in hours (0...24)
35
- dec: Declination of target center, in degrees (-90...90)
36
57
  lat: Latitude of observer's location
37
58
  lon: Longitude of observer's location
59
+ altitude: Tuple of altitude range to plot (min, max)
60
+ azimuth: Tuple of azimuth range to plot (min, max)
38
61
  dt: Date/time of observation (*must be timezone-aware*). Default = current UTC time.
39
62
  ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
40
63
  style: Styling for the plot (colors, sizes, fonts, etc)
41
64
  resolution: Size (in pixels) of largest dimension of the map
42
65
  hide_colliding_labels: If True, then labels will not be plotted if they collide with another existing label
43
- raise_on_below_horizon: If True, then a ValueError will be raised if the target is below the horizon at the observing time/location
44
66
  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.
45
67
  autoscale: If True, then the scale will be automatically set based on resolution
68
+ suppress_warnings: If True (the default), then all warnings will be suppressed
46
69
 
47
70
  Returns:
48
- OpticPlot: A new instance of an OpticPlot
71
+ HorizonPlot: A new instance of an HorizonPlot
49
72
 
50
73
  """
51
74
 
75
+ _coordinate_system = CoordinateSystem.AZ_ALT
76
+
52
77
  FIELD_OF_VIEW_MAX = 9.0
53
78
 
54
79
  def __init__(
55
80
  self,
56
81
  lat: float,
57
82
  lon: float,
58
- altitude: tuple[float, float] = (0, 60),
59
- azimuth: tuple[float, float] = (0, 90),
83
+ altitude: tuple[float, float],
84
+ azimuth: tuple[float, float],
60
85
  dt: datetime = None,
61
86
  ephemeris: str = "de421_2001.bsp",
62
- style: PlotStyle = DEFAULT_OPTIC_STYLE,
63
- resolution: int = 2048,
87
+ style: PlotStyle = DEFAULT_HORIZON_STYLE,
88
+ resolution: int = 4096,
64
89
  hide_colliding_labels: bool = True,
65
90
  scale: float = 1.0,
66
91
  autoscale: bool = False,
92
+ suppress_warnings: bool = True,
67
93
  *args,
68
94
  **kwargs,
69
95
  ) -> "HorizonPlot":
@@ -75,16 +101,31 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
75
101
  hide_colliding_labels,
76
102
  scale=scale,
77
103
  autoscale=autoscale,
104
+ suppress_warnings=suppress_warnings,
78
105
  *args,
79
106
  **kwargs,
80
107
  )
108
+
109
+ if azimuth[0] >= azimuth[1]:
110
+ raise ValueError("Azimuth min must be less than max")
111
+ if azimuth[1] - azimuth[0] > 180:
112
+ raise ValueError("Azimuth range cannot be greater than 180 degrees")
113
+
114
+ if altitude[0] >= altitude[1]:
115
+ raise ValueError("Altitude min must be less than max")
116
+ if altitude[1] - altitude[0] > 90:
117
+ raise ValueError("Altitude range cannot be greater than 90 degrees")
118
+
81
119
  self.logger.debug("Creating HorizonPlot...")
82
120
  self.alt = altitude
83
121
  self.az = azimuth
122
+ self.center_alt = sum(altitude) / 2
84
123
  self.center_az = sum(azimuth) / 2
85
124
  self.lat = lat
86
125
  self.lon = lon
87
126
 
127
+ self._geodetic = ccrs.Geodetic()
128
+ self._plate_carree = ccrs.PlateCarree()
88
129
  self._crs = ccrs.CRS(
89
130
  proj4_params=[
90
131
  ("proj", "latlong"),
@@ -93,21 +134,43 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
93
134
  globe=ccrs.Globe(ellipse="sphere", flattening=0),
94
135
  )
95
136
 
96
- self._calc_position()
97
137
  self._init_plot()
98
- self._adjust_radec_minmax()
99
138
 
139
+ self.altaz_mask = self._extent_mask_altaz()
140
+ self.logger.debug(f"Extent = AZ ({self.az}) ALT ({self.alt})")
141
+
142
+ self._calc_position()
143
+
144
+ @cache
100
145
  def _prepare_coords(self, ra, dec) -> (float, float):
101
146
  """Converts RA/DEC to AZ/ALT"""
102
- point = SkyfieldStar(ra_hours=ra, dec_degrees=dec)
103
- position = self.observe(point)
104
- pos_apparent = position.apparent()
105
- pos_alt, pos_az, _ = pos_apparent.altaz()
147
+ if ra > 360:
148
+ ra -= 360
149
+ if ra < 0:
150
+ ra += 360
151
+ point = SkyfieldStar(ra_hours=ra / 15, dec_degrees=dec)
152
+ position = self.observe(point).apparent()
153
+ pos_alt, pos_az, _ = position.altaz()
106
154
  return pos_az.degrees, pos_alt.degrees
107
155
 
156
+ def _prepare_star_coords(self, df, limit_by_altaz=True):
157
+ stars_apparent = self.observe(SkyfieldStar.from_dataframe(df)).apparent()
158
+ nearby_stars_alt, nearby_stars_az, _ = stars_apparent.altaz()
159
+ df["x"], df["y"] = (
160
+ nearby_stars_az.degrees,
161
+ nearby_stars_alt.degrees,
162
+ )
163
+ if limit_by_altaz:
164
+ extent = self._extent_mask_altaz()
165
+ df["_geometry_az_alt"] = gpd.points_from_xy(df.x, df.y)
166
+ df = df[df["_geometry_az_alt"].intersects(extent)]
167
+
168
+ return df
169
+
108
170
  def _plot_kwargs(self) -> dict:
109
171
  return dict(transform=self._crs)
110
172
 
173
+ @cache
111
174
  def in_bounds(self, ra, dec) -> bool:
112
175
  """Determine if a coordinate is within the bounds of the plot.
113
176
 
@@ -119,12 +182,7 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
119
182
  True if the coordinate is in bounds, otherwise False
120
183
  """
121
184
  az, alt = self._prepare_coords(ra, dec)
122
- return (
123
- az < self.az[1]
124
- and az > self.az[0]
125
- and alt < self.alt[1]
126
- and alt > self.alt[0]
127
- )
185
+ return self.in_bounds_altaz(alt, az)
128
186
 
129
187
  def in_bounds_altaz(self, alt, az, scale: float = 1) -> bool:
130
188
  """Determine if a coordinate is within the bounds of the plot.
@@ -136,224 +194,228 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
136
194
  Returns:
137
195
  True if the coordinate is in bounds, otherwise False
138
196
  """
139
- # x, y = self._proj.transform_point(az, alt, self._crs)
140
- return (
141
- az < self.az[1]
142
- and az > self.az[0]
143
- and alt < self.alt[1]
144
- and alt > self.alt[0]
145
- )
197
+ return self.altaz_mask.contains(Point(az, alt))
198
+
199
+ def _in_bounds_xy(self, x: float, y: float) -> bool:
200
+ return self.in_bounds_altaz(y, x) # alt = y, az = x
146
201
 
147
202
  def _polygon(self, points, style, **kwargs):
148
203
  super()._polygon(points, style, transform=self._crs, **kwargs)
149
204
 
150
205
  def _calc_position(self):
151
206
  earth = self.ephemeris["earth"]
152
-
153
207
  self.location = earth + wgs84.latlon(self.lat, self.lon)
154
208
  self.observe = self.location.at(self.timescale).observe
155
209
 
156
- # get radec at center horizon
157
- center = self.location.at(self.timescale).from_altaz(
158
- alt_degrees=0, az_degrees=self.center_az
159
- )
160
- print(self.center_az)
161
- print(center.radec())
162
- locations = [
163
- self.location.at(self.timescale).from_altaz(
164
- alt_degrees=self.alt[0], az_degrees=self.az[0]
165
- ), # lower left
166
- self.location.at(self.timescale).from_altaz(
167
- alt_degrees=self.alt[0], az_degrees=self.az[1]
168
- ), # lower right
169
- self.location.at(self.timescale).from_altaz(
170
- alt_degrees=self.alt[1], az_degrees=self.center_az
171
- ), # top center
172
- # self.location.at(self.timescale).from_altaz(alt_degrees=self.alt[1], az_degrees=self.az[0]), # upper left
173
- # self.location.at(self.timescale).from_altaz(alt_degrees=self.alt[1], az_degrees=self.az[1]), # upper right
174
- ]
175
-
176
- self.ra_min = None
177
- self.ra_max = None
178
- self.dec_max = None
179
- self.dec_min = None
180
-
181
- for location in locations:
182
- ra, dec, _ = location.radec()
183
- ra = ra.hours
184
- dec = dec.degrees
185
- if self.ra_min is None or ra < self.ra_min:
186
- self.ra_min = ra
187
-
188
- if self.ra_max is None or ra > self.ra_max:
189
- self.ra_max = ra
190
-
191
- if self.dec_min is None or dec < self.dec_min:
192
- self.dec_min = dec
193
-
194
- if self.dec_max is None or dec > self.dec_max:
195
- self.dec_max = dec
196
-
197
- # self.star = SkyfieldStar(ra_hours=self.ra, dec_degrees=self.dec)
198
- # self.position = self.observe(self.star)
199
- # self.pos_apparent = self.position.apparent()
200
- # self.pos_alt, self.pos_az, _ = self.pos_apparent.altaz()
201
-
202
- # if self.pos_alt.degrees < 0 and self.raise_on_below_horizon:
203
- # raise ValueError("Target is below horizon at specified time/location.")
204
-
205
- def _adjust_radec_minmax(self):
206
- # self.ra_min = self.ra - self.optic.true_fov / 15 * 1.08
207
- # self.ra_max = self.ra + self.optic.true_fov / 15 * 1.08
208
- # self.dec_max = self.dec + self.optic.true_fov / 2 * 1.03
209
- # self.dec_min = self.dec - self.optic.true_fov / 2 * 1.03
210
-
211
- if self.dec_max > 70 or self.dec_min < -70:
212
- # naive method of getting all the stars near the poles
213
- self.ra_min = 0
214
- self.ra_max = 24
215
-
216
- # TODO : below are in ra/dec - need to convert to alt/az
217
- # adjust declination to match extent
218
- extent = self.ax.get_extent(crs=ccrs.PlateCarree())
219
- self.dec_min = extent[2]
220
- self.dec_max = extent[3]
221
-
222
- # adjust right ascension to match extent
223
- if self.ra_max < 24:
224
- ra_min = (-1 * extent[1]) / 15
225
- ra_max = (-1 * extent[0]) / 15
226
-
227
- if ra_min < 0 or ra_max < 0:
228
- ra_min += 24
229
- ra_max += 24
230
-
231
- self.ra_min = ra_min
232
- self.ra_max = ra_max
210
+ # locations = [
211
+ # self.location.at(self.timescale).from_altaz(
212
+ # alt_degrees=self.alt[0], az_degrees=self.az[0]
213
+ # ), # lower left
214
+ # self.location.at(self.timescale).from_altaz(
215
+ # alt_degrees=self.alt[0], az_degrees=self.az[1]
216
+ # ), # lower right
217
+ # self.location.at(self.timescale).from_altaz(
218
+ # alt_degrees=self.alt[1], az_degrees=self.center_az
219
+ # ), # top center
220
+ # self.location.at(self.timescale).from_altaz(
221
+ # alt_degrees=self.center_alt, az_degrees=self.center_az
222
+ # ), # center
223
+ # self.location.at(self.timescale).from_altaz(alt_degrees=self.alt[1], az_degrees=self.az[0]), # upper left
224
+ # self.location.at(self.timescale).from_altaz(alt_degrees=self.alt[1], az_degrees=self.az[1]), # upper right
225
+ # ]
226
+
227
+ # self.ra_min = None
228
+ # self.ra_max = None
229
+ # self.dec_max = None
230
+ # self.dec_min = None
231
+ # print(self.alt)
232
+ # print(self.az)
233
+ # for location in locations:
234
+ # ra, dec, _ = location.radec()
235
+ # ra = ra.hours
236
+ # dec = dec.degrees
237
+ # print(ra, dec)
238
+ # if self.ra_min is None or ra < self.ra_min:
239
+ # self.ra_min = ra
240
+
241
+ # if self.ra_max is None or ra > self.ra_max:
242
+ # self.ra_max = ra
243
+
244
+ # if self.dec_min is None or dec < self.dec_min:
245
+ # self.dec_min = dec
246
+
247
+ # if self.dec_max is None or dec > self.dec_max:
248
+ # self.dec_max = dec
249
+
250
+ # if self.dec_max > 70 or self.dec_min < -70:
251
+ # # naive method of getting all the stars near the poles
252
+ # self.ra_min = 0
253
+ # self.ra_max = 24
254
+ # else:
255
+ # self.ra_min = max(self.ra_min - 4, 0)
256
+ # self.ra_max = min(self.ra_max + 4, 24)
257
+
258
+ # self.dec_min -= 10
259
+ # self.dec_max += 10
260
+
261
+ self.ra_min = 0
262
+ self.ra_max = 360
263
+ self.dec_min = self.lat - 90
264
+ self.dec_max = self.lat + 90
233
265
 
234
266
  self.logger.debug(
235
267
  f"Extent = RA ({self.ra_min:.2f}, {self.ra_max:.2f}) DEC ({self.dec_min:.2f}, {self.dec_max:.2f})"
236
268
  )
237
269
 
238
- def _in_bounds_xy(self, x: float, y: float) -> bool:
239
- return self.in_bounds_altaz(y, x) # alt = y, az = x
270
+ def _adjust_altaz_minmax(self):
271
+ """deprecated"""
272
+ extent = list(self.ax.get_extent(crs=self._plate_carree))
273
+ self.alt = (extent[2], extent[3])
240
274
 
241
- def _prepare_star_coords(self, df):
242
- stars_apparent = self.observe(SkyfieldStar.from_dataframe(df)).apparent()
243
- nearby_stars_alt, nearby_stars_az, _ = stars_apparent.altaz()
244
- df["x"], df["y"] = (
245
- nearby_stars_az.degrees,
246
- nearby_stars_alt.degrees,
247
- )
248
- return df
275
+ if extent[0] < 0:
276
+ extent[0] += 180
277
+ if extent[1] < 0:
278
+ extent[1] += 180
249
279
 
250
- def _scatter_stars(self, ras, decs, sizes, alphas, colors, style=None, **kwargs):
251
- plotted = super()._scatter_stars(
252
- ras, decs, sizes, alphas, colors, style, **kwargs
253
- )
280
+ self.az = (extent[0], extent[1])
254
281
 
255
- if type(self._background_clip_path) == patches.Rectangle:
256
- # convert to generic path to handle possible rotation angle:
257
- clip_path = path.Path(self._background_clip_path.get_corners())
258
- plotted.set_clip_path(clip_path, transform=self.ax.transData)
259
- else:
260
- plotted.set_clip_path(self._background_clip_path)
282
+ self.logger.debug(f"Extent = AZ ({self.az}) ALT ({self.alt})")
261
283
 
262
- @use_style(ObjectStyle, "star")
263
- def stars(
284
+ @use_style(PathStyle, "horizon")
285
+ def horizon(
264
286
  self,
265
- mag: float = 6.0,
266
- catalog: StarCatalog = StarCatalog.HIPPARCOS,
267
- style: ObjectStyle = None,
268
- rasterize: bool = False,
269
- size_fn: Callable[[Star], float] = callables.size_by_magnitude,
270
- alpha_fn: Callable[[Star], float] = callables.alpha_by_magnitude,
271
- color_fn: Callable[[Star], str] = None,
272
- where: list = None,
273
- where_labels: list = None,
274
- labels: Mapping[int, str] = STAR_NAMES,
275
- legend_label: str = "Star",
276
- bayer_labels: bool = False,
277
- *args,
278
- **kwargs,
287
+ style: PathStyle = None,
288
+ labels: dict[int, str] = DEFAULT_HORIZON_LABELS,
289
+ show_degree_labels: bool = True,
290
+ degree_step: int = 15,
291
+ show_ticks: bool = True,
292
+ tick_step: int = 5,
279
293
  ):
280
294
  """
281
- Plots stars
295
+ Plots rectangle for horizon that shows cardinal directions and azimuth labels.
282
296
 
283
297
  Args:
284
- mag: Limiting magnitude of stars to plot
285
- catalog: The catalog of stars to use
286
- style: If `None`, then the plot's style for stars will be used
287
- rasterize: If True, then the stars will be rasterized when plotted, which can speed up exporting to SVG and reduce the file size but with a loss of image quality
288
- size_fn: Callable for calculating the marker size of each star. If `None`, then the marker style's size will be used.
289
- alpha_fn: Callable for calculating the alpha value (aka "opacity") of each star. If `None`, then the marker style's alpha will be used.
290
- color_fn: Callable for calculating the color of each star. If `None`, then the marker style's color will be used.
291
- where: A list of expressions that determine which stars to plot. See [Selecting Objects](/reference-selecting-objects/) for details.
292
- where_labels: A list of expressions that determine which stars are labeled on the plot. See [Selecting Objects](/reference-selecting-objects/) for details.
293
- labels: A dictionary that maps a star's HIP id to the label that'll be plotted for that star. If you want to hide name labels, then set this arg to `None`.
294
- legend_label: Label for stars in the legend. If `None`, then they will not be in the legend.
295
- bayer_labels: If True, then Bayer labels for stars will be plotted. Set this to False if you want to hide Bayer labels.
298
+ style: Style of the horizon path. If None, then the plot's style definition will be used.
299
+ labels: Dictionary that maps azimuth values (0...360) to their cardinal direction labels (e.g. "N"). Default is to label each 45deg direction (e.g. "N", "NE", "E", etc)
300
+ show_degree_labels: If True, then azimuth degree labels will be plotted on the horizon path
301
+ degree_step: Step size for degree labels
302
+ 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
303
+ tick_step: Step size for tick marks
296
304
  """
297
- # optic_star_multiplier = 0.57 * (self.FIELD_OF_VIEW_MAX / self.optic.true_fov)
298
-
299
- # def size_fn_mx(st: Star) -> float:
300
- # return size_fn(st) * optic_star_multiplier
301
-
302
- super().stars(
303
- mag=mag,
304
- catalog=catalog,
305
- style=style,
306
- rasterize=rasterize,
307
- size_fn=size_fn,
308
- alpha_fn=alpha_fn,
309
- color_fn=color_fn,
310
- where=where,
311
- where_labels=where_labels,
312
- labels=labels,
313
- legend_label=legend_label,
314
- bayer_labels=bayer_labels,
315
- *args,
316
- **kwargs,
305
+
306
+ if show_degree_labels or show_ticks:
307
+ patch_y = -0.11 * self.scale
308
+ else:
309
+ patch_y = -0.08 * self.scale
310
+
311
+ bottom = patches.Polygon(
312
+ [
313
+ (0, 0),
314
+ (1, 0),
315
+ (1, patch_y),
316
+ (0, patch_y),
317
+ (0, 0),
318
+ ],
319
+ color=style.line.color.as_hex(),
320
+ transform=self.ax.transAxes,
321
+ clip_on=False,
317
322
  )
323
+ self.ax.add_patch(bottom)
324
+
325
+ def az_to_ax(d):
326
+ return self._to_ax(d, self.alt[0])[0]
327
+
328
+ for az in range(int(self.az[0]), int(self.az[1]), 1):
329
+ az = int(az)
330
+
331
+ if az >= 360:
332
+ az -= 360
333
+
334
+ x = az_to_ax(az)
335
+
336
+ if x <= 0.03 or x >= 0.97 or math.isnan(x):
337
+ continue
338
+
339
+ if labels.get(az):
340
+ self.ax.annotate(
341
+ labels.get(az),
342
+ (x, patch_y + 0.027),
343
+ xycoords=self.ax.transAxes,
344
+ **style.label.matplot_kwargs(self.scale),
345
+ clip_on=True,
346
+ )
347
+
348
+ if show_degree_labels and az % degree_step == 0:
349
+ self.ax.annotate(
350
+ str(az) + "\u00b0",
351
+ (x, -0.011 * self.scale),
352
+ xycoords=self.ax.transAxes,
353
+ **self.style.gridlines.label.matplot_kwargs(self.scale),
354
+ clip_on=True,
355
+ )
356
+
357
+ elif show_ticks and az % tick_step == 0:
358
+ self.ax.annotate(
359
+ "|",
360
+ (x, -0.011 * self.scale),
361
+ xycoords=self.ax.transAxes,
362
+ **self.style.gridlines.label.matplot_kwargs(self.scale / 2),
363
+ clip_on=True,
364
+ )
365
+
366
+ if show_degree_labels or show_ticks:
367
+ self.ax.plot(
368
+ [0, 1],
369
+ [-0.04 * self.scale, -0.04 * self.scale],
370
+ lw=1,
371
+ color=style.label.font_color.as_hex(),
372
+ clip_on=False,
373
+ transform=self.ax.transAxes,
374
+ )
375
+
376
+ @use_style(PathStyle, "gridlines")
377
+ def gridlines(
378
+ self,
379
+ style: PathStyle = None,
380
+ az_locations: list[float] = None,
381
+ alt_locations: list[float] = None,
382
+ ):
383
+ """
384
+ Plots gridlines
318
385
 
319
- def _plot_border(self):
320
- # since we're using AzimuthalEquidistant projection, the center will always be (0, 0)
321
- x = 0
322
- y = 0
386
+ Args:
387
+ style: Styling of the gridlines. If None, then the plot's style (specified when creating the plot) will be used
388
+ az_locations: List of azimuth locations for the gridlines (in degrees, 0...360). Defaults to every 15 degrees
389
+ alt_locations: List of altitude locations for the gridlines (in degrees, -90...90). Defaults to every 10 degrees.
323
390
 
324
- # Background of Viewable Area
325
- self._background_clip_path = self.optic.patch(
326
- x,
327
- y,
328
- facecolor=self.style.background_color.as_hex(),
329
- linewidth=0,
330
- fill=True,
331
- zorder=ZOrderEnum.LAYER_1,
391
+ """
392
+ x_locations = az_locations or [x for x in range(0, 360, 15)]
393
+ x_locations = [x - 180 for x in x_locations]
394
+ y_locations = alt_locations or [d for d in range(-90, 90, 10)]
395
+
396
+ line_style_kwargs = style.line.matplot_kwargs()
397
+ gridlines = self.ax.gridlines(
398
+ draw_labels=False,
399
+ x_inline=False,
400
+ y_inline=False,
401
+ rotate_labels=False,
402
+ xpadding=12,
403
+ ypadding=12,
404
+ clip_on=True,
405
+ clip_path=self._background_clip_path,
406
+ gid="gridlines",
407
+ **line_style_kwargs,
332
408
  )
333
- self.ax.add_patch(self._background_clip_path)
409
+ gridlines.xlocator = FixedLocator(x_locations)
410
+ gridlines.ylocator = FixedLocator(y_locations)
334
411
 
335
- # Inner Border
336
- inner_border = self.optic.patch(
337
- x,
338
- y,
339
- linewidth=2 * self.scale,
340
- edgecolor=self.style.border_line_color.as_hex(),
341
- fill=False,
342
- zorder=ZOrderEnum.LAYER_5 + 100,
343
- )
344
- self.ax.add_patch(inner_border)
345
-
346
- # Outer border
347
- outer_border = self.optic.patch(
348
- x,
349
- y,
350
- padding=0.05,
351
- linewidth=20 * self.scale,
352
- edgecolor=self.style.border_bg_color.as_hex(),
353
- fill=False,
354
- zorder=ZOrderEnum.LAYER_5,
355
- )
356
- self.ax.add_patch(outer_border)
412
+ @cache
413
+ def _to_ax(self, az: float, alt: float) -> tuple[float, float]:
414
+ """Converts az/alt to axes coordinates"""
415
+ x, y = self._proj.transform_point(az, alt, self._crs)
416
+ data_to_axes = self.ax.transData + self.ax.transAxes.inverted()
417
+ x_axes, y_axes = data_to_axes.transform((x, y))
418
+ return x_axes, y_axes
357
419
 
358
420
  def _fit_to_ax(self) -> None:
359
421
  bbox = self.ax.get_window_extent().transformed(
@@ -362,6 +424,21 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
362
424
  width, height = bbox.width, bbox.height
363
425
  self.fig.set_size_inches(width, height)
364
426
 
427
+ def _plot_background_clip_path(self):
428
+ self._background_clip_path = patches.Rectangle(
429
+ (0, 0),
430
+ width=1,
431
+ height=1,
432
+ facecolor=self.style.background_color.as_hex(),
433
+ linewidth=0,
434
+ fill=True,
435
+ zorder=-3_000,
436
+ transform=self.ax.transAxes,
437
+ )
438
+
439
+ self.ax.add_patch(self._background_clip_path)
440
+ self._update_clip_path_polygon()
441
+
365
442
  def _init_plot(self):
366
443
  self._proj = ccrs.LambertAzimuthalEqualArea(
367
444
  central_longitude=sum(self.az) / 2,
@@ -381,18 +458,12 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
381
458
 
382
459
  bounds = [
383
460
  self.az[0],
384
- self.az[1] * 1.2,
461
+ self.az[1],
385
462
  self.alt[0],
386
463
  self.alt[1],
387
464
  ]
388
- print(bounds)
389
465
 
390
466
  self.ax.set_extent(bounds, crs=ccrs.PlateCarree())
391
- self.ax.gridlines()
392
467
 
393
- # self._plot_border()
394
468
  self._fit_to_ax()
395
-
396
- # self.ax.set_xlim(-1.06 * self.optic.xlim, 1.06 * self.optic.xlim)
397
- # self.ax.set_ylim(-1.06 * self.optic.ylim, 1.06 * self.optic.ylim)
398
- # self.optic.transform(self.ax)
469
+ self._plot_background_clip_path()