starplot 0.15.8__py2.py3-none-any.whl → 0.16.1__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 (43) hide show
  1. starplot/__init__.py +7 -2
  2. starplot/base.py +57 -60
  3. starplot/cli.py +3 -3
  4. starplot/config.py +56 -0
  5. starplot/data/__init__.py +5 -5
  6. starplot/data/bigsky.py +3 -3
  7. starplot/data/db.py +2 -2
  8. starplot/data/library/sky.db +0 -0
  9. starplot/geometry.py +48 -0
  10. starplot/horizon.py +194 -90
  11. starplot/map.py +71 -168
  12. starplot/mixins.py +0 -55
  13. starplot/models/dso.py +10 -2
  14. starplot/observer.py +71 -0
  15. starplot/optic.py +61 -26
  16. starplot/plotters/__init__.py +2 -0
  17. starplot/plotters/constellations.py +4 -6
  18. starplot/plotters/dsos.py +3 -2
  19. starplot/plotters/gradients.py +153 -0
  20. starplot/plotters/legend.py +247 -0
  21. starplot/plotters/milkyway.py +8 -5
  22. starplot/plotters/stars.py +5 -3
  23. starplot/projections.py +155 -55
  24. starplot/styles/base.py +98 -22
  25. starplot/styles/ext/antique.yml +0 -1
  26. starplot/styles/ext/blue_dark.yml +0 -1
  27. starplot/styles/ext/blue_gold.yml +60 -52
  28. starplot/styles/ext/blue_light.yml +0 -1
  29. starplot/styles/ext/blue_medium.yml +7 -7
  30. starplot/styles/ext/blue_night.yml +178 -0
  31. starplot/styles/ext/cb_wong.yml +0 -1
  32. starplot/styles/ext/gradient_presets.yml +158 -0
  33. starplot/styles/ext/grayscale.yml +0 -1
  34. starplot/styles/ext/grayscale_dark.yml +0 -1
  35. starplot/styles/ext/nord.yml +0 -1
  36. starplot/styles/extensions.py +90 -0
  37. starplot/zenith.py +174 -0
  38. {starplot-0.15.8.dist-info → starplot-0.16.1.dist-info}/METADATA +18 -11
  39. {starplot-0.15.8.dist-info → starplot-0.16.1.dist-info}/RECORD +42 -36
  40. starplot/settings.py +0 -26
  41. {starplot-0.15.8.dist-info → starplot-0.16.1.dist-info}/WHEEL +0 -0
  42. {starplot-0.15.8.dist-info → starplot-0.16.1.dist-info}/entry_points.txt +0 -0
  43. {starplot-0.15.8.dist-info → starplot-0.16.1.dist-info}/licenses/LICENSE +0 -0
starplot/horizon.py CHANGED
@@ -1,30 +1,33 @@
1
1
  import math
2
2
 
3
- from datetime import datetime
4
3
  from functools import cache
4
+ from typing import Callable
5
5
 
6
6
  import pandas as pd
7
7
  import geopandas as gpd
8
8
 
9
9
  from cartopy import crs as ccrs
10
10
  from matplotlib import pyplot as plt, patches
11
- from matplotlib.ticker import FixedLocator
11
+ from matplotlib.ticker import FixedLocator, FuncFormatter
12
12
  from skyfield.api import wgs84, Star as SkyfieldStar
13
- from shapely import Point
13
+ from shapely import Point, Polygon, MultiPolygon
14
14
  from starplot.coordinates import CoordinateSystem
15
15
  from starplot.base import BasePlot, DPI
16
16
  from starplot.mixins import ExtentMaskMixin
17
+ from starplot.observer import Observer
17
18
  from starplot.plotters import (
18
19
  ConstellationPlotterMixin,
19
20
  StarPlotterMixin,
20
21
  DsoPlotterMixin,
21
22
  MilkyWayPlotterMixin,
23
+ GradientBackgroundMixin,
22
24
  )
23
25
  from starplot.styles import (
24
26
  PlotStyle,
25
27
  extensions,
26
28
  use_style,
27
29
  PathStyle,
30
+ GradientDirection,
28
31
  )
29
32
 
30
33
  pd.options.mode.chained_assignment = None # default='warn'
@@ -50,6 +53,7 @@ class HorizonPlot(
50
53
  StarPlotterMixin,
51
54
  DsoPlotterMixin,
52
55
  MilkyWayPlotterMixin,
56
+ GradientBackgroundMixin,
53
57
  ):
54
58
  """Creates a new horizon plot.
55
59
 
@@ -73,16 +77,15 @@ class HorizonPlot(
73
77
  """
74
78
 
75
79
  _coordinate_system = CoordinateSystem.AZ_ALT
80
+ _gradient_direction = GradientDirection.LINEAR
76
81
 
77
82
  FIELD_OF_VIEW_MAX = 9.0
78
83
 
79
84
  def __init__(
80
85
  self,
81
- lat: float,
82
- lon: float,
83
86
  altitude: tuple[float, float],
84
87
  azimuth: tuple[float, float],
85
- dt: datetime = None,
88
+ observer: Observer = Observer(),
86
89
  ephemeris: str = "de421_2001.bsp",
87
90
  style: PlotStyle = DEFAULT_HORIZON_STYLE,
88
91
  resolution: int = 4096,
@@ -94,7 +97,7 @@ class HorizonPlot(
94
97
  **kwargs,
95
98
  ) -> "HorizonPlot":
96
99
  super().__init__(
97
- dt,
100
+ observer,
98
101
  ephemeris,
99
102
  style,
100
103
  resolution,
@@ -121,8 +124,6 @@ class HorizonPlot(
121
124
  self.az = azimuth
122
125
  self.center_alt = sum(altitude) / 2
123
126
  self.center_az = sum(azimuth) / 2
124
- self.lat = lat
125
- self.lon = lon
126
127
 
127
128
  self._geodetic = ccrs.Geodetic()
128
129
  self._plate_carree = ccrs.PlateCarree()
@@ -204,8 +205,8 @@ class HorizonPlot(
204
205
 
205
206
  def _calc_position(self):
206
207
  earth = self.ephemeris["earth"]
207
- self.location = earth + wgs84.latlon(self.lat, self.lon)
208
- self.observe = self.location.at(self.timescale).observe
208
+ self.location = earth + wgs84.latlon(self.observer.lat, self.observer.lon)
209
+ self.observe = self.location.at(self.observer.timescale).observe
209
210
 
210
211
  # locations = [
211
212
  # self.location.at(self.timescale).from_altaz(
@@ -260,36 +261,85 @@ class HorizonPlot(
260
261
 
261
262
  self.ra_min = 0
262
263
  self.ra_max = 360
263
- self.dec_min = self.lat - 90
264
- self.dec_max = self.lat + 90
264
+ self.dec_min = self.observer.lat - 90
265
+ self.dec_max = self.observer.lat + 90
265
266
 
266
267
  self.logger.debug(
267
268
  f"Extent = RA ({self.ra_min:.2f}, {self.ra_max:.2f}) DEC ({self.dec_min:.2f}, {self.dec_max:.2f})"
268
269
  )
269
270
 
270
- def _adjust_altaz_minmax(self):
271
- """deprecated"""
271
+ @cache
272
+ def _extent_mask_altaz(self):
273
+ """
274
+ Returns shapely geometry objects of the alt/az extent
275
+
276
+ If the extent crosses North cardinal direction, then a MultiPolygon will be returned
277
+ """
272
278
  extent = list(self.ax.get_extent(crs=self._plate_carree))
273
- self.alt = (extent[2], extent[3])
279
+ alt_min, alt_max = extent[2], extent[3]
280
+ az_min, az_max = extent[0], extent[1]
274
281
 
275
- if extent[0] < 0:
276
- extent[0] += 180
277
- if extent[1] < 0:
278
- extent[1] += 180
282
+ az_ul, _ = self._ax_to_azalt(0, 1)
283
+ az_ur, _ = self._ax_to_azalt(1, 1)
279
284
 
280
- self.az = (extent[0], extent[1])
285
+ if az_ul < 0:
286
+ az_ul += 360
281
287
 
282
- self.logger.debug(f"Extent = AZ ({self.az}) ALT ({self.alt})")
288
+ if az_ur < 0:
289
+ az_ur += 360
290
+
291
+ az_min = min(self.az[0], self.az[1], az_ul, az_ur)
292
+ az_max = max(self.az[0], self.az[1], az_ul, az_ur)
293
+
294
+ if az_min < 0:
295
+ az_min += 360
296
+ if az_max < 0:
297
+ az_max += 360
298
+
299
+ if az_min >= az_max:
300
+ az_max += 360
301
+
302
+ self.az = (az_min, az_max)
303
+ self.alt = (alt_min, alt_max)
304
+
305
+ if az_max <= 360:
306
+ coords = [
307
+ [az_min, alt_min],
308
+ [az_max, alt_min],
309
+ [az_max, alt_max],
310
+ [az_min, alt_max],
311
+ [az_min, alt_min],
312
+ ]
313
+ return Polygon(coords)
314
+
315
+ else:
316
+ coords_1 = [
317
+ [az_min, alt_min],
318
+ [360, alt_min],
319
+ [360, alt_max],
320
+ [az_min, alt_max],
321
+ [az_min, alt_min],
322
+ ]
323
+ coords_2 = [
324
+ [0, alt_min],
325
+ [az_max - 360, alt_min],
326
+ [az_max - 360, alt_max],
327
+ [0, alt_max],
328
+ [0, alt_min],
329
+ ]
330
+
331
+ return MultiPolygon(
332
+ [
333
+ Polygon(coords_1),
334
+ Polygon(coords_2),
335
+ ]
336
+ )
283
337
 
284
338
  @use_style(PathStyle, "horizon")
285
339
  def horizon(
286
340
  self,
287
341
  style: PathStyle = None,
288
342
  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,
293
343
  ):
294
344
  """
295
345
  Plots rectangle for horizon that shows cardinal directions and azimuth labels.
@@ -297,24 +347,15 @@ class HorizonPlot(
297
347
  Args:
298
348
  style: Style of the horizon path. If None, then the plot's style definition will be used.
299
349
  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
304
350
  """
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
-
351
+ patch_y = -0.11 * self.scale
311
352
  bottom = patches.Polygon(
312
353
  [
313
- (0, 0),
314
- (1, 0),
354
+ (0, -0.04 * self.scale),
355
+ (1, -0.04 * self.scale),
315
356
  (1, patch_y),
316
357
  (0, patch_y),
317
- (0, 0),
358
+ (0, -0.04 * self.scale),
318
359
  ],
319
360
  color=style.line.color.as_hex(),
320
361
  transform=self.ax.transAxes,
@@ -322,92 +363,137 @@ class HorizonPlot(
322
363
  )
323
364
  self.ax.add_patch(bottom)
324
365
 
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):
366
+ for az, label in labels.items():
329
367
  az = int(az)
330
-
331
- if az >= 360:
332
- az -= 360
333
-
334
- x = az_to_ax(az)
335
-
368
+ x, _ = self._to_ax(az, self.alt[0])
336
369
  if x <= 0.03 or x >= 0.97 or math.isnan(x):
337
370
  continue
338
371
 
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
+ self.ax.annotate(
373
+ label,
374
+ (x, patch_y + 0.027),
375
+ xycoords=self.ax.transAxes,
376
+ **style.label.matplot_kwargs(self.scale),
372
377
  clip_on=False,
373
- transform=self.ax.transAxes,
374
378
  )
375
379
 
376
380
  @use_style(PathStyle, "gridlines")
377
381
  def gridlines(
378
382
  self,
379
383
  style: PathStyle = None,
384
+ show_labels: list = ["left", "right", "bottom"],
380
385
  az_locations: list[float] = None,
381
386
  alt_locations: list[float] = None,
387
+ az_formatter_fn: Callable[[float], str] = None,
388
+ alt_formatter_fn: Callable[[float], str] = None,
389
+ divider_line: bool = True,
390
+ show_ticks: bool = True,
391
+ tick_step: int = 5,
382
392
  ):
383
393
  """
384
394
  Plots gridlines
385
395
 
386
396
  Args:
387
397
  style: Styling of the gridlines. If None, then the plot's style (specified when creating the plot) will be used
398
+ show_labels: List of locations where labels should be shown (options: "left", "right", "top", "bottom")
388
399
  az_locations: List of azimuth locations for the gridlines (in degrees, 0...360). Defaults to every 15 degrees
389
400
  alt_locations: List of altitude locations for the gridlines (in degrees, -90...90). Defaults to every 10 degrees.
390
-
401
+ az_formatter_fn: Callable for creating labels of azimuth gridlines
402
+ alt_formatter_fn: Callable for creating labels of altitude gridlines
403
+ divider_line: If True, then a divider line will be plotted below the azimuth labels on the bottom of the plot (this is helpful when also plotting the horizon)
404
+ 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
405
+ tick_step: Step size for tick marks
391
406
  """
407
+ az_formatter_fn_default = lambda az: f"{round(az)}\u00b0 " # noqa: E731
408
+ alt_formatter_fn_default = lambda alt: f"{round(alt)}\u00b0 " # noqa: E731
409
+
410
+ az_formatter_fn = az_formatter_fn or az_formatter_fn_default
411
+ alt_formatter_fn = alt_formatter_fn or alt_formatter_fn_default
412
+
413
+ def az_formatter(x, pos) -> str:
414
+ if x < 0:
415
+ x += 360
416
+ return az_formatter_fn(x)
417
+
418
+ def alt_formatter(x, pos) -> str:
419
+ return alt_formatter_fn(x)
420
+
392
421
  x_locations = az_locations or [x for x in range(0, 360, 15)]
393
422
  x_locations = [x - 180 for x in x_locations]
394
423
  y_locations = alt_locations or [d for d in range(-90, 90, 10)]
395
424
 
425
+ label_style_kwargs = style.label.matplot_kwargs()
426
+ label_style_kwargs.pop("va")
427
+ label_style_kwargs.pop("ha")
428
+
396
429
  line_style_kwargs = style.line.matplot_kwargs()
430
+
397
431
  gridlines = self.ax.gridlines(
398
- draw_labels=False,
432
+ draw_labels=show_labels,
399
433
  x_inline=False,
400
434
  y_inline=False,
401
435
  rotate_labels=False,
402
436
  xpadding=12,
403
437
  ypadding=12,
404
- clip_on=True,
405
- clip_path=self._background_clip_path,
406
438
  gid="gridlines",
439
+ xlocs=FixedLocator(x_locations),
440
+ xformatter=FuncFormatter(az_formatter),
441
+ xlabel_style=label_style_kwargs,
442
+ ylocs=FixedLocator(y_locations),
443
+ ylabel_style=label_style_kwargs,
444
+ yformatter=FuncFormatter(alt_formatter),
407
445
  **line_style_kwargs,
408
446
  )
409
- gridlines.xlocator = FixedLocator(x_locations)
410
- gridlines.ylocator = FixedLocator(y_locations)
447
+ gridlines.set_zorder(style.line.zorder)
448
+
449
+ if show_labels:
450
+ self._axis_labels = True
451
+
452
+ # gridlines.xlocator = FixedLocator(x_locations)
453
+ # gridlines.xformatter = FuncFormatter(az_formatter)
454
+ # gridlines.xlabel_style = label_style_kwargs
455
+
456
+ # gridlines.ylocator = FixedLocator(y_locations)
457
+ # gridlines.yformatter = FuncFormatter(alt_formatter)
458
+ # gridlines.ylabel_style = label_style_kwargs
459
+ # print(gridlines.label_artists)
460
+ # for label in gridlines.label_artists:
461
+ # label.set_zorder(style.label.zorder)
462
+
463
+ if divider_line:
464
+ self.ax.plot(
465
+ [0, 1],
466
+ [-0.04 * self.scale, -0.04 * self.scale],
467
+ lw=1,
468
+ color=style.label.font_color.as_hex(),
469
+ clip_on=False,
470
+ transform=self.ax.transAxes,
471
+ )
472
+
473
+ if not show_ticks or len(x_locations) < 2:
474
+ return
475
+
476
+ # sort x locations so we iterate in order
477
+ x_locations_sorted = sorted(x_locations)
478
+ for i, az in enumerate(x_locations_sorted[1:], start=1):
479
+ prev_az = x_locations_sorted[i - 1]
480
+
481
+ # start at az label location + tick step cause we only want ticks between labels
482
+ for az_tick in range(prev_az + tick_step, az, tick_step):
483
+ a = int(az_tick)
484
+ if a >= 360:
485
+ a -= 360
486
+ x, _ = self._to_ax(a, self.alt[0])
487
+
488
+ if x <= 0.03 or x >= 0.97 or math.isnan(x):
489
+ continue
490
+
491
+ self.ax.annotate(
492
+ "|",
493
+ (x, -0.011 * self.scale),
494
+ xycoords=self.ax.transAxes,
495
+ **self.style.gridlines.label.matplot_kwargs(self.scale / 2),
496
+ )
411
497
 
412
498
  @cache
413
499
  def _to_ax(self, az: float, alt: float) -> tuple[float, float]:
@@ -417,6 +503,13 @@ class HorizonPlot(
417
503
  x_axes, y_axes = data_to_axes.transform((x, y))
418
504
  return x_axes, y_axes
419
505
 
506
+ @cache
507
+ def _ax_to_azalt(self, x: float, y: float) -> tuple[float, float]:
508
+ trans = self.ax.transAxes + self.ax.transData.inverted()
509
+ x_projected, y_projected = trans.transform((x, y)) # axes to data
510
+ az, alt = self._crs.transform_point(x_projected, y_projected, self._proj)
511
+ return float(az), float(alt)
512
+
420
513
  def _fit_to_ax(self) -> None:
421
514
  bbox = self.ax.get_window_extent().transformed(
422
515
  self.fig.dpi_scale_trans.inverted()
@@ -425,16 +518,23 @@ class HorizonPlot(
425
518
  self.fig.set_size_inches(width, height)
426
519
 
427
520
  def _plot_background_clip_path(self):
521
+ if self.style.has_gradient_background():
522
+ background_color = "#ffffff00"
523
+ self._plot_gradient_background(self.style.background_color)
524
+ else:
525
+ background_color = self.style.background_color.as_hex()
526
+
428
527
  self._background_clip_path = patches.Rectangle(
429
528
  (0, 0),
430
529
  width=1,
431
530
  height=1,
432
- facecolor=self.style.background_color.as_hex(),
531
+ facecolor=background_color,
433
532
  linewidth=0,
434
533
  fill=True,
435
534
  zorder=-3_000,
436
535
  transform=self.ax.transAxes,
437
536
  )
537
+ self.ax.set_facecolor(background_color)
438
538
 
439
539
  self.ax.add_patch(self._background_clip_path)
440
540
  self._update_clip_path_polygon()
@@ -466,4 +566,8 @@ class HorizonPlot(
466
566
  self.ax.set_extent(bounds, crs=ccrs.PlateCarree())
467
567
 
468
568
  self._fit_to_ax()
569
+
570
+ # if self.gradient_preset:
571
+ # self.apply_gradient_background(self.gradient_preset)
572
+
469
573
  self._plot_background_clip_path()