starplot 0.18.1__py2.py3-none-any.whl → 0.19.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. starplot/__init__.py +33 -27
  2. starplot/cli.py +5 -8
  3. starplot/config.py +1 -0
  4. starplot/data/__init__.py +3 -5
  5. starplot/data/catalogs.py +23 -11
  6. starplot/data/db.py +1 -7
  7. starplot/data/library/constellation_names.parquet +0 -0
  8. starplot/data/library/dso_names.parquet +0 -0
  9. starplot/data/library/star_designations.parquet +0 -0
  10. starplot/data/translations.py +45 -0
  11. starplot/models/__init__.py +3 -1
  12. starplot/models/constellation.py +7 -1
  13. starplot/models/milky_way.py +30 -0
  14. starplot/models/observer.py +11 -2
  15. starplot/plots/__init__.py +6 -0
  16. starplot/{base.py → plots/base.py} +74 -439
  17. starplot/{horizon.py → plots/horizon.py} +12 -10
  18. starplot/{map.py → plots/map.py} +10 -7
  19. starplot/{optic.py → plots/optic.py} +21 -30
  20. starplot/{zenith.py → plots/zenith.py} +31 -8
  21. starplot/plotters/__init__.py +9 -7
  22. starplot/plotters/arrow.py +1 -1
  23. starplot/plotters/constellations.py +46 -61
  24. starplot/plotters/dsos.py +33 -16
  25. starplot/plotters/experimental.py +0 -1
  26. starplot/plotters/milkyway.py +15 -6
  27. starplot/plotters/stars.py +19 -36
  28. starplot/plotters/text.py +464 -0
  29. starplot/styles/__init__.py +4 -4
  30. starplot/styles/base.py +20 -18
  31. starplot/styles/ext/antique.yml +2 -2
  32. starplot/styles/ext/blue_dark.yml +0 -1
  33. starplot/styles/ext/color_print.yml +2 -2
  34. {starplot-0.18.1.dist-info → starplot-0.19.0.dist-info}/METADATA +5 -3
  35. {starplot-0.18.1.dist-info → starplot-0.19.0.dist-info}/RECORD +38 -36
  36. starplot/data/library/sky.db +0 -0
  37. {starplot-0.18.1.dist-info → starplot-0.19.0.dist-info}/WHEEL +0 -0
  38. {starplot-0.18.1.dist-info → starplot-0.19.0.dist-info}/entry_points.txt +0 -0
  39. {starplot-0.18.1.dist-info → starplot-0.19.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,464 @@
1
+ from dataclasses import dataclass
2
+ from random import randrange
3
+
4
+ import numpy as np
5
+ import rtree
6
+ from shapely import Point
7
+ from matplotlib.text import Text
8
+
9
+ from starplot.config import settings as StarplotSettings, SvgTextType
10
+ from starplot.styles import AnchorPointEnum, LabelStyle
11
+ from starplot.styles.helpers import use_style
12
+ from starplot.geometry import (
13
+ unwrap_polygon_360,
14
+ random_point_in_polygon_at_distance,
15
+ )
16
+
17
+ """
18
+ Long term strategy:
19
+
20
+ - plot all markers FIRST (but keep track of labels)
21
+ - on export, find best positions for labels
22
+ - introduce some "priority" for labels (e.g. order by)
23
+
24
+ """
25
+
26
+ BBox = tuple[float, float, float, float]
27
+
28
+
29
+ @dataclass
30
+ class CollisionHandler:
31
+ """
32
+ Dataclass that describes how to handle label collisions with other objects, like text, markers, constellation lines, etc.
33
+ """
34
+
35
+ allow_clipped: bool = False
36
+ """If True, then labels will be plotted if they're clipped (i.e. part of the label is outside the plot area)"""
37
+
38
+ allow_label_collisions: bool = False
39
+ """If True, then labels will be plotted if they collide with another label"""
40
+
41
+ allow_marker_collisions: bool = False
42
+ """If True, then labels will be plotted if they collide with another marker"""
43
+
44
+ allow_constellation_line_collisions: bool = False
45
+ """If True, then labels will be plotted if they collide with a constellation line"""
46
+
47
+ plot_on_fail: bool = False
48
+ """If True, then labels will be plotted even if no allowed position is found. They will be plotted at their last attempted position."""
49
+
50
+ attempts: int = 500
51
+ """Max attempts to find a good label position"""
52
+
53
+ seed: int = None
54
+ """Random seed for randomly generating points"""
55
+
56
+ anchor_fallbacks: list[AnchorPointEnum] = None
57
+ """
58
+ If a point-based label's preferred anchor point results in a collision, then these fallbacks will be tried in
59
+ sequence until a collision-free position is found.
60
+
61
+ Default:
62
+ ```python
63
+ [
64
+ AnchorPointEnum.BOTTOM_RIGHT,
65
+ AnchorPointEnum.TOP_LEFT,
66
+ AnchorPointEnum.TOP_RIGHT,
67
+ AnchorPointEnum.BOTTOM_LEFT,
68
+ AnchorPointEnum.BOTTOM_CENTER,
69
+ AnchorPointEnum.TOP_CENTER,
70
+ AnchorPointEnum.RIGHT_CENTER,
71
+ AnchorPointEnum.LEFT_CENTER,
72
+ ]
73
+ ```
74
+ """
75
+
76
+ def __post_init__(self):
77
+ self.anchor_fallbacks = self.anchor_fallbacks or [
78
+ AnchorPointEnum.BOTTOM_RIGHT,
79
+ AnchorPointEnum.TOP_LEFT,
80
+ AnchorPointEnum.TOP_RIGHT,
81
+ AnchorPointEnum.BOTTOM_LEFT,
82
+ AnchorPointEnum.BOTTOM_CENTER,
83
+ AnchorPointEnum.TOP_CENTER,
84
+ AnchorPointEnum.RIGHT_CENTER,
85
+ AnchorPointEnum.LEFT_CENTER,
86
+ ]
87
+
88
+
89
+ class TextPlotterMixin:
90
+ def __init__(self, *args, **kwargs):
91
+ self.labels = []
92
+ self._labels_rtree = rtree.index.Index()
93
+ self._constellations_rtree = rtree.index.Index()
94
+ self._stars_rtree = rtree.index.Index()
95
+ self._markers_rtree = rtree.index.Index()
96
+ self.collision_handler = kwargs.pop("collision_handler", CollisionHandler())
97
+
98
+ def _is_label_collision(self, bbox: BBox) -> bool:
99
+ ix = list(self._labels_rtree.intersection(bbox))
100
+ return len(ix) > 0
101
+
102
+ def _is_constellation_collision(self, bbox: BBox) -> bool:
103
+ ix = list(self._constellations_rtree.intersection(bbox))
104
+ return len(ix) > 0
105
+
106
+ def _is_star_collision(self, bbox: BBox) -> bool:
107
+ ix = list(self._stars_rtree.intersection(bbox))
108
+ return len(ix) > 0
109
+
110
+ def _is_marker_collision(self, bbox: BBox) -> bool:
111
+ ix = list(self._markers_rtree.intersection(bbox))
112
+ return len(ix) > 0
113
+
114
+ def _is_clipped(self, points) -> bool:
115
+ p = self._clip_path_polygon
116
+
117
+ for x, y in points:
118
+ if not p.contains(Point(x, y)):
119
+ return True
120
+
121
+ return False
122
+
123
+ def _add_label_to_rtree(self, label: Text, extent=None):
124
+ extent = extent or label.get_window_extent(
125
+ renderer=self.fig.canvas.get_renderer()
126
+ )
127
+ self.labels.append(label)
128
+ self._labels_rtree.insert(
129
+ 0, (extent.x0 - 1, extent.y0 - 1, extent.x1 + 1, extent.y1 + 1)
130
+ )
131
+
132
+ def _is_open_space(
133
+ self,
134
+ bbox: BBox,
135
+ padding=0,
136
+ avoid_clipped=True,
137
+ avoid_label_collisions=True,
138
+ avoid_marker_collisions=True,
139
+ avoid_constellation_collision=True,
140
+ ) -> bool:
141
+ """
142
+ Returns true if the boox covers an open space (i.e. no collisions)
143
+
144
+ Args:
145
+ bbox: 4-element tuple of lower left and upper right coordinates
146
+ """
147
+ x0, y0, x1, y1 = bbox
148
+ points = [(x0, y0), (x1, y1)]
149
+ bbox = (
150
+ x0 - padding,
151
+ y0 - padding,
152
+ x1 + padding,
153
+ y1 + padding,
154
+ )
155
+
156
+ if any([np.isnan(c) for c in (x0, y0, x1, y1)]):
157
+ return False
158
+
159
+ if avoid_clipped and self._is_clipped(points):
160
+ return False
161
+
162
+ if avoid_label_collisions and self._is_label_collision(bbox):
163
+ return False
164
+
165
+ if avoid_marker_collisions and (
166
+ self._is_star_collision(bbox) or self._is_marker_collision(bbox)
167
+ ):
168
+ return False
169
+
170
+ if avoid_constellation_collision and self._is_constellation_collision(bbox):
171
+ return False
172
+
173
+ return True
174
+
175
+ def _get_label_bbox(self, label: Text) -> BBox:
176
+ extent = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
177
+ return (float(extent.x0), float(extent.y0), float(extent.x1), float(extent.y1))
178
+
179
+ def _maybe_remove_label(
180
+ self,
181
+ label: Text,
182
+ collision_handler: CollisionHandler,
183
+ ) -> bool:
184
+ """Returns true if the label is removed, else false"""
185
+ extent = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
186
+ bbox = (float(extent.x0), float(extent.y0), float(extent.x1), float(extent.y1))
187
+ points = [(extent.x0, extent.y0), (extent.x1, extent.y1)]
188
+
189
+ if any([np.isnan(c) for c in bbox]):
190
+ label.remove()
191
+ return True
192
+
193
+ if not collision_handler.allow_clipped and self._is_clipped(points):
194
+ label.remove()
195
+ return True
196
+
197
+ if not collision_handler.allow_label_collisions and self._is_label_collision(
198
+ bbox
199
+ ):
200
+ label.remove()
201
+ return True
202
+
203
+ if not collision_handler.allow_marker_collisions and (
204
+ self._is_star_collision(bbox) or self._is_marker_collision(bbox)
205
+ ):
206
+ label.remove()
207
+ return True
208
+
209
+ if (
210
+ not collision_handler.allow_constellation_line_collisions
211
+ and self._is_constellation_collision(bbox)
212
+ ):
213
+ label.remove()
214
+ return True
215
+
216
+ return False
217
+
218
+ def _text(self, x, y, text, **kwargs) -> Text:
219
+ """Plots text at (x, y)"""
220
+ label = self.ax.annotate(
221
+ text,
222
+ (x, y),
223
+ **kwargs,
224
+ **self._plot_kwargs(),
225
+ )
226
+ if kwargs.get("clip_on"):
227
+ label.set_clip_on(True)
228
+ label.set_clip_path(self._background_clip_path)
229
+ return label
230
+
231
+ def _text_point(
232
+ self,
233
+ ra: float,
234
+ dec: float,
235
+ text: str,
236
+ collision_handler: CollisionHandler,
237
+ **kwargs,
238
+ ) -> Text | None:
239
+ if not text:
240
+ return None
241
+
242
+ x, y = self._prepare_coords(ra, dec)
243
+
244
+ if StarplotSettings.svg_text_type == SvgTextType.PATH:
245
+ kwargs["path_effects"] = kwargs.get("path_effects", [self.text_border])
246
+
247
+ original_va = kwargs.pop("va", None)
248
+ original_ha = kwargs.pop("ha", None)
249
+ original_offset_x, original_offset_y = kwargs.pop("xytext", (0, 0))
250
+ attempts = 0
251
+
252
+ anchors = [(original_va, original_ha)]
253
+ for a in collision_handler.anchor_fallbacks:
254
+ d = AnchorPointEnum.from_str(a).as_matplot()
255
+ anchors.append((d["va"], d["ha"]))
256
+
257
+ for va, ha in anchors:
258
+ attempts += 1
259
+ offset_x, offset_y = original_offset_x, original_offset_y
260
+ if original_ha != ha:
261
+ offset_x *= -1
262
+
263
+ if original_va != va:
264
+ offset_y *= -1
265
+
266
+ if ha == "center":
267
+ offset_x = 0
268
+ offset_y = 0
269
+
270
+ label = self._text(
271
+ x, y, text, va=va, ha=ha, xytext=(offset_x, offset_y), **kwargs
272
+ )
273
+
274
+ if (
275
+ collision_handler.plot_on_fail
276
+ and label
277
+ and (attempts == collision_handler.attempts or attempts == len(anchors))
278
+ ):
279
+ self._add_label_to_rtree(label)
280
+ return label
281
+
282
+ removed = self._maybe_remove_label(label, collision_handler)
283
+
284
+ if not removed:
285
+ self._add_label_to_rtree(label)
286
+ return label
287
+ elif attempts == collision_handler.attempts or attempts == len(anchors):
288
+ return None
289
+
290
+ # from matplotlib.patches import Rectangle
291
+ # bbox = label.get_window_extent(renderer=self.fig.canvas.get_renderer())
292
+ # bbox = bbox.transformed(self.ax.transAxes.inverted())
293
+ # # bbox = bbox.padded(1)
294
+ # # bbox = bbox.expanded(1, 2)
295
+ # rect = Rectangle(
296
+ # # Bbox(x0=0.19034844035799406, y0=0.8351746595026188, x1=0.20519408725358892, y1=0.8615984521601776)
297
+ # (bbox.x0, bbox.y0), # (x, y) position in display pixels
298
+ # width=bbox.width,
299
+ # height=bbox.height,
300
+ # transform=self.ax.transAxes,
301
+ # fill=False,
302
+ # facecolor='none',
303
+ # edgecolor='red',
304
+ # linewidth=1,
305
+ # alpha=1,
306
+ # zorder=100_000,
307
+ # )
308
+ # self.ax.add_patch(rect)
309
+
310
+ def _text_area(
311
+ self,
312
+ ra: float,
313
+ dec: float,
314
+ text: str,
315
+ area,
316
+ collision_handler: CollisionHandler,
317
+ **kwargs,
318
+ ) -> Text | None:
319
+ kwargs["va"] = "center"
320
+ kwargs["ha"] = "center"
321
+
322
+ if StarplotSettings.svg_text_type == SvgTextType.PATH:
323
+ kwargs["path_effects"] = kwargs.get("path_effects", [self.text_border])
324
+
325
+ padding = 6
326
+ max_distance = 3_000
327
+ distance_step_size = 2
328
+ attempts = 0
329
+ height = None
330
+ width = None
331
+ bbox = None
332
+ areas = (
333
+ [p for p in area.geoms] if "MultiPolygon" == str(area.geom_type) else [area]
334
+ )
335
+ new_areas = []
336
+ origin = Point(ra, dec)
337
+
338
+ for a in areas:
339
+ unwrapped = unwrap_polygon_360(a)
340
+ # new_buffer = unwrapped.area / 10 * -1 * buffer * self.scale
341
+ # new_buffer = -1 * buffer * self.scale
342
+ # new_poly = unwrapped.buffer(new_buffer)
343
+ new_areas.append(unwrapped)
344
+
345
+ for d in range(0, max_distance, distance_step_size):
346
+ distance = d / 20
347
+ poly = randrange(len(new_areas))
348
+ point = random_point_in_polygon_at_distance(
349
+ new_areas[poly],
350
+ origin_point=origin,
351
+ distance=distance,
352
+ max_iterations=10,
353
+ seed=collision_handler.seed,
354
+ )
355
+
356
+ if point is None:
357
+ continue
358
+
359
+ x, y = self._prepare_coords(point.x, point.y)
360
+
361
+ if height and width:
362
+ data_xy = self._proj.transform_point(x, y, self._crs)
363
+ display_x, display_y = self.ax.transData.transform(data_xy)
364
+ bbox = (
365
+ display_x - width / 2,
366
+ display_y - height / 2,
367
+ display_x + width / 2,
368
+ display_y + height / 2,
369
+ )
370
+ label = None
371
+
372
+ else:
373
+ label = self._text(x, y, text, **kwargs)
374
+ bbox = self._get_label_bbox(label)
375
+ height = bbox[3] - bbox[1]
376
+ width = bbox[2] - bbox[0]
377
+
378
+ is_open = self._is_open_space(
379
+ bbox,
380
+ padding=padding,
381
+ avoid_clipped=not collision_handler.allow_clipped,
382
+ avoid_constellation_collision=not collision_handler.allow_constellation_line_collisions,
383
+ avoid_marker_collisions=not collision_handler.allow_marker_collisions,
384
+ avoid_label_collisions=not collision_handler.allow_label_collisions,
385
+ )
386
+
387
+ # # TODO : remove label if not fully inside area?
388
+
389
+ attempts += 1
390
+
391
+ if is_open and label is None:
392
+ label = self._text(x, y, text, **kwargs)
393
+
394
+ if is_open or (
395
+ collision_handler.plot_on_fail
396
+ and attempts == collision_handler.attempts
397
+ ):
398
+ self._add_label_to_rtree(label)
399
+ return label
400
+
401
+ elif label is not None:
402
+ label.remove()
403
+
404
+ elif attempts == collision_handler.attempts:
405
+ return None
406
+
407
+ @use_style(LabelStyle)
408
+ def text(
409
+ self,
410
+ text: str,
411
+ ra: float,
412
+ dec: float,
413
+ style: LabelStyle = None,
414
+ collision_handler: CollisionHandler = None,
415
+ **kwargs,
416
+ ):
417
+ """
418
+ Plots text
419
+
420
+ Args:
421
+ text: Text to plot
422
+ ra: Right ascension of text (0...360)
423
+ dec: Declination of text (-90...90)
424
+ style: Styling of the text
425
+ collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on collisions with other labels, markers, etc. If `None`, then the collision handler of the plot will be used.
426
+ """
427
+ if not text:
428
+ return
429
+
430
+ style = style.model_copy() # need a copy because we possibly mutate it below
431
+
432
+ collision_handler = collision_handler or self.collision_handler
433
+
434
+ if style.offset_x == "auto":
435
+ style.offset_x = 0
436
+
437
+ if style.offset_y == "auto":
438
+ style.offset_y = 0
439
+
440
+ if kwargs.get("area"):
441
+ return self._text_area(
442
+ ra,
443
+ dec,
444
+ text,
445
+ **style.matplot_kwargs(self.scale),
446
+ area=kwargs.pop("area"),
447
+ collision_handler=collision_handler,
448
+ xycoords="data",
449
+ xytext=(style.offset_x * self.scale, style.offset_y * self.scale),
450
+ textcoords="offset points",
451
+ **kwargs,
452
+ )
453
+ else:
454
+ return self._text_point(
455
+ ra,
456
+ dec,
457
+ text,
458
+ **style.matplot_kwargs(self.scale),
459
+ collision_handler=collision_handler,
460
+ xycoords="data",
461
+ xytext=(style.offset_x * self.scale, style.offset_y * self.scale),
462
+ textcoords="offset points",
463
+ **kwargs,
464
+ )
@@ -1,6 +1,6 @@
1
- from .base import * # noqa: F401,F403
2
- from .helpers import * # noqa: F401,F403
1
+ # ruff: noqa: F401,F403
3
2
 
4
- # from .extensions import * # noqa: F401
3
+ from .base import *
4
+ from .helpers import *
5
5
 
6
- import starplot.styles.extensions as style_extensions # noqa: F401
6
+ import starplot.styles.extensions as style_extensions
starplot/styles/base.py CHANGED
@@ -2,7 +2,7 @@ import json
2
2
 
3
3
  from enum import Enum
4
4
  from pathlib import Path
5
- from typing import Optional, Union, List
5
+ from typing import Optional, Union
6
6
 
7
7
  import yaml
8
8
 
@@ -90,7 +90,7 @@ class FontWeightEnum(int, Enum):
90
90
 
91
91
  THIN = 100
92
92
  EXTRA_LIGHT = 200
93
- LIGHT = 300
93
+ # LIGHT = 300 # matplotlib's font dict doesn't have 300?
94
94
  NORMAL = 400
95
95
  MEDIUM = 500
96
96
  SEMI_BOLD = 600
@@ -98,6 +98,20 @@ class FontWeightEnum(int, Enum):
98
98
  EXTRA_BOLD = 800
99
99
  HEAVY = 900
100
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
+
101
115
 
102
116
  class FontStyleEnum(str, Enum):
103
117
  NORMAL = "normal"
@@ -598,7 +612,7 @@ class LabelStyle(BaseStyle):
598
612
  fontsize=self.font_size * scale,
599
613
  fontstyle=self.font_style,
600
614
  fontname=self.font_name,
601
- weight=self.font_weight,
615
+ weight=FontWeightEnum(self.font_weight).as_matplot(),
602
616
  alpha=self.font_alpha,
603
617
  zorder=self.zorder,
604
618
  )
@@ -744,7 +758,7 @@ class LegendStyle(BaseStyle):
744
758
  framealpha=self.background_alpha,
745
759
  prop={
746
760
  "family": self.font_name,
747
- "weight": self.font_weight,
761
+ "weight": FontWeightEnum(self.font_weight),
748
762
  "size": self.font_size * scale,
749
763
  },
750
764
  labelcolor=self.font_color.as_hex(),
@@ -799,18 +813,6 @@ class PlotStyle(BaseStyle):
799
813
 
800
814
  text_border_color: ColorStr = ColorStr("#fff")
801
815
 
802
- text_anchor_fallbacks: List[AnchorPointEnum] = [
803
- AnchorPointEnum.BOTTOM_RIGHT,
804
- AnchorPointEnum.TOP_LEFT,
805
- AnchorPointEnum.TOP_RIGHT,
806
- AnchorPointEnum.BOTTOM_LEFT,
807
- AnchorPointEnum.BOTTOM_CENTER,
808
- AnchorPointEnum.TOP_CENTER,
809
- AnchorPointEnum.RIGHT_CENTER,
810
- AnchorPointEnum.LEFT_CENTER,
811
- ]
812
- """If a label's preferred anchor point results in a collision, then these fallbacks will be tried in sequence until a collision-free position is found."""
813
-
814
816
  # Borders
815
817
  border_font_size: int = 18
816
818
  border_font_weight: FontWeightEnum = FontWeightEnum.BOLD
@@ -858,7 +860,7 @@ class PlotStyle(BaseStyle):
858
860
 
859
861
  bayer_labels: LabelStyle = LabelStyle(
860
862
  font_size=21,
861
- font_weight=FontWeightEnum.LIGHT,
863
+ font_weight=FontWeightEnum.EXTRA_LIGHT,
862
864
  font_name="GFS Didot",
863
865
  zorder=ZOrderEnum.LAYER_4,
864
866
  anchor_point=AnchorPointEnum.TOP_LEFT,
@@ -1158,7 +1160,7 @@ class PlotStyle(BaseStyle):
1158
1160
  label=LabelStyle(
1159
1161
  font_size=22,
1160
1162
  font_color="#999",
1161
- font_weight=FontWeightEnum.LIGHT,
1163
+ font_weight=FontWeightEnum.EXTRA_LIGHT,
1162
1164
  font_alpha=0.65,
1163
1165
  zorder=ZOrderEnum.LAYER_3,
1164
1166
  ),
@@ -52,7 +52,7 @@ constellation_lines:
52
52
  alpha: 0.3
53
53
  color: hsl(48, 80%, 14%)
54
54
  constellation_labels:
55
- font_weight: 300
55
+ font_weight: 200
56
56
  font_color: hsl(60, 3%, 52%)
57
57
  font_alpha: 0.46
58
58
  font_name: "serif"
@@ -77,7 +77,7 @@ gridlines:
77
77
  font_alpha: 0.8
78
78
  font_color: hsl(60, 3%, 92%)
79
79
  font_size: 8
80
- font_weight: 300
80
+ font_weight: 200
81
81
  line:
82
82
  alpha: 0.6
83
83
  color: hsl(68, 11%, 71%)
@@ -45,7 +45,6 @@ constellation_lines:
45
45
  constellation_labels:
46
46
  font_alpha: 0.37
47
47
  font_color: hsl(209, 23%, 80%)
48
- font_weight: 300
49
48
  constellation_borders:
50
49
  width: 2
51
50
  color: hsl(209, 50%, 74%)
@@ -24,7 +24,7 @@ celestial_equator:
24
24
  constellation:
25
25
  label:
26
26
  font_size: 4
27
- font_weight: 300
27
+ font_weight: 200
28
28
  font_color: '#8c8c8c'
29
29
  font_alpha: 0.1
30
30
  line:
@@ -51,7 +51,7 @@ gridlines:
51
51
  font_alpha: 1
52
52
  font_color: hsl(211, 80%, 97%)
53
53
  font_size: 18
54
- font_weight: 300
54
+ font_weight: 200
55
55
  line:
56
56
  alpha: 0.4
57
57
  color: '#888'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: starplot
3
- Version: 0.18.1
3
+ Version: 0.19.0
4
4
  Summary: Star charts and maps of the sky
5
5
  Keywords: astronomy,stars,charts,maps,constellations,sky,plotting
6
6
  Author-email: Steve Berardi <hello@steveberardi.com>
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.10
11
11
  Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Framework :: Matplotlib
14
15
  License-File: LICENSE
15
16
  Requires-Dist: matplotlib >= 3.8.0
16
17
  Requires-Dist: numpy >= 1.26.2
@@ -39,7 +40,7 @@ Project-URL: Source, https://github.com/steveberardi/starplot
39
40
  ![License](https://img.shields.io/github/license/steveberardi/starplot?style=for-the-badge&color=8b63b0)
40
41
  ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/steveberardi/starplot/test.yml?style=for-the-badge&color=88b063)
41
42
 
42
- **Starplot** is a Python library for creating star charts and maps of the sky.
43
+ **Starplot** is a Python library for creating star charts and maps of the sky
43
44
 
44
45
  - 🗺️ **Maps** - including 10+ customizable projections
45
46
  - ⭐ **Zenith Charts** - shows the entire sky at a specific time and place
@@ -52,7 +53,7 @@ Project-URL: Source, https://github.com/steveberardi/starplot
52
53
  - 🚀 **Data Backend** - powered by DuckDB + Ibis for fast object lookup
53
54
  - 📓 **Custom Data Catalogs** - with helpers for building and optimizing
54
55
  - 🧭 **Label Collision Avoidance** - ensuring all labels are readable
55
- - 🌐 **Localization** - label translations for French, Chinese, and Persian (coming soon!)
56
+ - 🌐 **Localization** - label translations for Chinese, French, Lithuanian, Persian, and Spanish
56
57
 
57
58
  ## Examples
58
59
  *Zenith chart of the stars from a specific time/location:*
@@ -135,6 +136,7 @@ See more details on the [Public Roadmap](https://trello.com/b/sUksygn4/starplot-
135
136
  - [starplot-hyg](https://github.com/steveberardi/starplot-hyg)
136
137
  - [starplot-gaia-dr3](https://github.com/steveberardi/starplot-gaia-dr3)
137
138
  - [starplot-hyperleda](https://github.com/steveberardi/starplot-hyperleda)
139
+ - [starplot-milkyway](https://github.com/steveberardi/starplot-milkyway)
138
140
 
139
141
  ## License
140
142
  [MIT License](https://github.com/steveberardi/starplot/blob/main/LICENSE)