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/__init__.py CHANGED
@@ -1,11 +1,12 @@
1
1
  """Star charts and maps of the sky"""
2
2
 
3
- __version__ = "0.15.8"
3
+ __version__ = "0.16.1"
4
4
 
5
5
  from .base import BasePlot # noqa: F401
6
- from .map import MapPlot, Projection # noqa: F401
6
+ from .map import MapPlot # noqa: F401
7
7
  from .horizon import HorizonPlot # noqa: F401
8
8
  from .optic import OpticPlot # noqa: F401
9
+ from .zenith import ZenithPlot # noqa: F401
9
10
  from .models import (
10
11
  DSO, # noqa: F401
11
12
  DsoType, # noqa: F401
@@ -17,4 +18,8 @@ from .models import (
17
18
  ObjectList, # noqa: F401
18
19
  )
19
20
  from .styles import * # noqa: F401 F403
21
+ from .observer import Observer # noqa: F401
22
+ from .projections import * # noqa: F401 F403
23
+ from .config import settings # noqa: F401
24
+
20
25
  from ibis import _ # noqa: F401 F403
starplot/base.py CHANGED
@@ -1,5 +1,4 @@
1
1
  from abc import ABC, abstractmethod
2
- from datetime import datetime
3
2
  from typing import Dict, Union, Optional
4
3
  from random import randrange
5
4
  import logging
@@ -8,27 +7,29 @@ import numpy as np
8
7
  import rtree
9
8
  from matplotlib import patches
10
9
  from matplotlib import pyplot as plt, patheffects
10
+ from matplotlib.axes import Axes
11
+ from matplotlib.figure import Figure
11
12
  from matplotlib.lines import Line2D
12
- from pytz import timezone
13
13
  from shapely import Polygon, Point
14
14
 
15
15
  from starplot.coordinates import CoordinateSystem
16
16
  from starplot import geod, models, warnings
17
+ from starplot.config import settings, SvgTextType
17
18
  from starplot.data import load, ecliptic
18
19
  from starplot.models.planet import PlanetName, PLANET_LABELS_DEFAULT
19
20
  from starplot.models.moon import MoonPhase
21
+ from starplot.observer import Observer
20
22
  from starplot.styles import (
21
23
  AnchorPointEnum,
22
24
  PlotStyle,
23
25
  MarkerStyle,
24
26
  ObjectStyle,
25
27
  LabelStyle,
26
- LegendLocationEnum,
27
- LegendStyle,
28
28
  LineStyle,
29
29
  MarkerSymbolEnum,
30
30
  PathStyle,
31
31
  PolygonStyle,
32
+ GradientDirection,
32
33
  fonts,
33
34
  )
34
35
  from starplot.styles.helpers import use_style
@@ -67,10 +68,28 @@ class BasePlot(ABC):
67
68
  _background_clip_path = None
68
69
  _clip_path_polygon: Polygon = None # clip path in display coordinates
69
70
  _coordinate_system = CoordinateSystem.RA_DEC
71
+ _gradient_direction: GradientDirection = GradientDirection.LINEAR
72
+
73
+ ax: Axes
74
+ """
75
+ The underlying [Matplotlib axes](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html#matplotlib.axes.Axes) that everything is plotted on.
76
+
77
+ **Important**: Most Starplot plotting functions also specify a transform based on the plot's projection when plotting things on the Matplotlib Axes instance, so use this property at your own risk!
78
+ """
79
+
80
+ fig: Figure
81
+ """
82
+ The underlying [Matplotlib figure](https://matplotlib.org/stable/api/_as_gen/matplotlib.figure.Figure.html#matplotlib.figure.Figure) that the axes is drawn on.
83
+ """
84
+
85
+ style: PlotStyle
86
+ """
87
+ The plot's style.
88
+ """
70
89
 
71
90
  def __init__(
72
91
  self,
73
- dt: datetime = None,
92
+ observer: Observer = Observer(),
74
93
  ephemeris: str = "de421_2001.bsp",
75
94
  style: PlotStyle = DEFAULT_STYLE,
76
95
  resolution: int = 4096,
@@ -81,6 +100,11 @@ class BasePlot(ABC):
81
100
  *args,
82
101
  **kwargs,
83
102
  ):
103
+ if settings.svg_text_type == SvgTextType.PATH:
104
+ plt.rcParams["svg.fonttype"] = "path"
105
+ else:
106
+ plt.rcParams["svg.fonttype"] = "none"
107
+
84
108
  px = 1 / DPI # plt.rcParams["figure.dpi"] # pixel in inches
85
109
  self.pixels_per_point = DPI / 72
86
110
 
@@ -97,7 +121,7 @@ class BasePlot(ABC):
97
121
  if suppress_warnings:
98
122
  warnings.suppress()
99
123
 
100
- self.dt = dt or timezone("UTC").localize(datetime.now())
124
+ self.observer = observer
101
125
  self._ephemeris_name = ephemeris
102
126
  self.ephemeris = load(ephemeris)
103
127
  self.earth = self.ephemeris["earth"]
@@ -121,7 +145,6 @@ class BasePlot(ABC):
121
145
  linewidth=self.style.text_border_width * self.scale,
122
146
  foreground=self.style.text_border_color.as_hex(),
123
147
  )
124
- self.timescale = load.timescale().from_datetime(self.dt)
125
148
 
126
149
  self._objects = models.ObjectList()
127
150
  self._labeled_stars = []
@@ -359,7 +382,10 @@ class BasePlot(ABC):
359
382
  return None
360
383
 
361
384
  x, y = self._prepare_coords(ra, dec)
362
- kwargs["path_effects"] = kwargs.get("path_effects", [self.text_border])
385
+
386
+ if settings.svg_text_type == SvgTextType.PATH:
387
+ kwargs["path_effects"] = kwargs.get("path_effects", [self.text_border])
388
+
363
389
  remove_on_constellation_collision = kwargs.pop(
364
390
  "remove_on_constellation_collision", True
365
391
  )
@@ -495,6 +521,14 @@ class BasePlot(ABC):
495
521
  elif label is not None:
496
522
  label.remove()
497
523
 
524
+ @property
525
+ def magnitude_range(self) -> tuple[float, float]:
526
+ """
527
+ Range of magnitude for all plotted stars, as a tuple (min, max)
528
+ """
529
+ mags = [s.magnitude for s in self.objects.stars]
530
+ return (min(mags), max(mags))
531
+
498
532
  @use_style(LabelStyle)
499
533
  def text(
500
534
  self,
@@ -510,7 +544,7 @@ class BasePlot(ABC):
510
544
 
511
545
  Args:
512
546
  text: Text to plot
513
- ra: Right ascension of text (0...24)
547
+ ra: Right ascension of text (0...360)
514
548
  dec: Declination of text (-90...90)
515
549
  style: Styling of the text
516
550
  hide_on_collision: If True, then the text will not be plotted if it collides with another label
@@ -574,51 +608,6 @@ class BasePlot(ABC):
574
608
  style_kwargs["pad"] = style.line_spacing
575
609
  self.ax.set_title(text, **style_kwargs)
576
610
 
577
- @use_style(LegendStyle, "legend")
578
- def legend(self, style: LegendStyle = None):
579
- """
580
- Plots the legend.
581
-
582
- If the legend is already plotted, then it'll be removed first and then plotted again. So, it's safe to call this function multiple times if you need to 'refresh' the legend.
583
-
584
- Args:
585
- style: Styling of the legend. If None, then the plot's style (specified when creating the plot) will be used
586
- """
587
- if self._legend is not None:
588
- self._legend.remove()
589
-
590
- if not self._legend_handles:
591
- return
592
-
593
- bbox_kwargs = {}
594
-
595
- if style.location == LegendLocationEnum.OUTSIDE_BOTTOM:
596
- style.location = "lower center"
597
- offset_y = -0.08
598
- if getattr(self, "_axis_labels", False):
599
- offset_y -= 0.05
600
- bbox_kwargs = dict(
601
- bbox_to_anchor=(0.5, offset_y),
602
- )
603
-
604
- elif style.location == LegendLocationEnum.OUTSIDE_TOP:
605
- style.location = "upper center"
606
- offset_y = 1.08
607
- if getattr(self, "_axis_labels", False):
608
- offset_y += 0.05
609
- bbox_kwargs = dict(
610
- bbox_to_anchor=(0.5, offset_y),
611
- )
612
-
613
- self._legend = self.ax.legend(
614
- handles=self._legend_handles.values(),
615
- **style.matplot_kwargs(self.scale),
616
- **bbox_kwargs,
617
- ).set_zorder(
618
- # zorder is not a valid kwarg to legend(), so we have to set it afterwards
619
- style.zorder
620
- )
621
-
622
611
  def close_fig(self) -> None:
623
612
  """Closes the underlying matplotlib figure."""
624
613
  if self.fig:
@@ -674,7 +663,7 @@ class BasePlot(ABC):
674
663
  # Plot marker
675
664
  x, y = self._prepare_coords(ra, dec)
676
665
  style_kwargs = style.marker.matplot_scatter_kwargs(self.scale)
677
- self.ax.scatter(
666
+ result = self.ax.scatter(
678
667
  x,
679
668
  y,
680
669
  **style_kwargs,
@@ -722,7 +711,7 @@ class BasePlot(ABC):
722
711
  )
723
712
 
724
713
  if legend_label is not None:
725
- self._add_legend_handle_marker(legend_label, style.marker)
714
+ self._legend_handles[legend_label] = result
726
715
 
727
716
  @use_style(ObjectStyle, "planets")
728
717
  def planets(
@@ -744,7 +733,9 @@ class BasePlot(ABC):
744
733
  legend_label: How to label the planets in the legend. If `None`, then the planets will not be added to the legend
745
734
  """
746
735
  labels = labels or {}
747
- planets = models.Planet.all(self.dt, self.lat, self.lon, self._ephemeris_name)
736
+ planets = models.Planet.all(
737
+ self.observer.dt, self.observer.lat, self.observer.lon, self._ephemeris_name
738
+ )
748
739
 
749
740
  for p in planets:
750
741
  label = labels.get(p.name)
@@ -798,7 +789,10 @@ class BasePlot(ABC):
798
789
  legend_label: How the sun will be labeled in the legend
799
790
  """
800
791
  s = models.Sun.get(
801
- dt=self.dt, lat=self.lat, lon=self.lon, ephemeris=self._ephemeris_name
792
+ dt=self.observer.dt,
793
+ lat=self.observer.lat,
794
+ lon=self.observer.lon,
795
+ ephemeris=self._ephemeris_name,
802
796
  )
803
797
  s.name = label or s.name
804
798
 
@@ -909,7 +903,7 @@ class BasePlot(ABC):
909
903
  if geometry is not None:
910
904
  points = list(zip(*geometry.exterior.coords.xy))
911
905
 
912
- self._polygon(points, style, gid=kwargs.get("gid") or "polygon")
906
+ self._polygon(points, style, gid=kwargs.get("gid") or "polygon", **kwargs)
913
907
 
914
908
  if legend_label is not None:
915
909
  self._add_legend_handle_marker(
@@ -1073,7 +1067,10 @@ class BasePlot(ABC):
1073
1067
  label: How the Moon will be labeled on the plot and legend
1074
1068
  """
1075
1069
  m = models.Moon.get(
1076
- dt=self.dt, lat=self.lat, lon=self.lon, ephemeris=self._ephemeris_name
1070
+ dt=self.observer.dt,
1071
+ lat=self.observer.lat,
1072
+ lon=self.observer.lon,
1073
+ ephemeris=self._ephemeris_name,
1077
1074
  )
1078
1075
  m.name = label or m.name
1079
1076
 
starplot/cli.py CHANGED
@@ -6,7 +6,7 @@ COMMANDS = ["setup"]
6
6
 
7
7
 
8
8
  def setup(options):
9
- from starplot import settings
9
+ from starplot.config import settings
10
10
  from starplot.data import db, bigsky
11
11
 
12
12
  print("Installing DuckDB spatial extension...")
@@ -16,11 +16,11 @@ def setup(options):
16
16
 
17
17
  fonts.load()
18
18
 
19
- print(f"Installed to: {settings.DUCKDB_EXTENSION_PATH}")
19
+ print(f"Installed to: {settings.duckdb_extension_path}")
20
20
 
21
21
  if "--install-big-sky" in options:
22
22
  bigsky.download_if_not_exists()
23
- print(f"Big Sky Catalog downloaded and installed to: {settings.DOWNLOAD_PATH}")
23
+ print(f"Big Sky Catalog downloaded and installed to: {settings.download_path}")
24
24
 
25
25
 
26
26
  def main():
starplot/config.py ADDED
@@ -0,0 +1,56 @@
1
+ from enum import Enum
2
+ from pathlib import Path
3
+
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
+
6
+
7
+ STARPLOT_PATH = Path(__file__).resolve().parent
8
+ """Path of starplot source"""
9
+
10
+ DATA_PATH = STARPLOT_PATH / "data" / "library"
11
+ """Path of starplot data"""
12
+
13
+
14
+ RAW_DATA_PATH = STARPLOT_PATH.parent.parent / "raw"
15
+ BUILD_PATH = STARPLOT_PATH.parent.parent / "build"
16
+
17
+
18
+ class SvgTextType(str, Enum):
19
+ PATH = "path"
20
+ ELEMENT = "element"
21
+
22
+
23
+ class Settings(BaseSettings):
24
+ model_config = SettingsConfigDict(env_prefix="STARPLOT_")
25
+ """Configuration for the Pydantic settings model. Do not change."""
26
+
27
+ download_path: Path = DATA_PATH / "downloads"
28
+ """
29
+ Path for downloaded data, including the Big Sky catalog, ephemeris files, etc.
30
+
31
+ Default = `<starplot_source_path>/data/library/downloads/`
32
+ """
33
+
34
+ duckdb_extension_path: Path = DATA_PATH / "duckdb-extensions"
35
+ """
36
+ Path for the DuckDB spatial extension, which is required for the data backend.
37
+
38
+ Default = `<starplot_source_path>/data/library/duckdb-extensions/`
39
+ """
40
+
41
+ svg_text_type: SvgTextType = SvgTextType.PATH
42
+ """
43
+ Method for rendering text in SVG exports:
44
+
45
+ - `"path"` (default) will render all text as paths. This will increase the filesize,
46
+ but allow all viewers to see the font correctly (even if they don't have the font
47
+ installed on their system).
48
+
49
+ - `"element"` will render all text as an [SVG `<text>` element](https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/text),
50
+ which means the text will be editable in graphic design applications but the text may render in a system default font if the original
51
+ font isn't available. **Important: when using the "element" method, text borders will be turned OFF.**
52
+
53
+ """
54
+
55
+
56
+ settings = Settings()
starplot/data/__init__.py CHANGED
@@ -1,13 +1,13 @@
1
1
  from skyfield.api import Loader
2
2
 
3
- from starplot import settings
3
+ from starplot.config import settings, DATA_PATH
4
4
 
5
- load = Loader(settings.DATA_PATH)
5
+ load = Loader(DATA_PATH)
6
6
 
7
7
 
8
8
  class DataFiles:
9
- BIG_SKY = settings.DOWNLOAD_PATH / "bigsky.0.4.0.stars.parquet"
9
+ BIG_SKY = settings.download_path / "bigsky.0.4.0.stars.parquet"
10
10
 
11
- BIG_SKY_MAG11 = settings.DATA_PATH / "bigsky.0.4.0.stars.mag11.parquet"
11
+ BIG_SKY_MAG11 = DATA_PATH / "bigsky.0.4.0.stars.mag11.parquet"
12
12
 
13
- DATABASE = settings.DATA_PATH / "sky.db"
13
+ DATABASE = DATA_PATH / "sky.db"
starplot/data/bigsky.py CHANGED
@@ -3,7 +3,7 @@ from pathlib import Path
3
3
 
4
4
  import pandas as pd
5
5
 
6
- from starplot import settings
6
+ from starplot.config import settings
7
7
  from starplot.data import DataFiles, utils
8
8
 
9
9
 
@@ -21,7 +21,7 @@ def get_url(version: str = BIG_SKY_VERSION, filename: str = BIG_SKY_FILENAME):
21
21
 
22
22
  def download(
23
23
  url: str = None,
24
- download_path: str = settings.DOWNLOAD_PATH,
24
+ download_path: str = settings.download_path,
25
25
  download_filename: str = BIG_SKY_FILENAME,
26
26
  build_file: str = DataFiles.BIG_SKY,
27
27
  ):
@@ -102,7 +102,7 @@ def exists(path) -> bool:
102
102
  def download_if_not_exists(
103
103
  filename: str = DataFiles.BIG_SKY,
104
104
  url: str = None,
105
- download_path: str = settings.DOWNLOAD_PATH,
105
+ download_path: str = settings.download_path,
106
106
  download_filename: str = BIG_SKY_FILENAME,
107
107
  build_file: str = DataFiles.BIG_SKY,
108
108
  ):
starplot/data/db.py CHANGED
@@ -2,7 +2,7 @@ from functools import cache
2
2
 
3
3
  import ibis
4
4
 
5
- from starplot import settings
5
+ from starplot.config import settings
6
6
  from starplot.data import DataFiles
7
7
 
8
8
 
@@ -12,6 +12,6 @@ def connect():
12
12
  DataFiles.DATABASE, read_only=True
13
13
  ) # , threads=2, memory_limit="1GB"
14
14
  connection.raw_sql(
15
- f"SET extension_directory = '{str(settings.DUCKDB_EXTENSION_PATH)}';"
15
+ f"SET extension_directory = '{str(settings.duckdb_extension_path)}';"
16
16
  )
17
17
  return connection
Binary file
starplot/geometry.py CHANGED
@@ -95,6 +95,54 @@ def unwrap_polygon_360(polygon: Polygon) -> Polygon:
95
95
  return polygon
96
96
 
97
97
 
98
+ def split_polygon_at_zero(polygon: Polygon) -> list[Polygon]:
99
+ """
100
+ Splits a polygon at the first point of Aries (RA=0)
101
+
102
+ Args:
103
+ polygon: Polygon that possibly needs splitting
104
+
105
+ Returns:
106
+ List of polygons
107
+ """
108
+ ra, dec = [p for p in polygon.exterior.coords.xy]
109
+
110
+ if min(ra) < 180 and max(ra) > 300:
111
+ new_ra = [r + 360 if r < 180 else r for r in ra]
112
+ new_polygon = Polygon(list(zip(new_ra, dec)))
113
+
114
+ polygon_1 = new_polygon.intersection(
115
+ Polygon(
116
+ [
117
+ [0, -90],
118
+ [360, -90],
119
+ [360, 90],
120
+ [0, 90],
121
+ [0, -90],
122
+ ]
123
+ )
124
+ )
125
+
126
+ polygon_2 = new_polygon.intersection(
127
+ Polygon(
128
+ [
129
+ [360, -90],
130
+ [720, -90],
131
+ [720, 90],
132
+ [360, 90],
133
+ [360, -90],
134
+ ]
135
+ )
136
+ )
137
+
138
+ p2_ra, p2_dec = [p for p in polygon_2.exterior.coords.xy]
139
+ p2_new_ra = [ra - 360 for ra in p2_ra]
140
+
141
+ return [polygon_1, Polygon(list(zip(p2_new_ra, p2_dec)))]
142
+
143
+ return [polygon]
144
+
145
+
98
146
  def random_point_in_polygon(
99
147
  polygon: Polygon, max_iterations: int = 100, seed: int = None
100
148
  ) -> Point: