starplot 0.10.2__py2.py3-none-any.whl → 0.11.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.
starplot/map.py CHANGED
@@ -13,6 +13,7 @@ from skyfield.api import Star as SkyfieldStar, wgs84
13
13
  import geopandas as gpd
14
14
  import numpy as np
15
15
 
16
+ from starplot import geod
16
17
  from starplot.base import BasePlot
17
18
  from starplot.data import DataFiles, constellations as condata, stars
18
19
  from starplot.data.constellations import CONSTELLATIONS_FULL_NAMES
@@ -20,6 +21,7 @@ from starplot.mixins import ExtentMaskMixin
20
21
  from starplot.plotters import StarPlotterMixin, DsoPlotterMixin
21
22
  from starplot.projections import Projection
22
23
  from starplot.styles import (
24
+ ObjectStyle,
23
25
  LabelStyle,
24
26
  LineStyle,
25
27
  PlotStyle,
@@ -119,8 +121,6 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
119
121
  "Zenith projection requires a global extent: ra_min=0, ra_max=24, dec_min=-90, dec_max=90"
120
122
  )
121
123
 
122
- self.stars_df = stars.load("hipparcos")
123
-
124
124
  self._geodetic = ccrs.Geodetic()
125
125
  self._plate_carree = ccrs.PlateCarree()
126
126
  self._crs = ccrs.CRS(
@@ -356,6 +356,7 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
356
356
  use_arrow=True,
357
357
  bbox=self._extent_mask(),
358
358
  )
359
+ stars_df = stars.load("hipparcos")
359
360
 
360
361
  if constellations_gdf.empty:
361
362
  return
@@ -372,8 +373,8 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
372
373
  hiplines = conline_hips[c.id]
373
374
 
374
375
  for s1_hip, s2_hip in hiplines:
375
- s1 = self.stars_df.loc[s1_hip]
376
- s2 = self.stars_df.loc[s2_hip]
376
+ s1 = stars_df.loc[s1_hip]
377
+ s2 = stars_df.loc[s2_hip]
377
378
 
378
379
  s1_ra = s1.ra_hours * 15
379
380
  s2_ra = s2.ra_hours * 15
@@ -438,32 +439,142 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
438
439
  **style_kwargs,
439
440
  )
440
441
 
441
- @use_style(PolygonStyle)
442
+ @use_style(ObjectStyle, "zenith")
443
+ def zenith(
444
+ self,
445
+ style: ObjectStyle = None,
446
+ label: str = None,
447
+ legend_label: str = "Zenith",
448
+ ):
449
+ """
450
+ Plots a marker for the zenith (requires `lat`, `lon`, and `dt` to be defined when creating the plot)
451
+
452
+ Args:
453
+ style: Style of the zenith marker. If None, then the plot's style definition will be used.
454
+ label: Label for the zenith
455
+ legend_label: Label in the legend
456
+ """
457
+ if self.lat is None or self.lon is None or self.dt is None:
458
+ raise ValueError("lat, lon, and dt are required for plotting the zenith")
459
+
460
+ geographic = wgs84.latlon(latitude_degrees=self.lat, longitude_degrees=self.lon)
461
+ observer = geographic.at(self.timescale)
462
+ zenith = observer.from_altaz(alt_degrees=90, az_degrees=0)
463
+ ra, dec, _ = zenith.radec()
464
+
465
+ self.marker(
466
+ ra.hours,
467
+ dec.degrees,
468
+ label,
469
+ style,
470
+ legend_label,
471
+ )
472
+
473
+ @use_style(PathStyle, "horizon")
442
474
  def horizon(
443
475
  self,
444
- style: PolygonStyle = PolygonStyle(
445
- fill_color=None,
446
- edge_color="red",
447
- line_style="dashed",
448
- edge_width=4,
449
- zorder=1000,
450
- ),
476
+ style: PathStyle = None,
477
+ labels: list = ["N", "E", "S", "W"],
451
478
  ):
452
479
  """
453
480
  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)
454
481
 
455
482
  Args:
456
- style: Style of the polygon
483
+ style: Style of the horizon path. If None, then the plot's style definition will be used.
484
+ labels: List of labels for cardinal directions. **NOTE: labels should be in the order: North, East, South, West.**
457
485
  """
458
486
  if self.lat is None or self.lon is None or self.dt is None:
459
487
  raise ValueError("lat, lon, and dt are required for plotting the horizon")
460
488
 
461
- self.circle(
462
- ((self.timescale.gmst + self.lon / 15.0) % 24, self.lat),
463
- 90,
464
- style,
489
+ geographic = wgs84.latlon(latitude_degrees=self.lat, longitude_degrees=self.lon)
490
+ observer = geographic.at(self.timescale)
491
+ zenith = observer.from_altaz(alt_degrees=90, az_degrees=0)
492
+ ra, dec, _ = zenith.radec()
493
+
494
+ points = geod.ellipse(
495
+ center=(ra.hours, dec.degrees),
496
+ height_degrees=180,
497
+ width_degrees=180,
498
+ num_pts=100,
499
+ )
500
+ x = []
501
+ y = []
502
+ verts = []
503
+
504
+ # TODO : handle map edges better
505
+
506
+ for ra, dec in points:
507
+ ra = ra / 15
508
+ x0, y0 = self._prepare_coords(ra, dec)
509
+ x.append(x0)
510
+ y.append(y0)
511
+ verts.append((x0, y0))
512
+
513
+ style_kwargs = {}
514
+ if self.projection == Projection.ZENITH:
515
+ """
516
+ For zenith projections, we plot the horizon as a patch because
517
+ plottting as a line results in extra pixels on bottom.
518
+
519
+ TODO : investigate why line is extra thick on bottom when plotting line
520
+ """
521
+ style_kwargs = style.line.matplot_kwargs(self._size_multiplier)
522
+ style_kwargs["clip_on"] = False
523
+ style_kwargs["edgecolor"] = style_kwargs.pop("color")
524
+
525
+ patch = patches.Polygon(
526
+ verts,
527
+ facecolor=None,
528
+ fill=False,
529
+ transform=self._crs,
530
+ **style_kwargs,
531
+ )
532
+ self.ax.add_patch(patch)
533
+
534
+ else:
535
+ style_kwargs["clip_on"] = True
536
+ style_kwargs["clip_path"] = self._background_clip_path
537
+ self.ax.plot(
538
+ x,
539
+ y,
540
+ dash_capstyle=style.line.dash_capstyle,
541
+ **style.line.matplot_kwargs(self._size_multiplier),
542
+ **style_kwargs,
543
+ **self._plot_kwargs(),
544
+ )
545
+
546
+ # self.circle(
547
+ # (ra.hours, dec.degrees),
548
+ # 90,
549
+ # style,
550
+ # num_pts=200,
551
+ # )
552
+
553
+ if not labels:
554
+ return
555
+
556
+ north = observer.from_altaz(alt_degrees=0, az_degrees=0)
557
+ east = observer.from_altaz(alt_degrees=0, az_degrees=90)
558
+ south = observer.from_altaz(alt_degrees=0, az_degrees=180)
559
+ west = observer.from_altaz(alt_degrees=0, az_degrees=270)
560
+
561
+ cardinal_directions = [north, east, south, west]
562
+
563
+ text_kwargs = dict(
564
+ **style.label.matplot_kwargs(self._size_multiplier),
565
+ hide_on_collision=False,
566
+ xytext=(style.label.offset_x, style.label.offset_y),
567
+ textcoords="offset pixels",
568
+ path_effects=[],
465
569
  )
466
570
 
571
+ if self.projection == Projection.ZENITH:
572
+ text_kwargs["clip_on"] = False
573
+
574
+ for i, position in enumerate(cardinal_directions):
575
+ ra, dec, _ = position.radec()
576
+ self._text(ra.hours, dec.degrees, labels[i], **text_kwargs)
577
+
467
578
  @use_style(PathStyle, "gridlines")
468
579
  def gridlines(
469
580
  self,
@@ -492,7 +603,7 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
492
603
  """
493
604
 
494
605
  ra_formatter_fn_default = lambda r: f"{math.floor(r)}h" # noqa: E731
495
- dec_formatter_fn_default = lambda d: f"{round(d)}\u00b0" # noqa: E731
606
+ dec_formatter_fn_default = lambda d: f"{round(d)}\u00b0 " # noqa: E731
496
607
 
497
608
  ra_formatter_fn = ra_formatter_fn or ra_formatter_fn_default
498
609
  dec_formatter_fn = dec_formatter_fn or dec_formatter_fn_default
@@ -573,7 +684,9 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
573
684
  layout="constrained",
574
685
  )
575
686
  bounds = self._latlon_bounds()
687
+ center_lat = (bounds[2] + bounds[3]) / 2
576
688
  center_lon = (bounds[0] + bounds[1]) / 2
689
+ self._center_lat = center_lat
577
690
  self._center_lon = center_lon
578
691
 
579
692
  if self.projection in [
@@ -584,6 +697,10 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
584
697
  # Calculate LST to shift RA DEC to be in line with current date and time
585
698
  lst = -(360.0 * self.timescale.gmst / 24.0 + self.lon) % 360.0
586
699
  self._proj = Projection.crs(self.projection, lon=lst, lat=self.lat)
700
+ elif self.projection == Projection.LAMBERT_AZ_EQ_AREA:
701
+ self._proj = Projection.crs(
702
+ self.projection, center_lat=center_lat, center_lon=center_lon
703
+ )
587
704
  else:
588
705
  self._proj = Projection.crs(self.projection, center_lon)
589
706
  self._proj.threshold = 1000
@@ -592,11 +709,11 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
592
709
  if self._is_global_extent():
593
710
  if self.projection == Projection.ZENITH:
594
711
  theta = np.linspace(0, 2 * np.pi, 100)
595
- center, radius = [0.5, 0.5], 0.51
712
+ center, radius = [0.5, 0.5], 0.45
596
713
  verts = np.vstack([np.sin(theta), np.cos(theta)]).T
597
714
  circle = path.Path(verts * radius + center)
598
715
  extent = self.ax.get_extent(crs=self._proj)
599
- self.ax.set_extent((p / 3.75 for p in extent), crs=self._proj)
716
+ self.ax.set_extent((p / 3.548 for p in extent), crs=self._proj)
600
717
  self.ax.set_boundary(circle, transform=self.ax.transAxes)
601
718
  else:
602
719
  # this cartopy function works better for setting global extents
@@ -609,28 +726,14 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
609
726
 
610
727
  self.logger.debug(f"Projection = {self.projection.value.upper()}")
611
728
 
612
- if self.projection == Projection.ZENITH:
613
- self._plot_border()
614
- else:
615
- # draw patch in axes coords, which are easier to work with
616
- # in cases like this cause they go from 0...1 in all plots
617
- self._background_clip_path = patches.Rectangle(
618
- (0, 0),
619
- width=1,
620
- height=1,
621
- facecolor=self.style.background_color.as_hex(),
622
- linewidth=0,
623
- fill=True,
624
- zorder=-2_000,
625
- transform=self.ax.transAxes,
626
- )
627
- self.ax.add_patch(self._background_clip_path)
729
+ self._plot_background_clip_path()
628
730
 
629
731
  self._fit_to_ax()
630
732
 
631
733
  @use_style(LabelStyle, "info_text")
632
734
  def info(self, style: LabelStyle = None):
633
- """Plots info text in the lower left corner, including date/time and lat/lon.
735
+ """
736
+ Plots info text in the lower left corner, including date/time and lat/lon.
634
737
 
635
738
  _Only available for ZENITH projections_
636
739
 
@@ -643,70 +746,100 @@ class MapPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
643
746
  dt_str = self.dt.strftime("%m/%d/%Y @ %H:%M:%S") + " " + self.dt.tzname()
644
747
  info = f"{str(self.lat)}, {str(self.lon)}\n{dt_str}"
645
748
  self.ax.text(
646
- 0.01,
647
- 0.01,
749
+ 0.05,
750
+ 0.05,
648
751
  info,
649
752
  transform=self.ax.transAxes,
650
753
  **style.matplot_kwargs(self._size_multiplier * 1.36),
651
754
  )
652
755
 
653
- def _plot_border(self):
654
- """Plots circle border for Zenith projections"""
655
- border_font_kwargs = dict(
656
- fontsize=self.style.border_font_size * self._size_multiplier * 2.26,
657
- weight=self.style.border_font_weight,
658
- color=self.style.border_font_color.as_hex(),
659
- transform=self.ax.transAxes,
660
- zorder=5200,
661
- )
662
- self.ax.text(0.5, 0.98, "N", **border_font_kwargs)
663
- self.ax.text(0.9752, 0.5, "W", **border_font_kwargs)
664
- self.ax.text(0.00455, 0.5, "E", **border_font_kwargs)
665
- self.ax.text(0.5, 0.00456, "S", **border_font_kwargs)
756
+ def _plot_background_clip_path(self):
757
+ if self.projection == Projection.ZENITH:
758
+ self._background_clip_path = patches.Circle(
759
+ (0.50, 0.50),
760
+ radius=0.45,
761
+ fill=True,
762
+ facecolor=self.style.background_color.as_hex(),
763
+ # edgecolor=self.style.border_line_color.as_hex(),
764
+ linewidth=0, # 4 * self._size_multiplier,
765
+ zorder=-2_000,
766
+ transform=self.ax.transAxes,
767
+ )
768
+ else:
769
+ # draw patch in axes coords, which are easier to work with
770
+ # in cases like this cause they go from 0...1 in all plots
771
+ self._background_clip_path = patches.Rectangle(
772
+ (0, 0),
773
+ width=1,
774
+ height=1,
775
+ facecolor=self.style.background_color.as_hex(),
776
+ linewidth=0,
777
+ fill=True,
778
+ zorder=-2_000,
779
+ transform=self.ax.transAxes,
780
+ )
666
781
 
667
- background_circle = patches.Circle(
668
- (0.5, 0.5),
669
- radius=0.475,
670
- fill=True,
671
- facecolor=self.style.background_color.as_hex(),
672
- edgecolor=self.style.border_line_color.as_hex(),
673
- linewidth=8 * self._size_multiplier,
674
- zorder=-1_000,
675
- transform=self.ax.transAxes,
676
- )
677
- self.ax.add_patch(background_circle)
782
+ self.ax.add_patch(self._background_clip_path)
678
783
 
679
- self._background_clip_path = background_circle
784
+ def border(self, cardinal_direction_labels: list = ["N", "E", "S", "W"]):
785
+ """
786
+ Plots a border around the map.
787
+
788
+ _Only available for ZENITH projections_
789
+
790
+ Args:
791
+ cardinal_direction_labels: List of labels for cardinal directions on zenith plots. Order matters, labels should be in the order: North, East, South, West.
792
+ """
793
+
794
+ if not self.projection == Projection.ZENITH:
795
+ raise NotImplementedError("borders only available for zenith projections")
796
+
797
+ if cardinal_direction_labels:
798
+ n, e, s, w = cardinal_direction_labels
799
+ border_font_kwargs = dict(
800
+ fontsize=self.style.border_font_size * self._size_multiplier * 2.26,
801
+ weight=self.style.border_font_weight,
802
+ color=self.style.border_font_color.as_hex(),
803
+ transform=self.ax.transAxes,
804
+ zorder=5000,
805
+ )
806
+ self.ax.text(0.5, 0.986, n, **border_font_kwargs)
807
+ self.ax.text(0.978, 0.5, w, **border_font_kwargs)
808
+ self.ax.text(-0.002, 0.5, e, **border_font_kwargs)
809
+ self.ax.text(0.5, -0.002, s, **border_font_kwargs)
680
810
 
681
811
  border_circle = patches.Circle(
682
812
  (0.5, 0.5),
683
- radius=0.5,
813
+ radius=0.495,
684
814
  fill=False,
685
815
  edgecolor=self.style.border_bg_color.as_hex(),
686
- linewidth=90 * self._size_multiplier,
687
- zorder=1_000,
816
+ linewidth=72 * self._size_multiplier,
817
+ zorder=3_000,
688
818
  transform=self.ax.transAxes,
819
+ clip_on=False,
689
820
  )
690
821
  self.ax.add_patch(border_circle)
691
822
 
692
- border_line_circle = patches.Circle(
823
+ inner_border_line_circle = patches.Circle(
693
824
  (0.5, 0.5),
694
- radius=0.51,
825
+ radius=0.473,
695
826
  fill=False,
696
827
  edgecolor=self.style.border_line_color.as_hex(),
697
- linewidth=8 * self._size_multiplier,
698
- zorder=1_200,
828
+ linewidth=4.2 * self._size_multiplier,
829
+ zorder=3_000,
699
830
  transform=self.ax.transAxes,
831
+ clip_on=False,
700
832
  )
701
- self.ax.add_patch(border_line_circle)
833
+ self.ax.add_patch(inner_border_line_circle)
702
834
 
703
- inner_border_line_circle = patches.Circle(
835
+ outer_border_line_circle = patches.Circle(
704
836
  (0.5, 0.5),
705
- radius=0.472,
837
+ radius=0.52,
706
838
  fill=False,
707
839
  edgecolor=self.style.border_line_color.as_hex(),
708
- linewidth=4 * self._size_multiplier,
709
- zorder=2_000,
840
+ linewidth=8.2 * self._size_multiplier,
841
+ zorder=8_000,
710
842
  transform=self.ax.transAxes,
843
+ clip_on=False,
711
844
  )
712
- self.ax.add_patch(inner_border_line_circle)
845
+ self.ax.add_patch(outer_border_line_circle)
@@ -1,3 +1,4 @@
1
+ from .constellation import Constellation # noqa: F401,F403
1
2
  from .dso import DSO # noqa: F401,F403
2
3
  from .star import Star # noqa: F401,F403
3
4
  from .planet import Planet # noqa: F401,F403
starplot/models/base.py CHANGED
@@ -1,7 +1,12 @@
1
+ from typing import Optional
1
2
  from abc import ABC, abstractmethod
2
3
 
4
+ from skyfield.api import position_of_radec, load_constellation_map
5
+
3
6
  from starplot.mixins import CreateMapMixin, CreateOpticMixin
4
7
 
8
+ constellation_at = load_constellation_map()
9
+
5
10
 
6
11
  class Expression:
7
12
  def __init__(self, func=None) -> None:
@@ -104,10 +109,22 @@ class SkyObject(CreateMapMixin, CreateOpticMixin, metaclass=Meta):
104
109
  dec: float
105
110
  """Declination, in degrees (-90...90)"""
106
111
 
112
+ constellation_id: Optional[str] = None
113
+ """Identifier of the constellation that contains this object. The ID is the three-letter (all lowercase) abbreviation from the International Astronomical Union (IAU)."""
114
+
107
115
  def __init__(self, ra: float, dec: float) -> None:
108
116
  self.ra = ra
109
117
  self.dec = dec
110
118
 
119
+ pos = position_of_radec(ra, dec)
120
+ self.constellation_id = constellation_at(pos).lower()
121
+
122
+ def constellation(self):
123
+ """Returns an instance of the [`Constellation`][starplot.models.Constellation] that contains this object"""
124
+ from starplot.models import Constellation
125
+
126
+ return Constellation.get(iau_id=self.constellation_id)
127
+
111
128
 
112
129
  class SkyObjectManager(ABC):
113
130
  @abstractmethod
@@ -0,0 +1,72 @@
1
+ from starplot.models.base import SkyObject, SkyObjectManager
2
+ from starplot.data import constellations
3
+
4
+
5
+ class ConstellationManager(SkyObjectManager):
6
+ @classmethod
7
+ def all(cls):
8
+ for iau_id in constellations.iterator():
9
+ name, ra, dec = constellations.get(iau_id)
10
+ yield Constellation(
11
+ ra=ra, dec=dec, iau_id=iau_id, name=name.replace("\n", " ")
12
+ )
13
+
14
+
15
+ class Constellation(SkyObject):
16
+ """
17
+ Constellation model.
18
+ """
19
+
20
+ _manager = ConstellationManager
21
+
22
+ iau_id: str = None
23
+ """International Astronomical Union (IAU) three-letter designation, all lowercase"""
24
+
25
+ name: str = None
26
+ """Name"""
27
+
28
+ def __init__(
29
+ self,
30
+ ra: float,
31
+ dec: float,
32
+ iau_id: str,
33
+ name: str = None,
34
+ ) -> None:
35
+ super().__init__(ra, dec)
36
+ self.iau_id = iau_id.lower()
37
+ self.name = name
38
+
39
+ def __repr__(self) -> str:
40
+ return f"Constellation(iau_id={self.iau_id}, name={self.name}, ra={self.ra}, dec={self.dec})"
41
+
42
+ @classmethod
43
+ def get(**kwargs) -> "Constellation":
44
+ """
45
+ Get a Constellation, by matching its attributes.
46
+
47
+ Example: `hercules = Constellation.get(name="Hercules")`
48
+
49
+ Args:
50
+ **kwargs: Attributes on the constellation you want to match
51
+
52
+ Raises: `ValueError` if more than one constellation is matched
53
+ """
54
+ pass
55
+
56
+ @classmethod
57
+ def find(where: list) -> list["Constellation"]:
58
+ """
59
+ Find Constellations
60
+
61
+ Args:
62
+ where: A list of expressions that determine which constellations to find. See [Selecting Objects](/reference-selecting-objects/) for details.
63
+
64
+ Returns:
65
+ List of Constellations that match all `where` expressions
66
+
67
+ """
68
+ pass
69
+
70
+ def constellation(self):
71
+ """Not applicable to Constellation model, raises `NotImplementedError`"""
72
+ raise NotImplementedError()
starplot/models/star.py CHANGED
@@ -40,6 +40,9 @@ class Star(SkyObject):
40
40
  hip: Optional[int] = None
41
41
  """Hipparcos Catalog ID, if available"""
42
42
 
43
+ tyc: Optional[str] = None
44
+ """Tycho ID, if available"""
45
+
43
46
  name: Optional[str] = None
44
47
  """Name, if available"""
45
48
 
@@ -51,12 +54,17 @@ class Star(SkyObject):
51
54
  bv: float = None,
52
55
  hip: int = None,
53
56
  name: str = None,
57
+ tyc: str = None,
54
58
  ) -> None:
55
59
  super().__init__(ra, dec)
56
60
  self.magnitude = magnitude
57
61
  self.bv = bv
58
62
  self.hip = hip
59
63
  self.name = name
64
+ self.tyc = tyc
65
+
66
+ def __repr__(self) -> str:
67
+ return f"Star(hip={self.hip}, tyc={self.tyc}, magnitude={self.magnitude}, ra={self.ra}, dec={self.dec})"
60
68
 
61
69
  @classmethod
62
70
  def get(**kwargs) -> "Star":
starplot/optic.py CHANGED
@@ -189,7 +189,7 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
189
189
  def stars(
190
190
  self,
191
191
  mag: float = 6.0,
192
- catalog: StarCatalog = StarCatalog.TYCHO_1,
192
+ catalog: StarCatalog = StarCatalog.BIG_SKY_MAG11,
193
193
  style: ObjectStyle = None,
194
194
  rasterize: bool = False,
195
195
  size_fn: Callable[[Star], float] = callables.size_by_magnitude_for_optic,
@@ -208,7 +208,7 @@ class OpticPlot(BasePlot, ExtentMaskMixin, StarPlotterMixin, DsoPlotterMixin):
208
208
 
209
209
  Args:
210
210
  mag: Limiting magnitude of stars to plot
211
- catalog: The catalog of stars to use: "hipparcos" or "tycho-1"
211
+ catalog: The catalog of stars to use
212
212
  style: If `None`, then the plot's style for stars will be used
213
213
  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
214
214
  size_fn: Callable for calculating the marker size of each star. If `None`, then the marker style's size will be used.
@@ -1,4 +1,5 @@
1
1
  from typing import Callable, Mapping
2
+ from functools import cache
2
3
 
3
4
  import numpy as np
4
5
 
@@ -12,6 +13,7 @@ from starplot.styles import ObjectStyle, LabelStyle, use_style
12
13
 
13
14
 
14
15
  class StarPlotterMixin:
16
+ @cache
15
17
  def _load_stars(self, catalog, limiting_magnitude=None):
16
18
  stardata = stars.load(catalog)
17
19
 
@@ -125,7 +127,7 @@ class StarPlotterMixin:
125
127
 
126
128
  Args:
127
129
  mag: Limiting magnitude of stars to plot. For more control of what stars to plot, use the `where` kwarg. **Note:** if you pass `mag` and `where` then `mag` will be ignored
128
- catalog: The catalog of stars to use: "hipparcos" or "tycho-1" -- Hipparcos is the default and has about 10x less stars than Tycho-1 but will also plot much faster
130
+ catalog: The catalog of stars to use: "hipparcos", "big-sky-mag11", or "big-sky" -- see [`StarCatalog`](/reference-data/#starplot.data.stars.StarCatalog) for details
129
131
  style: If `None`, then the plot's style for stars will be used
130
132
  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
131
133
  size_fn: Callable for calculating the marker size of each star. If `None`, then the marker style's size will be used.
@@ -140,9 +142,12 @@ class StarPlotterMixin:
140
142
  self.logger.debug("Plotting stars...")
141
143
 
142
144
  # fallback to style if callables are None
145
+ color_hex = (
146
+ style.marker.color.as_hex()
147
+ ) # calculate color hex once here to avoid repeated calls in color_fn()
143
148
  size_fn = size_fn or (lambda d: style.marker.size)
144
149
  alpha_fn = alpha_fn or (lambda d: style.marker.alpha)
145
- color_fn = color_fn or (lambda d: style.marker.color.as_hex())
150
+ color_fn = color_fn or (lambda d: color_hex)
146
151
 
147
152
  where = where or []
148
153
 
@@ -171,9 +176,22 @@ class StarPlotterMixin:
171
176
  for star in nearby_stars_df.itertuples():
172
177
  m = star.magnitude
173
178
  ra, dec = star.ra, star.dec
174
- hip_id = star.Index
175
179
 
176
- obj = Star(ra=ra / 15, dec=dec, magnitude=m, bv=star.bv)
180
+ if catalog == StarCatalog.HIPPARCOS:
181
+ hip_id = star.Index
182
+ tyc_id = None
183
+ else:
184
+ hip_id = star.hip
185
+ tyc_id = star.Index
186
+
187
+ obj = Star(
188
+ ra=ra / 15,
189
+ dec=dec,
190
+ magnitude=m,
191
+ bv=star.bv,
192
+ hip=hip_id,
193
+ tyc=tyc_id,
194
+ )
177
195
 
178
196
  if np.isfinite(hip_id):
179
197
  obj.hip = hip_id
@@ -205,7 +223,9 @@ class StarPlotterMixin:
205
223
  colors,
206
224
  style=style,
207
225
  zorder=style.marker.zorder,
208
- edgecolors=self.style.background_color.as_hex(),
226
+ edgecolors=style.marker.edge_color.as_hex()
227
+ if style.marker.edge_color
228
+ else "none",
209
229
  rasterized=rasterize,
210
230
  )
211
231
 
starplot/projections.py CHANGED
@@ -28,6 +28,12 @@ class Projection(str, Enum):
28
28
  **This is a _perspective_ projection, so it requires the following `kwargs` when creating the plot: `lat`, `lon`, and `dt`**. _The perspective of the globe will be based on these values._
29
29
  """
30
30
 
31
+ ROBINSON = "robinson"
32
+ """Good for showing the entire celestial sphere in one plot"""
33
+
34
+ LAMBERT_AZ_EQ_AREA = "lambert_az_eq_area"
35
+ """Lambert Azimuthal Equal-Area projection - accurately shows area, but distorts angles."""
36
+
31
37
  STEREOGRAPHIC = "stereographic"
32
38
  """
33
39
  Similar to the North/South Stereographic projection, but this version is location-dependent.
@@ -47,10 +53,12 @@ class Projection(str, Enum):
47
53
  projections = {
48
54
  Projection.STEREO_NORTH: ccrs.NorthPolarStereo,
49
55
  Projection.STEREO_SOUTH: ccrs.SouthPolarStereo,
56
+ Projection.LAMBERT_AZ_EQ_AREA: ccrs.LambertAzimuthalEqualArea,
50
57
  Projection.MERCATOR: ccrs.Mercator,
51
58
  Projection.MOLLWEIDE: ccrs.Mollweide,
52
59
  Projection.MILLER: ccrs.Miller,
53
60
  Projection.ORTHOGRAPHIC: ccrs.Orthographic,
61
+ Projection.ROBINSON: ccrs.Robinson,
54
62
  Projection.STEREOGRAPHIC: ccrs.Stereographic,
55
63
  Projection.ZENITH: ccrs.Stereographic,
56
64
  }
@@ -64,4 +72,7 @@ class Projection(str, Enum):
64
72
  central_longitude=kwargs["lon"], central_latitude=kwargs["lat"]
65
73
  )
66
74
  else:
75
+ if kwargs.get("center_lat") is not None:
76
+ kwargs["central_latitude"] = kwargs.pop("center_lat")
77
+
67
78
  return proj_class(center_lon, **kwargs)