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.
- starplot/__init__.py +33 -27
- starplot/cli.py +5 -8
- starplot/config.py +1 -0
- starplot/data/__init__.py +3 -5
- starplot/data/catalogs.py +23 -11
- starplot/data/db.py +1 -7
- starplot/data/library/constellation_names.parquet +0 -0
- starplot/data/library/dso_names.parquet +0 -0
- starplot/data/library/star_designations.parquet +0 -0
- starplot/data/translations.py +45 -0
- starplot/models/__init__.py +3 -1
- starplot/models/constellation.py +7 -1
- starplot/models/milky_way.py +30 -0
- starplot/models/observer.py +11 -2
- starplot/plots/__init__.py +6 -0
- starplot/{base.py → plots/base.py} +74 -439
- starplot/{horizon.py → plots/horizon.py} +12 -10
- starplot/{map.py → plots/map.py} +10 -7
- starplot/{optic.py → plots/optic.py} +21 -30
- starplot/{zenith.py → plots/zenith.py} +31 -8
- starplot/plotters/__init__.py +9 -7
- starplot/plotters/arrow.py +1 -1
- starplot/plotters/constellations.py +46 -61
- starplot/plotters/dsos.py +33 -16
- starplot/plotters/experimental.py +0 -1
- starplot/plotters/milkyway.py +15 -6
- starplot/plotters/stars.py +19 -36
- starplot/plotters/text.py +464 -0
- starplot/styles/__init__.py +4 -4
- starplot/styles/base.py +20 -18
- starplot/styles/ext/antique.yml +2 -2
- starplot/styles/ext/blue_dark.yml +0 -1
- starplot/styles/ext/color_print.yml +2 -2
- {starplot-0.18.1.dist-info → starplot-0.19.0.dist-info}/METADATA +5 -3
- {starplot-0.18.1.dist-info → starplot-0.19.0.dist-info}/RECORD +38 -36
- starplot/data/library/sky.db +0 -0
- {starplot-0.18.1.dist-info → starplot-0.19.0.dist-info}/WHEEL +0 -0
- {starplot-0.18.1.dist-info → starplot-0.19.0.dist-info}/entry_points.txt +0 -0
- {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
|
+
)
|
starplot/styles/__init__.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
from .helpers import * # noqa: F401,F403
|
|
1
|
+
# ruff: noqa: F401,F403
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
from .base import *
|
|
4
|
+
from .helpers import *
|
|
5
5
|
|
|
6
|
-
import starplot.styles.extensions as style_extensions
|
|
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
|
|
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.
|
|
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.
|
|
1163
|
+
font_weight=FontWeightEnum.EXTRA_LIGHT,
|
|
1162
1164
|
font_alpha=0.65,
|
|
1163
1165
|
zorder=ZOrderEnum.LAYER_3,
|
|
1164
1166
|
),
|
starplot/styles/ext/antique.yml
CHANGED
|
@@ -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:
|
|
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:
|
|
80
|
+
font_weight: 200
|
|
81
81
|
line:
|
|
82
82
|
alpha: 0.6
|
|
83
83
|
color: hsl(68, 11%, 71%)
|
|
@@ -24,7 +24,7 @@ celestial_equator:
|
|
|
24
24
|
constellation:
|
|
25
25
|
label:
|
|
26
26
|
font_size: 4
|
|
27
|
-
font_weight:
|
|
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:
|
|
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.
|
|
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
|

|
|
40
41
|

|
|
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,
|
|
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)
|