starplot 0.13.0__py2.py3-none-any.whl → 0.14.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.

Potentially problematic release.


This version of starplot might be problematic. Click here for more details.

starplot/horizon.py CHANGED
@@ -1,69 +1,93 @@
1
1
  from datetime import datetime
2
- from typing import Callable, Mapping
2
+ from functools import cache
3
3
 
4
4
  import pandas as pd
5
+ import geopandas as gpd
5
6
 
6
7
  from cartopy import crs as ccrs
7
- from matplotlib import pyplot as plt, patches, path
8
+ from matplotlib import pyplot as plt, patches
9
+ from matplotlib.ticker import FixedLocator
8
10
  from skyfield.api import wgs84, Star as SkyfieldStar
9
11
 
10
- from starplot import callables
12
+ from starplot.coordinates import CoordinateSystem
11
13
  from starplot.base import BasePlot, DPI
12
- from starplot.data.stars import StarCatalog, STAR_NAMES
13
14
  from starplot.mixins import ExtentMaskMixin
14
- from starplot.models import Star
15
- from starplot.plotters import StarPlotterMixin, DsoPlotterMixin
15
+ from starplot.plotters import (
16
+ ConstellationPlotterMixin,
17
+ StarPlotterMixin,
18
+ DsoPlotterMixin,
19
+ MilkyWayPlotterMixin,
20
+ )
16
21
  from starplot.styles import (
17
22
  PlotStyle,
18
- ObjectStyle,
19
23
  extensions,
20
24
  use_style,
21
- ZOrderEnum,
25
+ PathStyle,
22
26
  )
23
27
 
24
28
  pd.options.mode.chained_assignment = None # default='warn'
25
29
 
26
- DEFAULT_OPTIC_STYLE = PlotStyle().extend(extensions.OPTIC)
27
-
28
-
29
- class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
30
+ DEFAULT_HORIZON_STYLE = PlotStyle().extend(extensions.MAP)
31
+
32
+ DEFAULT_HORIZON_LABELS = {
33
+ 0: "N",
34
+ 45: "NE",
35
+ 90: "E",
36
+ 135: "SE",
37
+ 180: "S",
38
+ 225: "SW",
39
+ 270: "W",
40
+ 315: "NW",
41
+ }
42
+
43
+
44
+ class HorizonPlot(
45
+ BasePlot,
46
+ ExtentMaskMixin,
47
+ ConstellationPlotterMixin,
48
+ StarPlotterMixin,
49
+ DsoPlotterMixin,
50
+ MilkyWayPlotterMixin,
51
+ ):
30
52
  """Creates a new horizon plot.
31
53
 
32
54
  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
55
  lat: Latitude of observer's location
37
56
  lon: Longitude of observer's location
57
+ altitude: Tuple of altitude range to plot (min, max)
58
+ azimuth: Tuple of azimuth range to plot (min, max)
38
59
  dt: Date/time of observation (*must be timezone-aware*). Default = current UTC time.
39
60
  ephemeris: Ephemeris to use for calculating planet positions (see [Skyfield's documentation](https://rhodesmill.org/skyfield/planets.html) for details)
40
61
  style: Styling for the plot (colors, sizes, fonts, etc)
41
62
  resolution: Size (in pixels) of largest dimension of the map
42
63
  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
64
  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
65
  autoscale: If True, then the scale will be automatically set based on resolution
66
+ suppress_warnings: If True (the default), then all warnings will be suppressed
46
67
 
47
68
  Returns:
48
- OpticPlot: A new instance of an OpticPlot
69
+ HorizonPlot: A new instance of an HorizonPlot
49
70
 
50
71
  """
51
72
 
73
+ _coordinate_system = CoordinateSystem.AZ_ALT
74
+
52
75
  FIELD_OF_VIEW_MAX = 9.0
53
76
 
54
77
  def __init__(
55
78
  self,
56
79
  lat: float,
57
80
  lon: float,
58
- altitude: tuple[float, float] = (0, 60),
59
- azimuth: tuple[float, float] = (0, 90),
81
+ altitude: tuple[float, float],
82
+ azimuth: tuple[float, float],
60
83
  dt: datetime = None,
61
84
  ephemeris: str = "de421_2001.bsp",
62
- style: PlotStyle = DEFAULT_OPTIC_STYLE,
63
- resolution: int = 2048,
85
+ style: PlotStyle = DEFAULT_HORIZON_STYLE,
86
+ resolution: int = 4096,
64
87
  hide_colliding_labels: bool = True,
65
88
  scale: float = 1.0,
66
89
  autoscale: bool = False,
90
+ suppress_warnings: bool = True,
67
91
  *args,
68
92
  **kwargs,
69
93
  ) -> "HorizonPlot":
@@ -75,16 +99,31 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
75
99
  hide_colliding_labels,
76
100
  scale=scale,
77
101
  autoscale=autoscale,
102
+ suppress_warnings=suppress_warnings,
78
103
  *args,
79
104
  **kwargs,
80
105
  )
106
+
107
+ if azimuth[0] >= azimuth[1]:
108
+ raise ValueError("Azimuth min must be less than max")
109
+ if azimuth[1] - azimuth[0] > 180:
110
+ raise ValueError("Azimuth range cannot be greater than 180 degrees")
111
+
112
+ if altitude[0] >= altitude[1]:
113
+ raise ValueError("Altitude min must be less than max")
114
+ if altitude[1] - altitude[0] > 90:
115
+ raise ValueError("Altitude range cannot be greater than 90 degrees")
116
+
81
117
  self.logger.debug("Creating HorizonPlot...")
82
118
  self.alt = altitude
83
119
  self.az = azimuth
120
+ self.center_alt = sum(altitude) / 2
84
121
  self.center_az = sum(azimuth) / 2
85
122
  self.lat = lat
86
123
  self.lon = lon
87
124
 
125
+ self._geodetic = ccrs.Geodetic()
126
+ self._plate_carree = ccrs.PlateCarree()
88
127
  self._crs = ccrs.CRS(
89
128
  proj4_params=[
90
129
  ("proj", "latlong"),
@@ -97,17 +136,27 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
97
136
  self._init_plot()
98
137
  self._adjust_radec_minmax()
99
138
 
139
+ @cache
100
140
  def _prepare_coords(self, ra, dec) -> (float, float):
101
141
  """Converts RA/DEC to AZ/ALT"""
102
142
  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()
143
+ position = self.observe(point).apparent()
144
+ pos_alt, pos_az, _ = position.altaz()
106
145
  return pos_az.degrees, pos_alt.degrees
107
146
 
147
+ def _prepare_star_coords(self, df):
148
+ stars_apparent = self.observe(SkyfieldStar.from_dataframe(df)).apparent()
149
+ nearby_stars_alt, nearby_stars_az, _ = stars_apparent.altaz()
150
+ df["x"], df["y"] = (
151
+ nearby_stars_az.degrees,
152
+ nearby_stars_alt.degrees,
153
+ )
154
+ return df
155
+
108
156
  def _plot_kwargs(self) -> dict:
109
157
  return dict(transform=self._crs)
110
158
 
159
+ @cache
111
160
  def in_bounds(self, ra, dec) -> bool:
112
161
  """Determine if a coordinate is within the bounds of the plot.
113
162
 
@@ -119,12 +168,7 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
119
168
  True if the coordinate is in bounds, otherwise False
120
169
  """
121
170
  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
- )
171
+ return self.in_bounds_altaz(alt, az)
128
172
 
129
173
  def in_bounds_altaz(self, alt, az, scale: float = 1) -> bool:
130
174
  """Determine if a coordinate is within the bounds of the plot.
@@ -136,7 +180,9 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
136
180
  Returns:
137
181
  True if the coordinate is in bounds, otherwise False
138
182
  """
139
- # x, y = self._proj.transform_point(az, alt, self._crs)
183
+ if self.az[0] > 360 or self.az[1] > 360 and az < 90:
184
+ az += 360
185
+
140
186
  return (
141
187
  az < self.az[1]
142
188
  and az > self.az[0]
@@ -153,12 +199,6 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
153
199
  self.location = earth + wgs84.latlon(self.lat, self.lon)
154
200
  self.observe = self.location.at(self.timescale).observe
155
201
 
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
202
  locations = [
163
203
  self.location.at(self.timescale).from_altaz(
164
204
  alt_degrees=self.alt[0], az_degrees=self.az[0]
@@ -169,6 +209,9 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
169
209
  self.location.at(self.timescale).from_altaz(
170
210
  alt_degrees=self.alt[1], az_degrees=self.center_az
171
211
  ), # top center
212
+ self.location.at(self.timescale).from_altaz(
213
+ alt_degrees=self.center_alt, az_degrees=self.center_az
214
+ ), # center
172
215
  # self.location.at(self.timescale).from_altaz(alt_degrees=self.alt[1], az_degrees=self.az[0]), # upper left
173
216
  # self.location.at(self.timescale).from_altaz(alt_degrees=self.alt[1], az_degrees=self.az[1]), # upper right
174
217
  ]
@@ -194,42 +237,19 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
194
237
  if self.dec_max is None or dec > self.dec_max:
195
238
  self.dec_max = dec
196
239
 
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
240
  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
241
  if self.dec_max > 70 or self.dec_min < -70:
212
242
  # naive method of getting all the stars near the poles
213
243
  self.ra_min = 0
214
244
  self.ra_max = 24
215
245
 
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
246
+ self.dec_min -= 20
247
+ self.dec_max += 20
248
+ self.ra_min -= 4
249
+ self.ra_max += 4
230
250
 
231
- self.ra_min = ra_min
232
- self.ra_max = ra_max
251
+ if self.ra_min < 0:
252
+ self.ra_min = 0
233
253
 
234
254
  self.logger.debug(
235
255
  f"Extent = RA ({self.ra_min:.2f}, {self.ra_max:.2f}) DEC ({self.dec_min:.2f}, {self.dec_max:.2f})"
@@ -238,122 +258,154 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
238
258
  def _in_bounds_xy(self, x: float, y: float) -> bool:
239
259
  return self.in_bounds_altaz(y, x) # alt = y, az = x
240
260
 
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,
261
+ def _read_geo_package(self, filename: str):
262
+ """Returns GeoDataFrame of a GeoPackage file"""
263
+
264
+ # if self.ra_min <= 0 and self.ra_max >= 24:
265
+ # lon_min = -180
266
+ # lon_max = 180
267
+ # else:
268
+ # lon_min = self.ra_max * 15 - 180 # ra_to_lon(24 - self.ra_max)
269
+ # lon_max = self.ra_min * 15 - 180 # ra_to_lon(24 - self.ra_min)
270
+
271
+ # extent = self._extent_mask()
272
+ # extent = (
273
+ # lon_min,
274
+ # self.dec_min,
275
+ # lon_max,
276
+ # self.dec_max,
277
+ # )
278
+
279
+ return gpd.read_file(
280
+ filename,
281
+ engine="pyogrio",
282
+ use_arrow=True,
283
+ # bbox=extent,
247
284
  )
248
- return df
249
285
 
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
- )
254
-
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)
261
-
262
- @use_style(ObjectStyle, "star")
263
- def stars(
286
+ @use_style(PathStyle, "horizon")
287
+ def horizon(
264
288
  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,
289
+ style: PathStyle = None,
290
+ labels: dict[int, str] = DEFAULT_HORIZON_LABELS,
291
+ show_degree_labels: bool = True,
292
+ degree_step: int = 15,
293
+ show_ticks: bool = True,
294
+ tick_step: int = 5,
279
295
  ):
280
296
  """
281
- Plots stars
297
+ Plots rectangle for horizon that shows cardinal directions and azimuth labels.
282
298
 
283
299
  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.
300
+ style: Style of the horizon path. If None, then the plot's style definition will be used.
301
+ 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)
302
+ show_degree_labels: If True, then azimuth degree labels will be plotted on the horizon path
303
+ degree_step: Step size for degree labels
304
+ 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
305
+ tick_step: Step size for tick marks
296
306
  """
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,
307
+ bottom = patches.Polygon(
308
+ [
309
+ (0, 0),
310
+ (1, 0),
311
+ (1, -0.1 * self.scale),
312
+ (0, -0.1 * self.scale),
313
+ (0, 0),
314
+ ],
315
+ color=style.line.color.as_hex(),
316
+ transform=self.ax.transAxes,
317
+ clip_on=False,
318
+ )
319
+ self.ax.add_patch(bottom)
320
+
321
+ def az_to_ax(d):
322
+ return self._to_ax(d, self.alt[0])[0]
323
+
324
+ for az in range(self.az[0] + 2, self.az[1], 1):
325
+ az = int(az)
326
+
327
+ if az >= 360:
328
+ az -= 360
329
+
330
+ if labels.get(az):
331
+ self.ax.annotate(
332
+ labels.get(az),
333
+ (az_to_ax(az), -0.074 * self.scale),
334
+ xycoords=self.ax.transAxes,
335
+ **style.label.matplot_kwargs(self.scale),
336
+ clip_on=False,
337
+ )
338
+
339
+ if show_degree_labels and az % degree_step == 0:
340
+ self.ax.annotate(
341
+ str(az) + "\u00b0",
342
+ (az_to_ax(az), -0.011 * self.scale),
343
+ xycoords=self.ax.transAxes,
344
+ **self.style.gridlines.label.matplot_kwargs(self.scale),
345
+ clip_on=False,
346
+ )
347
+
348
+ elif show_ticks and az % tick_step == 0:
349
+ self.ax.annotate(
350
+ "|",
351
+ (az_to_ax(az), -0.011 * self.scale),
352
+ xycoords=self.ax.transAxes,
353
+ **self.style.gridlines.label.matplot_kwargs(self.scale / 2),
354
+ clip_on=False,
355
+ )
356
+
357
+ self.ax.plot(
358
+ [0, 1],
359
+ [-0.04 * self.scale, -0.04 * self.scale],
360
+ lw=1,
361
+ color=style.label.font_color.as_hex(),
362
+ clip_on=False,
363
+ transform=self.ax.transAxes,
317
364
  )
318
365
 
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
366
+ @use_style(PathStyle, "gridlines")
367
+ def gridlines(
368
+ self,
369
+ style: PathStyle = None,
370
+ az_locations: list[float] = None,
371
+ alt_locations: list[float] = None,
372
+ ):
373
+ """
374
+ Plots gridlines
323
375
 
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,
332
- )
333
- self.ax.add_patch(self._background_clip_path)
376
+ Args:
377
+ style: Styling of the gridlines. If None, then the plot's style (specified when creating the plot) will be used
378
+ az_locations: List of azimuth locations for the gridlines (in degrees, 0...360). Defaults to every 15 degrees
379
+ alt_locations: List of altitude locations for the gridlines (in degrees, -90...90). Defaults to every 10 degrees.
334
380
 
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,
381
+ """
382
+ x_locations = az_locations or [x for x in range(0, 360, 15)]
383
+ x_locations = [x - 180 for x in x_locations]
384
+ y_locations = alt_locations or [d for d in range(-90, 90, 10)]
385
+
386
+ line_style_kwargs = style.line.matplot_kwargs()
387
+ gridlines = self.ax.gridlines(
388
+ draw_labels=False,
389
+ x_inline=False,
390
+ y_inline=False,
391
+ rotate_labels=False,
392
+ xpadding=12,
393
+ ypadding=12,
394
+ clip_on=True,
395
+ clip_path=self._background_clip_path,
396
+ gid="gridlines",
397
+ **line_style_kwargs,
355
398
  )
356
- self.ax.add_patch(outer_border)
399
+ gridlines.xlocator = FixedLocator(x_locations)
400
+ gridlines.ylocator = FixedLocator(y_locations)
401
+
402
+ @cache
403
+ def _to_ax(self, az: float, alt: float) -> tuple[float, float]:
404
+ """Converts az/alt to axes coordinates"""
405
+ x, y = self._proj.transform_point(az, alt, self._crs)
406
+ data_to_axes = self.ax.transData + self.ax.transAxes.inverted()
407
+ x_axes, y_axes = data_to_axes.transform((x, y))
408
+ return x_axes, y_axes
357
409
 
358
410
  def _fit_to_ax(self) -> None:
359
411
  bbox = self.ax.get_window_extent().transformed(
@@ -362,6 +414,20 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
362
414
  width, height = bbox.width, bbox.height
363
415
  self.fig.set_size_inches(width, height)
364
416
 
417
+ def _plot_background_clip_path(self):
418
+ self._background_clip_path = patches.Rectangle(
419
+ (0, 0),
420
+ width=1,
421
+ height=1,
422
+ facecolor=self.style.background_color.as_hex(),
423
+ linewidth=0,
424
+ fill=True,
425
+ zorder=-3_000,
426
+ transform=self.ax.transAxes,
427
+ )
428
+
429
+ self.ax.add_patch(self._background_clip_path)
430
+
365
431
  def _init_plot(self):
366
432
  self._proj = ccrs.LambertAzimuthalEqualArea(
367
433
  central_longitude=sum(self.az) / 2,
@@ -381,18 +447,12 @@ class HorizonPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
381
447
 
382
448
  bounds = [
383
449
  self.az[0],
384
- self.az[1] * 1.2,
450
+ self.az[1],
385
451
  self.alt[0],
386
452
  self.alt[1],
387
453
  ]
388
- print(bounds)
389
454
 
390
455
  self.ax.set_extent(bounds, crs=ccrs.PlateCarree())
391
- self.ax.gridlines()
392
456
 
393
- # self._plot_border()
457
+ self._plot_background_clip_path()
394
458
  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)