starplot 0.20.5__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 (98) hide show
  1. starplot/__init__.py +59 -0
  2. starplot/callables.py +176 -0
  3. starplot/cli.py +29 -0
  4. starplot/config.py +80 -0
  5. starplot/coordinates.py +7 -0
  6. starplot/data/__init__.py +22 -0
  7. starplot/data/catalogs.py +356 -0
  8. starplot/data/constellations.py +71 -0
  9. starplot/data/db.py +33 -0
  10. starplot/data/dsos.py +85 -0
  11. starplot/data/ecliptic.py +201 -0
  12. starplot/data/library/constellation_names.parquet +0 -0
  13. starplot/data/library/dso_names.parquet +0 -0
  14. starplot/data/library/readme.md +1 -0
  15. starplot/data/library/star_designations.parquet +0 -0
  16. starplot/data/stars.py +79 -0
  17. starplot/data/translations.py +408 -0
  18. starplot/data/utils.py +35 -0
  19. starplot/geometry.py +319 -0
  20. starplot/mixins.py +360 -0
  21. starplot/models/__init__.py +21 -0
  22. starplot/models/base.py +77 -0
  23. starplot/models/comet.py +302 -0
  24. starplot/models/constellation.py +151 -0
  25. starplot/models/dso.py +317 -0
  26. starplot/models/milky_way.py +30 -0
  27. starplot/models/moon.py +130 -0
  28. starplot/models/objects.py +29 -0
  29. starplot/models/observer.py +125 -0
  30. starplot/models/optics.py +342 -0
  31. starplot/models/planet.py +137 -0
  32. starplot/models/satellite.py +138 -0
  33. starplot/models/star.py +253 -0
  34. starplot/models/sun.py +62 -0
  35. starplot/plots/__init__.py +7 -0
  36. starplot/plots/base.py +1023 -0
  37. starplot/plots/galaxy.py +372 -0
  38. starplot/plots/horizon.py +543 -0
  39. starplot/plots/map.py +515 -0
  40. starplot/plots/optic.py +469 -0
  41. starplot/plots/zenith.py +217 -0
  42. starplot/plotters/__init__.py +9 -0
  43. starplot/plotters/arrow.py +174 -0
  44. starplot/plotters/constellations.py +298 -0
  45. starplot/plotters/debug.py +21 -0
  46. starplot/plotters/dsos.py +294 -0
  47. starplot/plotters/experimental.py +722 -0
  48. starplot/plotters/gradients.py +153 -0
  49. starplot/plotters/legend.py +253 -0
  50. starplot/plotters/milkyway.py +51 -0
  51. starplot/plotters/stars.py +319 -0
  52. starplot/plotters/text.py +802 -0
  53. starplot/profile.py +16 -0
  54. starplot/projections.py +184 -0
  55. starplot/styles/__init__.py +6 -0
  56. starplot/styles/base.py +1344 -0
  57. starplot/styles/ext/antique.yml +175 -0
  58. starplot/styles/ext/blue_dark.yml +163 -0
  59. starplot/styles/ext/blue_gold.yml +147 -0
  60. starplot/styles/ext/blue_light.yml +123 -0
  61. starplot/styles/ext/blue_medium.yml +142 -0
  62. starplot/styles/ext/blue_night.yml +185 -0
  63. starplot/styles/ext/cb_wong.yml +124 -0
  64. starplot/styles/ext/color_print.yml +111 -0
  65. starplot/styles/ext/gradient_presets.yml +158 -0
  66. starplot/styles/ext/grayscale.yml +94 -0
  67. starplot/styles/ext/grayscale_dark.yml +136 -0
  68. starplot/styles/ext/map.yml +12 -0
  69. starplot/styles/ext/nord.yml +158 -0
  70. starplot/styles/ext/optic.yml +20 -0
  71. starplot/styles/ext/publication.yml +8 -0
  72. starplot/styles/extensions.py +129 -0
  73. starplot/styles/fonts-library/gfs-didot/DESCRIPTION.en_us.html +9 -0
  74. starplot/styles/fonts-library/gfs-didot/GFSDidot-Regular.ttf +0 -0
  75. starplot/styles/fonts-library/gfs-didot/METADATA.pb +16 -0
  76. starplot/styles/fonts-library/gfs-didot/OFL.txt +94 -0
  77. starplot/styles/fonts-library/inter/Inter-Bold.ttf +0 -0
  78. starplot/styles/fonts-library/inter/Inter-BoldItalic.ttf +0 -0
  79. starplot/styles/fonts-library/inter/Inter-ExtraBold.ttf +0 -0
  80. starplot/styles/fonts-library/inter/Inter-ExtraLight.ttf +0 -0
  81. starplot/styles/fonts-library/inter/Inter-ExtraLightItalic.ttf +0 -0
  82. starplot/styles/fonts-library/inter/Inter-Italic.ttf +0 -0
  83. starplot/styles/fonts-library/inter/Inter-Light.ttf +0 -0
  84. starplot/styles/fonts-library/inter/Inter-LightItalic.ttf +0 -0
  85. starplot/styles/fonts-library/inter/Inter-Regular.ttf +0 -0
  86. starplot/styles/fonts-library/inter/Inter-SemiBold.ttf +0 -0
  87. starplot/styles/fonts-library/inter/Inter-SemiBoldItalic.ttf +0 -0
  88. starplot/styles/fonts-library/inter/LICENSE.txt +92 -0
  89. starplot/styles/fonts.py +15 -0
  90. starplot/styles/helpers.py +93 -0
  91. starplot/styles/markers.py +308 -0
  92. starplot/utils.py +169 -0
  93. starplot/warnings.py +21 -0
  94. starplot-0.20.5.dist-info/METADATA +146 -0
  95. starplot-0.20.5.dist-info/RECORD +98 -0
  96. starplot-0.20.5.dist-info/WHEEL +4 -0
  97. starplot-0.20.5.dist-info/entry_points.txt +3 -0
  98. starplot-0.20.5.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1344 @@
1
+ import json
2
+
3
+ from enum import Enum
4
+ from pathlib import Path
5
+ from typing import Optional, Union
6
+
7
+ import yaml
8
+
9
+ from pydantic import BaseModel
10
+ from pydantic.color import Color
11
+ from pydantic.functional_serializers import PlainSerializer
12
+ from matplotlib import patheffects
13
+ from typing_extensions import Annotated
14
+
15
+ from starplot.models.dso import DsoType
16
+ from starplot.styles.helpers import merge_dict
17
+ from starplot.styles.markers import (
18
+ ellipse,
19
+ circle_cross,
20
+ circle_crosshair,
21
+ circle_line,
22
+ circle_dot,
23
+ circle_dotted_rings,
24
+ )
25
+
26
+
27
+ ColorStr = Annotated[
28
+ Color,
29
+ PlainSerializer(
30
+ lambda c: c.as_hex() if c and c != "none" else None,
31
+ return_type=str,
32
+ ),
33
+ ]
34
+
35
+
36
+ HERE = Path(__file__).resolve().parent
37
+
38
+ PI = 3.141592653589793
39
+ SQR_2 = 1.41421356237
40
+
41
+
42
+ class BaseStyle(BaseModel):
43
+ __hash__ = object.__hash__
44
+
45
+ class Config:
46
+ extra = "forbid"
47
+ use_enum_values = True
48
+ validate_assignment = True
49
+
50
+ def __enter__(self):
51
+ self._original = self.model_copy(deep=True)
52
+ return self
53
+
54
+ def __exit__(self, exception_type, exception_value, traceback):
55
+ for field_name in self.__pydantic_fields__.keys():
56
+ original_value = getattr(self._original, field_name)
57
+ setattr(self, field_name, original_value)
58
+
59
+
60
+ class GradientDirection(str, Enum):
61
+ LINEAR = "linear"
62
+ RADIAL = "radial"
63
+ MOLLWEIDE = "mollweide"
64
+
65
+
66
+ class FillStyleEnum(str, Enum):
67
+ """Constants that represent the possible fill styles for markers."""
68
+
69
+ FULL = "full"
70
+ """Fill the marker completely"""
71
+
72
+ LEFT = "left"
73
+ """Fill the left half of the marker"""
74
+
75
+ RIGHT = "right"
76
+ """Fill the right half of the marker"""
77
+
78
+ BOTTOM = "bottom"
79
+ """Fill the bottom half"""
80
+
81
+ TOP = "top"
82
+ """Fill the top half"""
83
+
84
+ NONE = "none"
85
+ """Do not fill the marker. It'll still have an edge, but the inside will be transparent."""
86
+
87
+
88
+ class FontWeightEnum(int, Enum):
89
+ """Options for font weight."""
90
+
91
+ THIN = 100
92
+ EXTRA_LIGHT = 200
93
+ # LIGHT = 300 # matplotlib's font dict doesn't have 300?
94
+ NORMAL = 400
95
+ MEDIUM = 500
96
+ SEMI_BOLD = 600
97
+ BOLD = 700
98
+ EXTRA_BOLD = 800
99
+ HEAVY = 900
100
+
101
+ def as_matplot(self) -> str:
102
+ """Returns the font weight as a matplotlib string, which avoids a bug with integer font weights and rendering text as elements in SVG."""
103
+ return {
104
+ FontWeightEnum.THIN: "ultralight",
105
+ FontWeightEnum.EXTRA_LIGHT: "light", # matplotlib maps 'light' to 200, which is really extra light
106
+ # FontWeightEnum.LIGHT: "light",
107
+ FontWeightEnum.NORMAL: "normal",
108
+ FontWeightEnum.MEDIUM: "medium",
109
+ FontWeightEnum.SEMI_BOLD: "semibold",
110
+ FontWeightEnum.BOLD: "bold",
111
+ FontWeightEnum.EXTRA_BOLD: "extra bold",
112
+ FontWeightEnum.HEAVY: "black",
113
+ }[self.value]
114
+
115
+
116
+ class FontStyleEnum(str, Enum):
117
+ NORMAL = "normal"
118
+ ITALIC = "italic"
119
+ OBLIQUE = "oblique"
120
+
121
+
122
+ class MarkerSymbolEnum(str, Enum):
123
+ """Options for marker symbols"""
124
+
125
+ POINT = "point"
126
+ """\u00B7"""
127
+
128
+ PLUS = "plus"
129
+ """+"""
130
+
131
+ CIRCLE = "circle"
132
+ """\u25CF"""
133
+
134
+ SQUARE = "square"
135
+ """\u25A0"""
136
+
137
+ SQUARE_STRIPES_DIAGONAL = "square_stripes_diagonal"
138
+ """\u25A8"""
139
+
140
+ STAR = "star"
141
+ """\u2605"""
142
+
143
+ SUN = "sun"
144
+ """\u263C"""
145
+
146
+ DIAMOND = "diamond"
147
+ """\u25C6"""
148
+
149
+ TRIANGLE = "triangle"
150
+ """\u23F6"""
151
+
152
+ CIRCLE_PLUS = "circle_plus"
153
+ """\u2295"""
154
+
155
+ CIRCLE_CROSS = "circle_cross"
156
+ """\u1AA0"""
157
+
158
+ CIRCLE_CROSSHAIR = "circle_crosshair"
159
+ """No preview available, but this is the standard symbol for planetary nebulae"""
160
+
161
+ CIRCLE_DOT = "circle_dot"
162
+ """\u29BF"""
163
+
164
+ CIRCLE_DOTTED_EDGE = "circle_dotted_edge"
165
+ """\u25CC"""
166
+
167
+ CIRCLE_DOTTED_RINGS = "circle_dotted_rings"
168
+
169
+ CIRCLE_LINE = "circle_line"
170
+ """\u29B5 the standard symbol for double stars"""
171
+
172
+ COMET = "comet"
173
+ """\u2604"""
174
+
175
+ STAR_4 = "star_4"
176
+ """\u2726"""
177
+
178
+ STAR_8 = "star_8"
179
+ """\u2734"""
180
+
181
+ ELLIPSE = "ellipse"
182
+ """\u2B2D"""
183
+
184
+ def as_matplot(self) -> str:
185
+ """Returns the matplotlib value of this marker"""
186
+ return {
187
+ MarkerSymbolEnum.POINT: ".",
188
+ MarkerSymbolEnum.CIRCLE: "o",
189
+ MarkerSymbolEnum.SQUARE: "s",
190
+ MarkerSymbolEnum.PLUS: "P",
191
+ MarkerSymbolEnum.SQUARE_STRIPES_DIAGONAL: "$\u25A8$",
192
+ MarkerSymbolEnum.STAR: "*",
193
+ MarkerSymbolEnum.SUN: "$\u263C$",
194
+ MarkerSymbolEnum.DIAMOND: "D",
195
+ MarkerSymbolEnum.TRIANGLE: "^",
196
+ MarkerSymbolEnum.CIRCLE_PLUS: "$\u2295$",
197
+ MarkerSymbolEnum.CIRCLE_CROSS: circle_cross(),
198
+ MarkerSymbolEnum.CIRCLE_CROSSHAIR: circle_crosshair(),
199
+ MarkerSymbolEnum.CIRCLE_DOT: circle_dot(),
200
+ MarkerSymbolEnum.CIRCLE_DOTTED_EDGE: "$\u25CC$",
201
+ MarkerSymbolEnum.CIRCLE_DOTTED_RINGS: circle_dotted_rings(),
202
+ MarkerSymbolEnum.CIRCLE_LINE: circle_line(),
203
+ MarkerSymbolEnum.COMET: "$\u2604$",
204
+ MarkerSymbolEnum.STAR_4: "$\u2726$",
205
+ MarkerSymbolEnum.STAR_8: "$\u2734$",
206
+ MarkerSymbolEnum.ELLIPSE: ellipse(),
207
+ }[self.value]
208
+
209
+
210
+ class LineStyleEnum(str, Enum):
211
+ SOLID = "solid"
212
+ DASHED = "dashed"
213
+ DASHED_DOTS = "dashdot"
214
+ DOTTED = "dotted"
215
+
216
+
217
+ class CapStyleEnum(str, Enum):
218
+ BUTT = "butt"
219
+ PROJECTING = "projecting"
220
+ ROUND = "round"
221
+
222
+
223
+ class JoinStyleEnum(str, Enum):
224
+ MITRE = "mitre"
225
+ BEVEL = "bevel"
226
+ ROUND = "round"
227
+
228
+
229
+ class LegendLocationEnum(str, Enum):
230
+ """Options for the location of the map legend"""
231
+
232
+ INSIDE_TOP = "upper center"
233
+ INSIDE_TOP_LEFT = "upper left"
234
+ INSIDE_TOP_RIGHT = "upper right"
235
+ INSIDE_BOTTOM = "lower center"
236
+ INSIDE_BOTTOM_RIGHT = "lower right"
237
+ INSIDE_BOTTOM_LEFT = "lower left"
238
+
239
+ # OUTSIDE_TOP = "outside upper center"
240
+ # OUTSIDE_BOTTOM = "outside lower center"
241
+
242
+ OUTSIDE_TOP_LEFT = "outside left upper"
243
+ OUTSIDE_TOP_RIGHT = "outside right upper"
244
+ OUTSIDE_BOTTOM_RIGHT = "outside right lower"
245
+ OUTSIDE_BOTTOM_LEFT = "outside left lower"
246
+
247
+
248
+ class AnchorPointEnum(str, Enum):
249
+ """Options for the anchor point of labels"""
250
+
251
+ CENTER = "center"
252
+ LEFT_CENTER = "left center"
253
+ RIGHT_CENTER = "right center"
254
+ TOP_LEFT = "top left"
255
+ TOP_RIGHT = "top right"
256
+ TOP_CENTER = "top center"
257
+ BOTTOM_LEFT = "bottom left"
258
+ BOTTOM_RIGHT = "bottom right"
259
+ BOTTOM_CENTER = "bottom center"
260
+
261
+ def as_matplot(self) -> dict:
262
+ style = {}
263
+ # the values below look wrong, but they're inverted because the map coords are inverted
264
+ if self.value == AnchorPointEnum.BOTTOM_LEFT:
265
+ style["va"] = "top"
266
+ style["ha"] = "right"
267
+ elif self.value == AnchorPointEnum.BOTTOM_RIGHT:
268
+ style["va"] = "top"
269
+ style["ha"] = "left"
270
+ elif self.value == AnchorPointEnum.BOTTOM_CENTER:
271
+ style["va"] = "top"
272
+ style["ha"] = "center"
273
+ elif self.value == AnchorPointEnum.TOP_LEFT:
274
+ style["va"] = "bottom"
275
+ style["ha"] = "right"
276
+ elif self.value == AnchorPointEnum.TOP_RIGHT:
277
+ style["va"] = "bottom"
278
+ style["ha"] = "left"
279
+ elif self.value == AnchorPointEnum.TOP_CENTER:
280
+ style["va"] = "bottom"
281
+ style["ha"] = "center"
282
+ elif self.value == AnchorPointEnum.CENTER:
283
+ style["va"] = "center"
284
+ style["ha"] = "center"
285
+ elif self.value == AnchorPointEnum.LEFT_CENTER:
286
+ style["va"] = "center"
287
+ style["ha"] = "right"
288
+ elif self.value == AnchorPointEnum.RIGHT_CENTER:
289
+ style["va"] = "center"
290
+ style["ha"] = "left"
291
+
292
+ return style
293
+
294
+ @staticmethod
295
+ def from_str(value: str) -> "AnchorPointEnum":
296
+ options = {ap.value: ap for ap in AnchorPointEnum}
297
+ return options.get(value)
298
+
299
+
300
+ class AlignmentEnum(str, Enum):
301
+ """Alignment options for the legend's title and entries"""
302
+
303
+ LEFT = "left"
304
+ RIGHT = "right"
305
+ CENTER = "center"
306
+
307
+
308
+ class ZOrderEnum(int, Enum):
309
+ """
310
+ Z Order presets for managing layers
311
+ """
312
+
313
+ LAYER_1 = -2_000
314
+ """Bottom layer"""
315
+
316
+ LAYER_2 = -1_000
317
+
318
+ LAYER_3 = 0
319
+ """Middle layer"""
320
+
321
+ LAYER_4 = 1_000
322
+
323
+ LAYER_5 = 2_000
324
+ """Top layer"""
325
+
326
+
327
+ class MarkerStyle(BaseStyle):
328
+ """
329
+ Styling properties for markers.
330
+ """
331
+
332
+ color: Optional[ColorStr] = ColorStr("#000")
333
+ """Fill color of marker. Can be a hex, rgb, hsl, or word string."""
334
+
335
+ edge_color: Optional[ColorStr] = ColorStr("#000")
336
+ """Edge color of marker. Can be a hex, rgb, hsl, or word string."""
337
+
338
+ edge_width: float = 1
339
+ """Edge width of marker, in points. Not available for all marker symbols."""
340
+
341
+ line_style: Union[LineStyleEnum, tuple] = LineStyleEnum.SOLID
342
+ """Edge line style. Can be a predefined value in `LineStyleEnum` or a [Matplotlib linestyle tuple](https://matplotlib.org/stable/gallery/lines_bars_and_markers/linestyles.html)."""
343
+
344
+ dash_capstyle: CapStyleEnum = CapStyleEnum.PROJECTING
345
+ """Style of dash endpoints"""
346
+
347
+ symbol: MarkerSymbolEnum = MarkerSymbolEnum.POINT
348
+ """Symbol for marker"""
349
+
350
+ size: float = 22
351
+ """Size of marker in points"""
352
+
353
+ fill: FillStyleEnum = FillStyleEnum.NONE
354
+ """Fill style of marker"""
355
+
356
+ alpha: float = 1.0
357
+ """Alpha value (controls transparency)"""
358
+
359
+ zorder: int = ZOrderEnum.LAYER_2
360
+ """Zorder of marker"""
361
+
362
+ @property
363
+ def symbol_matplot(self) -> str:
364
+ return MarkerSymbolEnum(self.symbol).as_matplot()
365
+
366
+ def matplot_kwargs(self, scale: float = 1.0) -> dict:
367
+ return dict(
368
+ color=self.color.as_hex() if self.color else "none",
369
+ markeredgecolor=self.edge_color.as_hex() if self.edge_color else "none",
370
+ marker=MarkerSymbolEnum(self.symbol).as_matplot(),
371
+ markersize=self.size * scale,
372
+ fillstyle=self.fill,
373
+ alpha=self.alpha,
374
+ zorder=self.zorder,
375
+ )
376
+
377
+ def matplot_scatter_kwargs(self, scale: float = 1.0) -> dict:
378
+ plot_kwargs = self.matplot_kwargs(scale)
379
+ plot_kwargs["edgecolors"] = plot_kwargs.pop("markeredgecolor")
380
+
381
+ # matplotlib's plot() function takes the marker size in points diameter
382
+ # and the scatter() function takes it in points squared
383
+ plot_kwargs["s"] = ((plot_kwargs.pop("markersize") / scale) ** 2) * (scale**2)
384
+
385
+ plot_kwargs["c"] = plot_kwargs.pop("color")
386
+ plot_kwargs["linewidths"] = self.edge_width * scale
387
+ plot_kwargs["linestyle"] = self.line_style
388
+ plot_kwargs["capstyle"] = self.dash_capstyle
389
+
390
+ plot_kwargs.pop("fillstyle")
391
+
392
+ return plot_kwargs
393
+
394
+ def to_polygon_style(self):
395
+ return PolygonStyle(
396
+ fill_color=self.color.as_hex() if self.color else None,
397
+ edge_color=self.edge_color.as_hex() if self.edge_color else None,
398
+ edge_width=self.edge_width,
399
+ alpha=self.alpha,
400
+ zorder=self.zorder,
401
+ line_style=self.line_style,
402
+ )
403
+
404
+
405
+ class LineStyle(BaseStyle):
406
+ """
407
+ Styling properties for lines.
408
+ """
409
+
410
+ width: float = 4
411
+ """Width of line in points"""
412
+
413
+ color: ColorStr = ColorStr("#000")
414
+ """Color of the line. Can be a hex, rgb, hsl, or word string."""
415
+
416
+ style: Union[LineStyleEnum, tuple] = LineStyleEnum.SOLID
417
+ """Style of the line (e.g. solid, dashed, etc). Can be a predefined value in `LineStyleEnum` or a [Matplotlib linestyle tuple](https://matplotlib.org/stable/gallery/lines_bars_and_markers/linestyles.html)."""
418
+
419
+ dash_capstyle: CapStyleEnum = CapStyleEnum.PROJECTING
420
+ """Style of dash endpoints"""
421
+
422
+ alpha: float = 1.0
423
+ """Alpha value (controls transparency)"""
424
+
425
+ zorder: int = ZOrderEnum.LAYER_2
426
+ """Zorder of the line"""
427
+
428
+ edge_width: int = 0
429
+ """Width of the line's edge in points. _If the width or color is falsey then the line will NOT be drawn with an edge._"""
430
+
431
+ edge_color: Optional[ColorStr] = None
432
+ """Edge color of the line. _If the width or color is falsey then the line will NOT be drawn with an edge._"""
433
+
434
+ def matplot_kwargs(self, scale: float = 1.0) -> dict:
435
+ line_width = self.width * scale
436
+
437
+ result = dict(
438
+ color=self.color.as_hex(),
439
+ linestyle=self.style,
440
+ linewidth=line_width,
441
+ # dash_capstyle=self.dash_capstyle,
442
+ alpha=self.alpha,
443
+ zorder=self.zorder,
444
+ )
445
+
446
+ if self.edge_width and self.edge_color:
447
+ result["path_effects"] = [
448
+ patheffects.withStroke(
449
+ linewidth=line_width + 2 * self.edge_width * scale,
450
+ foreground=self.edge_color.as_hex(),
451
+ )
452
+ ]
453
+
454
+ return result
455
+
456
+ def matplot_line_collection_kwargs(self, scale: float = 1.0) -> dict:
457
+ plot_kwargs = self.matplot_kwargs(scale)
458
+ plot_kwargs["linewidths"] = plot_kwargs.pop("linewidth")
459
+ plot_kwargs["colors"] = plot_kwargs.pop("color")
460
+ return plot_kwargs
461
+
462
+
463
+ class PolygonStyle(BaseStyle):
464
+ """
465
+ Styling properties for polygons.
466
+ """
467
+
468
+ edge_width: float = 1
469
+ """Width of the polygon's edge in points"""
470
+
471
+ color: Optional[ColorStr] = None
472
+ """If specified, this will be the fill color AND edge color of the polygon"""
473
+
474
+ edge_color: Optional[ColorStr] = None
475
+ """Edge color of the polygon"""
476
+
477
+ fill_color: Optional[ColorStr] = None
478
+ """Fill color of the polygon"""
479
+
480
+ line_style: Union[LineStyleEnum, tuple] = LineStyleEnum.SOLID
481
+ """Edge line style. Can be a predefined value in `LineStyleEnum` or a [Matplotlib linestyle tuple](https://matplotlib.org/stable/gallery/lines_bars_and_markers/linestyles.html)."""
482
+
483
+ alpha: float = 1.0
484
+ """Alpha value (controls transparency)"""
485
+
486
+ zorder: int = -1
487
+ """Zorder of the polygon"""
488
+
489
+ def matplot_kwargs(self, scale: float = 1.0) -> dict:
490
+ styles = dict(
491
+ edgecolor=self.edge_color.as_hex() if self.edge_color else "none",
492
+ facecolor=self.fill_color.as_hex() if self.fill_color else "none",
493
+ fill=True if self.fill_color or self.color else False,
494
+ linewidth=self.edge_width * scale,
495
+ linestyle=self.line_style,
496
+ alpha=self.alpha,
497
+ zorder=self.zorder,
498
+ capstyle="round",
499
+ )
500
+ if self.color:
501
+ styles["color"] = self.color.as_hex()
502
+
503
+ return styles
504
+
505
+ def to_marker_style(self, symbol: MarkerSymbolEnum):
506
+ color = self.color.as_hex() if self.color else None
507
+ fill_color = self.fill_color.as_hex() if self.fill_color else None
508
+ fill_style = FillStyleEnum.FULL if color or fill_color else FillStyleEnum.NONE
509
+ return MarkerStyle(
510
+ symbol=symbol,
511
+ color=color or fill_color,
512
+ fill=fill_style,
513
+ edge_color=self.edge_color.as_hex() if self.edge_color else None,
514
+ edge_width=self.edge_width,
515
+ alpha=self.alpha,
516
+ zorder=self.zorder,
517
+ line_style=self.line_style,
518
+ )
519
+
520
+
521
+ class ArrowStyle(PolygonStyle):
522
+ body_width: float = 24
523
+ """Width of the arrow's body, in pixels"""
524
+
525
+ head_width: float = 60
526
+ """Width of the arrow's head, in pixels"""
527
+
528
+ head_height: float = 100
529
+ """Height of the arrow's head, in pixels"""
530
+
531
+ cap_style: CapStyleEnum = CapStyleEnum.BUTT
532
+ """Cap style of the arrow"""
533
+
534
+ join_style: JoinStyleEnum = JoinStyleEnum.MITRE
535
+ """Join style of the arrow"""
536
+
537
+ def shapely_kwargs(self):
538
+ cap_styles = {
539
+ CapStyleEnum.BUTT: "flat",
540
+ CapStyleEnum.ROUND: "round",
541
+ CapStyleEnum.PROJECTING: "square",
542
+ }
543
+ return {
544
+ "cap_style": cap_styles[self.cap_style],
545
+ "join_style": self.join_style,
546
+ }
547
+
548
+
549
+ class LabelStyle(BaseStyle):
550
+ """
551
+ Styling properties for a label.
552
+ """
553
+
554
+ font_size: float = 15
555
+ """Font size of the label, in points"""
556
+
557
+ font_weight: FontWeightEnum = FontWeightEnum.NORMAL
558
+ """Font weight (e.g. normal, bold, ultra bold, etc)"""
559
+
560
+ font_color: ColorStr = ColorStr("#000")
561
+ """Font's color"""
562
+
563
+ font_alpha: float = 1
564
+ """Font's alpha (transparency)"""
565
+
566
+ font_style: FontStyleEnum = FontStyleEnum.NORMAL
567
+ """Style of the label (e.g. normal, italic, etc)"""
568
+
569
+ font_name: Optional[str] = "Inter"
570
+ """Name of the font to use"""
571
+
572
+ font_family: Optional[str] = None
573
+ """Font family (e.g. 'monospace', 'sans-serif', 'serif', etc)"""
574
+
575
+ line_spacing: Optional[float] = None
576
+ """Spacing between lines of text"""
577
+
578
+ anchor_point: AnchorPointEnum = AnchorPointEnum.BOTTOM_RIGHT
579
+ """Anchor point of label"""
580
+
581
+ border_width: float = 0
582
+ """Width of border (also known as 'halos') around the text, in points"""
583
+
584
+ border_color: Optional[ColorStr] = None
585
+ """Color of border (also known as 'halos') around the text"""
586
+
587
+ offset_x: Union[float, int, str] = 0
588
+ """
589
+ Horizontal offset of the label, in points. Negative values supported.
590
+
591
+
592
+ **Auto Mode** (_experimental_): If the label is plotted as part of a marker (e.g. stars, via `marker()`, etc), then you can also
593
+ specify the offset as `"auto"` which will calculate the offset automatically based on the marker's size and place
594
+ the label just outside the marker (avoiding overlapping). To enable "auto" mode you have to specify BOTH offsets (x and y) as "auto."
595
+ """
596
+
597
+ offset_y: Union[float, int, str] = 0
598
+ """
599
+ Vertical offset of the label, in points. Negative values supported.
600
+
601
+ **Auto Mode** (_experimental_): If the label is plotted as part of a marker (e.g. stars, via `marker()`, etc), then you can also
602
+ specify the offset as `"auto"` which will calculate the offset automatically based on the marker's size and place
603
+ the label just outside the marker (avoiding overlapping). To enable "auto" mode you have to specify BOTH offsets (x and y) as "auto."
604
+ """
605
+
606
+ zorder: int = ZOrderEnum.LAYER_4
607
+ """Zorder of the label"""
608
+
609
+ def matplot_kwargs(self, scale: float = 1.0) -> dict:
610
+ style = dict(
611
+ color=self.font_color.as_hex(),
612
+ fontsize=self.font_size * scale,
613
+ fontstyle=self.font_style,
614
+ fontname=self.font_name,
615
+ weight=FontWeightEnum(self.font_weight).as_matplot(),
616
+ alpha=self.font_alpha,
617
+ zorder=self.zorder,
618
+ )
619
+
620
+ if self.font_family:
621
+ style["family"] = self.font_family
622
+ if self.line_spacing:
623
+ style["linespacing"] = self.line_spacing
624
+
625
+ if self.border_width != 0 and self.border_color is not None:
626
+ style["path_effects"] = [
627
+ patheffects.withStroke(
628
+ linewidth=self.border_width * scale,
629
+ foreground=self.border_color.as_hex(),
630
+ )
631
+ ]
632
+
633
+ style.update(AnchorPointEnum(self.anchor_point).as_matplot())
634
+
635
+ return style
636
+
637
+ def offset_from_marker(self, marker_symbol, marker_size, scale: float = 1.0):
638
+ """Handles auto offsets from marker"""
639
+
640
+ if self.offset_x != "auto" or self.offset_y != "auto":
641
+ return self
642
+
643
+ new_style = self.model_copy()
644
+
645
+ x_direction = -1 if new_style.anchor_point.endswith("left") else 1
646
+ y_direction = -1 if new_style.anchor_point.startswith("bottom") else 1
647
+
648
+ offset = (marker_size**0.5 / 2) / scale
649
+
650
+ # matplotlib seems to use marker size differently depending on symbol (for scatter)
651
+ # it is NOT strictly the area of the bounding box of the marker
652
+ if marker_symbol in [MarkerSymbolEnum.POINT]:
653
+ offset /= PI
654
+
655
+ elif marker_symbol != MarkerSymbolEnum.SQUARE:
656
+ offset /= SQR_2
657
+ offset *= scale
658
+
659
+ offset += 0.65
660
+ new_style.offset_x = offset * float(x_direction)
661
+ new_style.offset_y = offset * float(y_direction)
662
+
663
+ return new_style
664
+
665
+
666
+ class ObjectStyle(BaseStyle):
667
+ """Defines the style for a sky object (e.g. star, DSO)"""
668
+
669
+ marker: MarkerStyle = MarkerStyle()
670
+ """Style for the object's marker (see [MarkerStyle][starplot.styles.MarkerStyle])"""
671
+
672
+ label: LabelStyle = LabelStyle()
673
+ """Style for the object's label (see [LabelStyle][starplot.styles.LabelStyle])"""
674
+
675
+
676
+ class PathStyle(BaseStyle):
677
+ """Defines the style for a path (e.g. constellation lines)"""
678
+
679
+ line: LineStyle = LineStyle()
680
+ """Style for the line (see [LineStyle][starplot.styles.LineStyle])"""
681
+
682
+ label: LabelStyle = LabelStyle()
683
+ """Style for the path's label (see [LabelStyle][starplot.styles.LabelStyle])"""
684
+
685
+
686
+ class LegendStyle(BaseStyle):
687
+ """Defines the style for the map legend. *Only applies to map plots.*"""
688
+
689
+ alignment: AlignmentEnum = AlignmentEnum.LEFT
690
+ """Alignment for the legend's title and entries"""
691
+
692
+ location: LegendLocationEnum = LegendLocationEnum.INSIDE_BOTTOM_RIGHT
693
+ """Location of the legend, relative to the map area (inside or outside)"""
694
+
695
+ background_color: ColorStr = ColorStr("#fff")
696
+ """Background color of the legend box"""
697
+
698
+ background_alpha: float = 1.0
699
+ """Background's alpha (transparency)"""
700
+
701
+ padding: float = 0
702
+ """Padding on the outside of the legend. Negative numbers are supported."""
703
+
704
+ padding_x: float = 0
705
+ """Padding (in pixels) between the _outside_ of the legend and the map in the X axis. Negative numbers are supported."""
706
+
707
+ padding_y: float = 0
708
+ """Padding (in pixels) between the _outside_ of the legend and the map in the Y axis. Negative numbers are supported."""
709
+
710
+ expand: bool = False
711
+ """If True, the legend will be expanded to fit the full width of the map"""
712
+
713
+ num_columns: int = 8
714
+ """Number of columns in the legend"""
715
+
716
+ label_padding: float = 1.6
717
+ """Padding between legend labels"""
718
+
719
+ symbol_size: int = 34
720
+ """Size of symbols in the legend, in points"""
721
+
722
+ symbol_padding: float = 0.2
723
+ """Padding between each symbol and its label"""
724
+
725
+ border_color: ColorStr = ColorStr("#c5c5c5")
726
+ """Border color of the legend box"""
727
+
728
+ border_padding: float = 1.28
729
+ """Padding between legend entries and the legend border"""
730
+
731
+ font_name: str = "Inter"
732
+ """Font name for legend labels"""
733
+
734
+ font_size: int = 23
735
+ """Font size of the legend labels, in points"""
736
+
737
+ font_weight: FontWeightEnum = FontWeightEnum.NORMAL
738
+ """Font weight of the legend labels"""
739
+
740
+ font_color: ColorStr = ColorStr("#000")
741
+ """Font color for legend labels"""
742
+
743
+ title_font_size: int = 36
744
+ """Font size of the legend title"""
745
+
746
+ title_font_weight: FontWeightEnum = FontWeightEnum.BOLD
747
+ """Font weight of the legend title"""
748
+
749
+ title_font_name: str = "Inter"
750
+ """Name of the font to use for the title. Comma-separated list."""
751
+
752
+ zorder: int = ZOrderEnum.LAYER_5
753
+ """Zorder of the legend"""
754
+
755
+ def matplot_kwargs(self, scale: float = 1.0) -> dict:
756
+ return dict(
757
+ loc=self.location,
758
+ ncols=self.num_columns,
759
+ framealpha=self.background_alpha,
760
+ prop={
761
+ "family": self.font_name,
762
+ "weight": FontWeightEnum(self.font_weight).as_matplot(),
763
+ "size": self.font_size * scale,
764
+ },
765
+ labelcolor=self.font_color.as_hex(),
766
+ borderpad=self.border_padding,
767
+ labelspacing=self.label_padding,
768
+ handletextpad=self.symbol_padding,
769
+ mode="expand" if self.expand else None,
770
+ facecolor=self.background_color.as_hex(),
771
+ title_fontproperties=dict(
772
+ weight=FontWeightEnum(self.title_font_weight).as_matplot(),
773
+ size=self.title_font_size,
774
+ family=self.title_font_name.split(","),
775
+ ),
776
+ alignment=self.alignment,
777
+ edgecolor=self.border_color.as_hex(),
778
+ borderaxespad=self.padding,
779
+ )
780
+
781
+
782
+ class PlotStyle(BaseStyle):
783
+ """
784
+ Defines the styling for a plot
785
+ """
786
+
787
+ background_color: list[tuple[float, str]] | ColorStr = ColorStr("#fff")
788
+ """
789
+ Background color of the map region.
790
+
791
+ This can either be a single color (e.g. `#7abfff`) or a list that defines a gradient.
792
+
793
+ For gradients, the list items should be tuples with two elements: a float that defines
794
+ the stop and a string that defines the color for that stop. For example:
795
+
796
+ ```
797
+ "background_color": [
798
+ (0.0, "#7abfff"),
799
+ (0.2, "#7abfff"),
800
+ (0.9, "#568feb"),
801
+ (1.0, "#3f7ee3"), # the last stop should always be at 1.0
802
+ ]
803
+ ```
804
+
805
+ There are a few predefined gradients available as [style extensions](/reference-styling/#style-extensions).
806
+
807
+ **Gradient backgrounds are not yet supported for optic plots that use a camera.**
808
+ """
809
+
810
+ figure_background_color: ColorStr = ColorStr("#fff")
811
+
812
+ text_border_width: int = 2
813
+ """Text border (aka halos) width. This will apply to _all_ text labels on the plot. If you'd like to control these borders by object type, then set this global width to `0` and refer to the label style's `border_width` and `border_color` properties."""
814
+
815
+ text_border_color: ColorStr = ColorStr("#fff")
816
+
817
+ # Borders
818
+ border_font_size: int = 18
819
+ border_font_weight: FontWeightEnum = FontWeightEnum.BOLD
820
+ border_font_color: ColorStr = ColorStr("#000")
821
+ border_line_color: ColorStr = ColorStr("#000")
822
+ border_bg_color: ColorStr = ColorStr("#fff")
823
+
824
+ # Title
825
+ title: LabelStyle = LabelStyle(
826
+ font_size=70,
827
+ font_weight=FontWeightEnum.BOLD,
828
+ zorder=ZOrderEnum.LAYER_5,
829
+ line_spacing=150,
830
+ anchor_point=AnchorPointEnum.BOTTOM_CENTER,
831
+ )
832
+ """Styling for info text (only applies to zenith and optic plots)"""
833
+
834
+ # Info text
835
+ info_text: LabelStyle = LabelStyle(
836
+ font_size=30,
837
+ zorder=ZOrderEnum.LAYER_5,
838
+ font_family="Inter",
839
+ line_spacing=1.2,
840
+ anchor_point=AnchorPointEnum.BOTTOM_CENTER,
841
+ )
842
+ """Styling for info text (only applies to zenith and optic plots)"""
843
+
844
+ # Stars
845
+ star: ObjectStyle = ObjectStyle(
846
+ marker=MarkerStyle(
847
+ fill=FillStyleEnum.FULL,
848
+ zorder=ZOrderEnum.LAYER_3 + 1,
849
+ size=40,
850
+ edge_color=None,
851
+ ),
852
+ label=LabelStyle(
853
+ font_size=24,
854
+ font_weight=FontWeightEnum.BOLD,
855
+ zorder=ZOrderEnum.LAYER_3 + 2,
856
+ offset_x="auto",
857
+ offset_y="auto",
858
+ ),
859
+ )
860
+ """Styling for stars *(see [`ObjectStyle`][starplot.styles.ObjectStyle])*"""
861
+
862
+ bayer_labels: LabelStyle = LabelStyle(
863
+ font_size=21,
864
+ font_weight=FontWeightEnum.EXTRA_LIGHT,
865
+ font_name="GFS Didot",
866
+ zorder=ZOrderEnum.LAYER_4,
867
+ anchor_point=AnchorPointEnum.TOP_LEFT,
868
+ offset_x="auto",
869
+ offset_y="auto",
870
+ )
871
+ """Styling for Bayer labels of stars"""
872
+
873
+ flamsteed_labels: LabelStyle = LabelStyle(
874
+ font_size=13,
875
+ font_weight=FontWeightEnum.NORMAL,
876
+ zorder=ZOrderEnum.LAYER_4,
877
+ anchor_point=AnchorPointEnum.BOTTOM_LEFT,
878
+ offset_x="auto",
879
+ offset_y="auto",
880
+ )
881
+ """Styling for Flamsteed number labels of stars"""
882
+
883
+ planets: ObjectStyle = ObjectStyle(
884
+ marker=MarkerStyle(
885
+ symbol=MarkerSymbolEnum.CIRCLE,
886
+ size=28,
887
+ fill=FillStyleEnum.LEFT,
888
+ zorder=ZOrderEnum.LAYER_3,
889
+ alpha=1,
890
+ ),
891
+ label=LabelStyle(
892
+ font_size=28,
893
+ font_weight=FontWeightEnum.BOLD,
894
+ offset_x="auto",
895
+ offset_y="auto",
896
+ ),
897
+ )
898
+ """Styling for planets"""
899
+
900
+ moon: ObjectStyle = ObjectStyle(
901
+ marker=MarkerStyle(
902
+ symbol=MarkerSymbolEnum.CIRCLE,
903
+ size=50,
904
+ fill=FillStyleEnum.FULL,
905
+ color="#c8c8c8",
906
+ alpha=1,
907
+ zorder=ZOrderEnum.LAYER_4,
908
+ ),
909
+ label=LabelStyle(
910
+ font_size=28,
911
+ font_weight=FontWeightEnum.BOLD,
912
+ offset_x="auto",
913
+ offset_y="auto",
914
+ ),
915
+ )
916
+ """Styling for the moon"""
917
+
918
+ sun: ObjectStyle = ObjectStyle(
919
+ marker=MarkerStyle(
920
+ symbol=MarkerSymbolEnum.SUN,
921
+ size=80,
922
+ fill=FillStyleEnum.FULL,
923
+ color="#000",
924
+ zorder=ZOrderEnum.LAYER_4 - 100,
925
+ ),
926
+ label=LabelStyle(
927
+ font_size=28,
928
+ font_weight=FontWeightEnum.BOLD,
929
+ ),
930
+ )
931
+ """Styling for the Sun"""
932
+
933
+ # Deep Sky Objects (DSOs)
934
+ dso_open_cluster: ObjectStyle = ObjectStyle(
935
+ marker=MarkerStyle(
936
+ symbol=MarkerSymbolEnum.CIRCLE,
937
+ fill=FillStyleEnum.FULL,
938
+ line_style=(0, (1, 2)),
939
+ edge_width=1.3,
940
+ zorder=ZOrderEnum.LAYER_3 - 1,
941
+ ),
942
+ label=LabelStyle(offset_x="auto", offset_y="auto"),
943
+ )
944
+ """Styling for open star clusters"""
945
+
946
+ dso_association_stars: ObjectStyle = ObjectStyle(
947
+ marker=MarkerStyle(
948
+ symbol=MarkerSymbolEnum.CIRCLE,
949
+ fill=FillStyleEnum.FULL,
950
+ line_style=(0, (1, 2)),
951
+ edge_width=1.3,
952
+ zorder=ZOrderEnum.LAYER_3 - 1,
953
+ ),
954
+ label=LabelStyle(offset_x="auto", offset_y="auto"),
955
+ )
956
+ """Styling for associations of stars"""
957
+
958
+ dso_globular_cluster: ObjectStyle = ObjectStyle(
959
+ marker=MarkerStyle(
960
+ symbol=MarkerSymbolEnum.CIRCLE_CROSS,
961
+ fill=FillStyleEnum.FULL,
962
+ color="#555",
963
+ alpha=0.8,
964
+ edge_width=1.2,
965
+ zorder=ZOrderEnum.LAYER_3 - 1,
966
+ ),
967
+ label=LabelStyle(offset_x="auto", offset_y="auto"),
968
+ )
969
+ """Styling for globular star clusters"""
970
+
971
+ dso_galaxy: ObjectStyle = ObjectStyle(
972
+ marker=MarkerStyle(
973
+ symbol=MarkerSymbolEnum.ELLIPSE,
974
+ fill=FillStyleEnum.FULL,
975
+ zorder=ZOrderEnum.LAYER_3 - 1,
976
+ ),
977
+ label=LabelStyle(offset_x="auto", offset_y="auto"),
978
+ )
979
+ """Styling for galaxies"""
980
+
981
+ dso_nebula: ObjectStyle = ObjectStyle(
982
+ marker=MarkerStyle(
983
+ symbol=MarkerSymbolEnum.SQUARE,
984
+ fill=FillStyleEnum.FULL,
985
+ zorder=ZOrderEnum.LAYER_3 - 1,
986
+ ),
987
+ label=LabelStyle(offset_x="auto", offset_y="auto"),
988
+ )
989
+ """Styling for nebulas"""
990
+
991
+ dso_planetary_nebula: ObjectStyle = ObjectStyle(
992
+ marker=MarkerStyle(
993
+ symbol=MarkerSymbolEnum.CIRCLE_CROSSHAIR,
994
+ fill=FillStyleEnum.FULL,
995
+ edge_width=1.6,
996
+ size=26,
997
+ zorder=ZOrderEnum.LAYER_3 - 1,
998
+ ),
999
+ label=LabelStyle(offset_x="auto", offset_y="auto"),
1000
+ )
1001
+ """Styling for planetary nebulas"""
1002
+
1003
+ dso_double_star: ObjectStyle = ObjectStyle(
1004
+ marker=MarkerStyle(
1005
+ symbol=MarkerSymbolEnum.CIRCLE_LINE,
1006
+ fill=FillStyleEnum.TOP,
1007
+ zorder=ZOrderEnum.LAYER_3 - 1,
1008
+ ),
1009
+ label=LabelStyle(offset_x=1, offset_y=-1),
1010
+ )
1011
+ """Styling for double stars"""
1012
+
1013
+ dso_dark_nebula: ObjectStyle = ObjectStyle(
1014
+ marker=MarkerStyle(
1015
+ symbol=MarkerSymbolEnum.SQUARE,
1016
+ fill=FillStyleEnum.TOP,
1017
+ color="#000",
1018
+ zorder=ZOrderEnum.LAYER_3 - 1,
1019
+ ),
1020
+ label=LabelStyle(),
1021
+ )
1022
+ """Styling for dark nebulas"""
1023
+
1024
+ dso_supernova_remnant: ObjectStyle = ObjectStyle(
1025
+ marker=MarkerStyle(
1026
+ symbol=MarkerSymbolEnum.SQUARE,
1027
+ fill=FillStyleEnum.TOP,
1028
+ color="#000",
1029
+ zorder=ZOrderEnum.LAYER_3 - 1,
1030
+ ),
1031
+ label=LabelStyle(),
1032
+ )
1033
+ """Styling for supernova remnants"""
1034
+
1035
+ dso_nova_star: ObjectStyle = ObjectStyle(
1036
+ marker=MarkerStyle(
1037
+ symbol=MarkerSymbolEnum.SQUARE,
1038
+ fill=FillStyleEnum.TOP,
1039
+ color="#000",
1040
+ zorder=ZOrderEnum.LAYER_3 - 1,
1041
+ ),
1042
+ label=LabelStyle(),
1043
+ )
1044
+ """Styling for nova stars"""
1045
+
1046
+ dso_nonexistant: ObjectStyle = ObjectStyle(
1047
+ marker=MarkerStyle(
1048
+ symbol=MarkerSymbolEnum.SQUARE,
1049
+ fill=FillStyleEnum.TOP,
1050
+ color="#000",
1051
+ zorder=ZOrderEnum.LAYER_3 - 1,
1052
+ ),
1053
+ label=LabelStyle(),
1054
+ )
1055
+ """Styling for 'nonexistent' (as designated by OpenNGC) deep sky objects"""
1056
+
1057
+ dso_unknown: ObjectStyle = ObjectStyle(
1058
+ marker=MarkerStyle(
1059
+ symbol=MarkerSymbolEnum.SQUARE,
1060
+ fill=FillStyleEnum.TOP,
1061
+ color="#000",
1062
+ zorder=ZOrderEnum.LAYER_3 - 1,
1063
+ ),
1064
+ label=LabelStyle(),
1065
+ )
1066
+ """Styling for 'unknown' (as designated by OpenNGC) types of deep sky objects"""
1067
+
1068
+ dso_duplicate: ObjectStyle = ObjectStyle(
1069
+ marker=MarkerStyle(
1070
+ symbol=MarkerSymbolEnum.SQUARE,
1071
+ fill=FillStyleEnum.TOP,
1072
+ color="#000",
1073
+ zorder=ZOrderEnum.LAYER_3 - 1,
1074
+ ),
1075
+ label=LabelStyle(),
1076
+ )
1077
+ """Styling for 'duplicate record' (as designated by OpenNGC) types of deep sky objects"""
1078
+
1079
+ constellation_lines: LineStyle = LineStyle(
1080
+ color="#c8c8c8", zorder=ZOrderEnum.LAYER_3
1081
+ )
1082
+ """Styling for constellation lines"""
1083
+
1084
+ constellation_borders: LineStyle = LineStyle(
1085
+ color="#000",
1086
+ width=1.5,
1087
+ style=LineStyleEnum.DASHED,
1088
+ alpha=0.4,
1089
+ zorder=ZOrderEnum.LAYER_3,
1090
+ )
1091
+ """Styling for constellation borders"""
1092
+
1093
+ constellation_labels: LabelStyle = LabelStyle(
1094
+ font_size=21,
1095
+ font_weight=FontWeightEnum.NORMAL,
1096
+ zorder=ZOrderEnum.LAYER_3,
1097
+ anchor_point=AnchorPointEnum.CENTER,
1098
+ )
1099
+ """Styling for constellation labels"""
1100
+
1101
+ # Milky Way
1102
+ milky_way: PolygonStyle = PolygonStyle(
1103
+ fill_color="#d9d9d9",
1104
+ alpha=0.36,
1105
+ edge_width=0,
1106
+ zorder=ZOrderEnum.LAYER_1,
1107
+ )
1108
+ """Styling for the Milky Way (only applies to map plots)"""
1109
+
1110
+ # Legend
1111
+ legend: LegendStyle = LegendStyle()
1112
+ """Styling for legend"""
1113
+
1114
+ # Gridlines
1115
+ gridlines: PathStyle = PathStyle(
1116
+ line=LineStyle(
1117
+ color="#888",
1118
+ width=1,
1119
+ style=LineStyleEnum.SOLID,
1120
+ alpha=0.8,
1121
+ zorder=ZOrderEnum.LAYER_2,
1122
+ ),
1123
+ label=LabelStyle(
1124
+ font_size=24,
1125
+ font_color="#000",
1126
+ font_alpha=1,
1127
+ anchor_point=AnchorPointEnum.BOTTOM_CENTER,
1128
+ zorder=ZOrderEnum.LAYER_5 + 1000,
1129
+ ),
1130
+ )
1131
+ """Styling for gridlines (including Right Ascension / Declination labels). *Only applies to map plots*."""
1132
+
1133
+ ecliptic: PathStyle = PathStyle(
1134
+ line=LineStyle(
1135
+ color="#777",
1136
+ width=3,
1137
+ style=LineStyleEnum.DOTTED,
1138
+ dash_capstyle=CapStyleEnum.ROUND,
1139
+ alpha=1,
1140
+ zorder=ZOrderEnum.LAYER_3 - 1,
1141
+ ),
1142
+ label=LabelStyle(
1143
+ font_size=21,
1144
+ font_color="#777",
1145
+ font_alpha=1,
1146
+ font_weight=FontWeightEnum.NORMAL,
1147
+ border_width=8,
1148
+ border_color="#000",
1149
+ zorder=ZOrderEnum.LAYER_3,
1150
+ ),
1151
+ )
1152
+ """Styling for the Ecliptic"""
1153
+
1154
+ celestial_equator: PathStyle = PathStyle(
1155
+ line=LineStyle(
1156
+ color="#999",
1157
+ width=3,
1158
+ style=LineStyleEnum.DASHED_DOTS,
1159
+ alpha=1,
1160
+ zorder=ZOrderEnum.LAYER_3,
1161
+ ),
1162
+ label=LabelStyle(
1163
+ font_size=21,
1164
+ font_color="#999",
1165
+ font_weight=FontWeightEnum.NORMAL,
1166
+ font_alpha=1,
1167
+ border_width=8,
1168
+ border_color="#000",
1169
+ zorder=ZOrderEnum.LAYER_3,
1170
+ ),
1171
+ )
1172
+ """Styling for the Celestial Equator"""
1173
+
1174
+ galactic_equator: PathStyle = PathStyle(
1175
+ line=LineStyle(
1176
+ color="#999",
1177
+ width=3,
1178
+ style=LineStyleEnum.SOLID,
1179
+ alpha=0.65,
1180
+ zorder=ZOrderEnum.LAYER_3,
1181
+ ),
1182
+ label=LabelStyle(
1183
+ font_size=21,
1184
+ font_color="#7c7c7c",
1185
+ font_weight=FontWeightEnum.NORMAL,
1186
+ font_alpha=1,
1187
+ border_width=8,
1188
+ border_color="#000",
1189
+ zorder=ZOrderEnum.LAYER_3,
1190
+ ),
1191
+ )
1192
+ """Styling for the Galactic Equator"""
1193
+
1194
+ horizon: PathStyle = PathStyle(
1195
+ line=LineStyle(
1196
+ color="#fff",
1197
+ width=80,
1198
+ edge_width=4,
1199
+ edge_color="#000",
1200
+ style=LineStyleEnum.SOLID,
1201
+ dash_capstyle=CapStyleEnum.BUTT,
1202
+ alpha=1,
1203
+ zorder=ZOrderEnum.LAYER_5,
1204
+ ),
1205
+ label=LabelStyle(
1206
+ anchor_point=AnchorPointEnum.CENTER,
1207
+ font_color="#000",
1208
+ font_size=64,
1209
+ font_weight=FontWeightEnum.BOLD,
1210
+ zorder=ZOrderEnum.LAYER_5,
1211
+ ),
1212
+ )
1213
+ """Styling for the horizon"""
1214
+
1215
+ zenith: ObjectStyle = ObjectStyle(
1216
+ marker=MarkerStyle(
1217
+ symbol=MarkerSymbolEnum.TRIANGLE,
1218
+ size=24,
1219
+ fill=FillStyleEnum.FULL,
1220
+ color="#000",
1221
+ alpha=0.8,
1222
+ ),
1223
+ label=LabelStyle(font_size=14, font_weight=FontWeightEnum.BOLD),
1224
+ )
1225
+ """Styling for the zenith marker"""
1226
+
1227
+ optic_fov: PolygonStyle = PolygonStyle(
1228
+ fill_color=None,
1229
+ edge_color="red",
1230
+ line_style=[1, [2, 3]],
1231
+ edge_width=3,
1232
+ zorder=-1000,
1233
+ )
1234
+ """Styling for optic fields of view"""
1235
+
1236
+ arrow: ArrowStyle = ArrowStyle(
1237
+ fill_color="hsl(0, 99%, 31%)",
1238
+ edge_color="#ff0019",
1239
+ edge_width=2,
1240
+ zorder=ZOrderEnum.LAYER_4,
1241
+ )
1242
+ """Styling for optic fields of view"""
1243
+
1244
+ def get_dso_style(self, dso_type: DsoType):
1245
+ """Returns the style for a DSO type"""
1246
+ styles_by_type = {
1247
+ # Star Clusters ----------
1248
+ DsoType.OPEN_CLUSTER: self.dso_open_cluster,
1249
+ DsoType.GLOBULAR_CLUSTER: self.dso_globular_cluster,
1250
+ # Galaxies ----------
1251
+ DsoType.GALAXY: self.dso_galaxy,
1252
+ DsoType.GALAXY_PAIR: self.dso_galaxy,
1253
+ DsoType.GALAXY_TRIPLET: self.dso_galaxy,
1254
+ DsoType.GROUP_OF_GALAXIES: self.dso_galaxy,
1255
+ # Nebulas ----------
1256
+ DsoType.NEBULA: self.dso_nebula,
1257
+ DsoType.PLANETARY_NEBULA: self.dso_planetary_nebula,
1258
+ DsoType.EMISSION_NEBULA: self.dso_nebula,
1259
+ DsoType.STAR_CLUSTER_NEBULA: self.dso_nebula,
1260
+ DsoType.REFLECTION_NEBULA: self.dso_nebula,
1261
+ DsoType.HII_IONIZED_REGION: self.dso_nebula,
1262
+ # Stars ----------
1263
+ DsoType.STAR: self.star,
1264
+ DsoType.DOUBLE_STAR: self.dso_double_star,
1265
+ DsoType.ASSOCIATION_OF_STARS: self.dso_association_stars,
1266
+ # Others ----------
1267
+ DsoType.DARK_NEBULA: self.dso_dark_nebula,
1268
+ DsoType.SUPERNOVA_REMNANT: self.dso_supernova_remnant,
1269
+ DsoType.NOVA_STAR: self.dso_nova_star,
1270
+ DsoType.NONEXISTENT: self.dso_nonexistant,
1271
+ DsoType.UNKNOWN: self.dso_unknown,
1272
+ DsoType.DUPLICATE_RECORD: self.dso_duplicate,
1273
+ }
1274
+ return styles_by_type.get(dso_type)
1275
+
1276
+ @staticmethod
1277
+ def load_from_file(filename: str) -> "PlotStyle":
1278
+ """
1279
+ Load a style from a YAML file. The returned style is an extension of the default PlotStyle
1280
+ (see [`PlotStyle.extend`][starplot.styles.PlotStyle.extend]), so you only need to define
1281
+ properties you want to override from the default.
1282
+
1283
+ Args:
1284
+ filename: Filename of style file
1285
+
1286
+ Returns:
1287
+ PlotStyle: A new instance of a PlotStyle
1288
+ """
1289
+ with open(filename, "r") as sfile:
1290
+ style = yaml.safe_load(sfile)
1291
+ return PlotStyle().extend(style)
1292
+
1293
+ def dump_to_file(self, filename: str) -> None:
1294
+ """
1295
+ Save the style to a YAML file. ALL style properties will be written to the file.
1296
+
1297
+ Args:
1298
+ filename: Filename of style file
1299
+ """
1300
+ with open(filename, "w") as outfile:
1301
+ style_json = self.model_dump_json()
1302
+ style_yaml = yaml.dump(json.loads(style_json))
1303
+ outfile.write(style_yaml)
1304
+
1305
+ def extend(self, *args, **kwargs) -> "PlotStyle":
1306
+ """
1307
+ Adds one or more dicts of style overrides to the style and returns a new instance with
1308
+ those overrides.
1309
+
1310
+ Styles are added in sequential order, so if the first style arg has a property
1311
+ that is also in the last style arg, then the resulting style will have the value
1312
+ from the last style (similar to how CSS works).
1313
+
1314
+ ???- tip "Example Usage"
1315
+ Create an extension of the default style with the light blue color scheme, map optimizations,
1316
+ and change the constellation line color to red:
1317
+
1318
+ ```python
1319
+
1320
+ new_style = PlotStyle().extend(
1321
+ styles.extensions.BLUE_LIGHT,
1322
+ styles.extensions.MAP,
1323
+ {
1324
+ "constellation": {"line": {"color": "#e12d2d"}},
1325
+ },
1326
+ )
1327
+ ```
1328
+
1329
+ Args:
1330
+ args: One or more dicts of styles to add
1331
+
1332
+ Returns:
1333
+ PlotStyle: A new instance of a PlotStyle
1334
+ """
1335
+ style_json = self.model_dump_json()
1336
+ style_dict = json.loads(style_json)
1337
+ for a in args:
1338
+ if not isinstance(a, dict):
1339
+ raise TypeError("Style overrides must be dictionary types.")
1340
+ merge_dict(style_dict, a)
1341
+ return PlotStyle.parse_obj(style_dict)
1342
+
1343
+ def has_gradient_background(self):
1344
+ return isinstance(self.background_color, list)