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,174 @@
1
+ import random
2
+
3
+ import numpy as np
4
+
5
+ from matplotlib import patches
6
+ from shapely import LineString, Polygon, affinity, ops
7
+
8
+ from starplot.geometry import circle
9
+ from starplot.profile import profile
10
+ from starplot.coordinates import CoordinateSystem
11
+ from starplot.styles import ArrowStyle
12
+ from starplot.styles.helpers import use_style
13
+ from starplot.utils import points_on_line
14
+
15
+
16
+ class ArrowPlotterMixin:
17
+ def _to_axes(self, points):
18
+ ax_points = []
19
+
20
+ for ra, dec in points:
21
+ x, y = self._proj.transform_point(ra, dec, self._crs)
22
+ data_to_axes = self.ax.transData + self.ax.transAxes.inverted()
23
+ x_axes, y_axes = data_to_axes.transform((x, y))
24
+ ax_points.append([x_axes, y_axes])
25
+ return ax_points
26
+
27
+ def _to_display(self, points):
28
+ display_points = []
29
+
30
+ for ra, dec in points:
31
+ if self._coordinate_system == CoordinateSystem.RA_DEC:
32
+ ra *= -1
33
+
34
+ data_x, data_y = self._proj.transform_point(ra, dec, self._geodetic)
35
+ if np.isnan(data_x) or np.isnan(data_y):
36
+ continue
37
+
38
+ display_x, display_y = self.ax.transData.transform((data_x, data_y))
39
+ display_points.append((display_x, display_y))
40
+
41
+ return display_points
42
+
43
+ @profile
44
+ @use_style(ArrowStyle, "arrow")
45
+ def arrow(
46
+ self,
47
+ origin: tuple[float, float] = None,
48
+ target: tuple[float, float] = None,
49
+ style: ArrowStyle = None,
50
+ scale: float = 0.99,
51
+ length: float = 5,
52
+ max_attempts: int = 100,
53
+ ):
54
+ """
55
+ Plots an arrow from one point to another if you specify both `origin` and `target`. If you only specify a `target`, then the arrow will serve as a "pointer" to that target.
56
+
57
+ Args:
58
+ origin: Starting point (ra, dec)
59
+ target: Target of the arrow (ra, dec)
60
+ style: Style of the arrow
61
+ scale: Scaling factor for the arrow, to make it offset from the origin/target
62
+ length: If you only specify a target, then this will be the length of the arrow (in degrees). This value is ignored if you're plotting an arrow from one point to another.
63
+ max_attempts: If you only specify a target, then this will be the max number of attempts for plotting the arrow without colliding with labels. Arrow will be plotted on the final attempt.
64
+
65
+ """
66
+
67
+ def create_arrow(p0, p1):
68
+ ax_coords = self._to_axes([origin, target])
69
+ x, y = ax_coords[0]
70
+ target_x, target_y = ax_coords[1]
71
+
72
+ # angle to rotate head
73
+ angle_radians = np.atan2(target_y - y, target_x - x)
74
+ angle_degrees = np.degrees(angle_radians)
75
+
76
+ line_x = [
77
+ (1 - scale) * ax_coords[1][0] + scale * ax_coords[0][0],
78
+ (1 - scale) * ax_coords[0][0] + scale * ax_coords[1][0],
79
+ ]
80
+ line_y = [
81
+ (1 - scale) * ax_coords[1][1] + scale * ax_coords[0][1],
82
+ (1 - scale) * ax_coords[0][1] + scale * ax_coords[1][1],
83
+ ]
84
+
85
+ arrow_x = line_x[-1]
86
+ arrow_y = line_y[-1]
87
+
88
+ body_width = style.body_width / self.resolution * 0.5
89
+ head_width = style.head_width / self.resolution * 0.5
90
+ head_height = style.head_height / self.resolution * 0.5
91
+
92
+ arrow_line = LineString(list(zip(line_x, line_y)))
93
+ arrow_body = arrow_line.buffer(body_width, **style.shapely_kwargs())
94
+
95
+ arrow_head = Polygon(
96
+ [
97
+ (arrow_x, arrow_y),
98
+ (arrow_x + head_width, arrow_y - head_height),
99
+ (arrow_x - head_width, arrow_y - head_height),
100
+ (arrow_x, arrow_y),
101
+ ]
102
+ )
103
+
104
+ arrow_head = affinity.rotate(
105
+ arrow_head,
106
+ angle_degrees + 270,
107
+ origin=(arrow_x, arrow_y),
108
+ )
109
+
110
+ result = ops.split(arrow_line, arrow_head)
111
+ arrow_body = result.geoms[0].buffer(body_width, **style.shapely_kwargs())
112
+ return arrow_body.union(arrow_head.buffer(0.0001, **style.shapely_kwargs()))
113
+
114
+ if origin and target:
115
+ origin = self._prepare_coords(*origin)
116
+ target = self._prepare_coords(*target)
117
+ arrow_polygon = create_arrow(origin, target)
118
+
119
+ elif target:
120
+ # we only need to prepare the target coords because the origin will be
121
+ # based on the circle around the prepared target coords (so the origin will be automatically in the expected coord system)
122
+ target = self._prepare_coords(*target)
123
+ arrow_polygon = None
124
+ attempts = 0
125
+ padding = 8
126
+ polygon = circle(
127
+ center=target,
128
+ diameter_degrees=length * 2,
129
+ num_pts=200,
130
+ )
131
+
132
+ origins = list(zip(*polygon.exterior.coords.xy))
133
+
134
+ while arrow_polygon is None and attempts < max_attempts:
135
+ attempts += 1
136
+ origin = random.choice(origins)
137
+ display_points = self._to_display([origin, target])
138
+
139
+ if len(display_points) < 2:
140
+ continue
141
+
142
+ points_arrow = points_on_line(display_points[0], display_points[1], 25)
143
+
144
+ # check if arrow body collides with any labels
145
+ collides_with_label = False
146
+ for x, y in points_arrow:
147
+ bbox = (
148
+ x - padding,
149
+ y - padding,
150
+ x + padding,
151
+ y + padding,
152
+ )
153
+ if self._is_label_collision(bbox):
154
+ collides_with_label = True
155
+ break
156
+
157
+ # if arrow body does not collide with labels, we can create polygon and exit loop
158
+ if not collides_with_label or attempts == max_attempts:
159
+ arrow_polygon = create_arrow(origin, target)
160
+
161
+ else:
162
+ raise ValueError(
163
+ "To plot an arrow you must specify a target or a target and origin."
164
+ )
165
+
166
+ patch = patches.Polygon(
167
+ list(zip(*arrow_polygon.exterior.coords.xy)),
168
+ **style.matplot_kwargs(self.scale),
169
+ transform=self.ax.transAxes,
170
+ )
171
+ self.ax.add_patch(patch)
172
+ # Need to set clip path AFTER adding patch
173
+ patch.set_clip_on(True)
174
+ patch.set_clip_path(self._background_clip_path)
@@ -0,0 +1,298 @@
1
+ from typing import Callable
2
+
3
+ import rtree
4
+ from shapely import (
5
+ MultiPoint,
6
+ )
7
+ from matplotlib.collections import LineCollection
8
+ from ibis import _
9
+
10
+ from starplot.coordinates import CoordinateSystem
11
+ from starplot.data import db, constellations as condata
12
+ from starplot.data.catalogs import (
13
+ Catalog,
14
+ CONSTELLATIONS_IAU,
15
+ CONSTELLATION_BORDERS,
16
+ BIG_SKY_MAG11,
17
+ )
18
+ from starplot.data.stars import load as load_stars
19
+ from starplot.models import Star, Constellation
20
+ from starplot.models.constellation import from_tuple
21
+ from starplot.profile import profile
22
+ from starplot.styles import LineStyle, LabelStyle
23
+ from starplot.styles.helpers import use_style
24
+ from starplot.geometry import is_wrapped_polygon, line_segment, split_line_at_meridian
25
+ from starplot.plotters.text import CollisionHandler
26
+
27
+
28
+ class ConstellationPlotterMixin:
29
+ @profile
30
+ def _prepare_constellation_stars(
31
+ self, constellations: list[Constellation]
32
+ ) -> dict[int, tuple[float, float]]:
33
+ """
34
+ Returns dictionary of stars and their position:
35
+
36
+ {hip: (x,y)}
37
+
38
+ Where (x, y) is the plotted coordinate system (RA/DEC or AZ/ALT)
39
+ """
40
+ hips = []
41
+ for c in constellations:
42
+ hips.extend(c.star_hip_ids)
43
+
44
+ results = load_stars(
45
+ catalog=BIG_SKY_MAG11,
46
+ filters=[_.hip.isin(hips)],
47
+ )
48
+ results = results.select(
49
+ "ra",
50
+ "dec",
51
+ "epoch_year",
52
+ "hip",
53
+ "parallax_mas",
54
+ "ra_mas_per_year",
55
+ "dec_mas_per_year",
56
+ )
57
+ df = results.to_pandas()
58
+ df["ra_hours"], df["dec_degrees"] = (df.ra / 15, df.dec)
59
+ df = self._prepare_star_coords(df, limit_by_altaz=False)
60
+
61
+ return {star.hip: (star.x, star.y) for star in df.itertuples()}
62
+
63
+ @profile
64
+ @use_style(LineStyle, "constellation_lines")
65
+ def constellations(
66
+ self,
67
+ style: LineStyle = None,
68
+ where: list = None,
69
+ sql: str = None,
70
+ catalog: Catalog = CONSTELLATIONS_IAU,
71
+ ):
72
+ """Plots the constellation lines **only**. To plot constellation borders and/or labels, see separate functions for them.
73
+
74
+ **Important:** If you're plotting the constellation lines, then it's good to plot them _first_, because Starplot will use the constellation lines to determine where to place labels that are plotted afterwards (labels will look better if they're not crossing a constellation line).
75
+
76
+ Args:
77
+ style: Styling of the constellations. If None, then the plot's style (specified when creating the plot) will be used
78
+ where: A list of expressions that determine which constellations to plot. See [Selecting Objects](/reference-selecting-objects/) for details.
79
+ sql: SQL query for selecting constellations (table name is `_`). This query will be applied _after_ any filters in the `where` kwarg.
80
+ catalog: The catalog of constellations to use -- see [catalogs overview](/data/overview/) for details
81
+ """
82
+ self.logger.debug("Plotting constellation lines...")
83
+
84
+ where = where or []
85
+ ctr = 0
86
+
87
+ extent = self._extent_mask()
88
+ results = condata.load(extent=extent, filters=where, sql=sql, catalog=catalog)
89
+ constellations_df = results.to_pandas()
90
+
91
+ if constellations_df.empty:
92
+ return
93
+
94
+ constellations = [from_tuple(c) for c in constellations_df.itertuples()]
95
+
96
+ constellation_points_to_index = []
97
+ lines = []
98
+ constars = self._prepare_constellation_stars(constellations)
99
+
100
+ for c in constellations:
101
+ hiplines = c.star_hip_lines
102
+ inbounds = False
103
+
104
+ for s1_hip, s2_hip in hiplines:
105
+ if not constars.get(s1_hip) or not constars.get(s2_hip):
106
+ continue
107
+
108
+ x1, y1 = constars.get(s1_hip)
109
+ x2, y2 = constars.get(s2_hip)
110
+
111
+ if x1 == x2 and y1 == y2:
112
+ continue
113
+
114
+ if x1 - x2 > 60:
115
+ x2 += 360
116
+ elif x2 - x1 > 60:
117
+ x1 += 360
118
+
119
+ if not inbounds and (
120
+ self._in_bounds_xy(x1, y1) or self._in_bounds_xy(x2, y2)
121
+ ):
122
+ inbounds = True
123
+ elif not inbounds:
124
+ continue
125
+
126
+ xy_lines = []
127
+
128
+ if x2 > 360:
129
+ xy_lines = [*split_line_at_meridian((x1, y1), (x2, y2))]
130
+ elif x1 > 360:
131
+ xy_lines = [*split_line_at_meridian((x2, y2), (x1, y1))]
132
+ else:
133
+ xy_lines = [[(x1, y1), (x2, y2)]]
134
+
135
+ data_lines = [
136
+ [self._proj.transform_point(x, y, self._crs) for x, y in line]
137
+ for line in xy_lines
138
+ ]
139
+ display_lines = [
140
+ [self.ax.transData.transform(p) for p in line]
141
+ for line in data_lines
142
+ ]
143
+
144
+ lines.extend(data_lines)
145
+
146
+ radius = style.width * self.scale if style.width else 1
147
+
148
+ for display_start, display_end in display_lines:
149
+ for x, y in line_segment(display_start, display_end, radius * 4):
150
+ if x < 0 or y < 0:
151
+ continue
152
+ bbox = (
153
+ x - radius,
154
+ y - radius,
155
+ x + radius,
156
+ y + radius,
157
+ )
158
+ if self.debug_text:
159
+ self._debug_bbox(bbox, color="#39FF14", width=0.5)
160
+
161
+ constellation_points_to_index.append((ctr, bbox, None))
162
+ ctr += 1
163
+
164
+ if inbounds:
165
+ self._objects.constellations.append(c)
166
+
167
+ line_collection = LineCollection(
168
+ lines,
169
+ clip_on=True,
170
+ clip_path=self._background_clip_path,
171
+ gid="constellations-line",
172
+ **style.matplot_line_collection_kwargs(self.scale),
173
+ )
174
+ self.ax.add_collection(line_collection)
175
+
176
+ if self._constellations_rtree.get_size() == 0:
177
+ self._constellations_rtree = rtree.index.Index(
178
+ constellation_points_to_index
179
+ )
180
+ else:
181
+ for bbox in constellation_points_to_index:
182
+ self._constellations_rtree.insert(
183
+ 0,
184
+ bbox,
185
+ None,
186
+ )
187
+
188
+ @profile
189
+ @use_style(LineStyle, "constellation_borders")
190
+ def constellation_borders(
191
+ self, style: LineStyle = None, catalog: Catalog = CONSTELLATION_BORDERS
192
+ ):
193
+ """Plots the constellation borders
194
+
195
+ Args:
196
+ style: Styling of the constellation borders. If None, then the plot's style (specified when creating the plot) will be used
197
+ catalog: Catalog to use for constellation borders
198
+ """
199
+ con = db.connect()
200
+ borders = catalog._load(connection=con, table_name="constellation_borders")
201
+ borders = borders.mutate(
202
+ geometry=_.geometry.cast("geometry"), # cast WKB to geometry type
203
+ )
204
+
205
+ extent = self._extent_mask()
206
+ borders_df = borders.filter(_.geometry.intersects(extent)).to_pandas()
207
+
208
+ if borders_df.empty:
209
+ return
210
+
211
+ border_lines = []
212
+ geometries = [line.geometry for line in borders_df.itertuples()]
213
+
214
+ for ls in geometries:
215
+ if ls.length < 360:
216
+ ls = ls.segmentize(1)
217
+
218
+ xy = [c for c in ls.coords]
219
+
220
+ if self._coordinate_system == CoordinateSystem.RA_DEC:
221
+ border_lines.append(xy)
222
+
223
+ elif self._coordinate_system == CoordinateSystem.AZ_ALT:
224
+ coords = [self._prepare_coords(*p) for p in xy]
225
+ border_lines.append(coords)
226
+
227
+ else:
228
+ raise ValueError("Unrecognized coordinate system")
229
+
230
+ line_collection = LineCollection(
231
+ border_lines,
232
+ **style.matplot_line_collection_kwargs(self.scale),
233
+ transform=self._crs,
234
+ clip_on=True,
235
+ clip_path=self._background_clip_path,
236
+ gid="constellations-border",
237
+ )
238
+ self.ax.add_collection(line_collection)
239
+
240
+ @profile
241
+ @use_style(LabelStyle, "constellation_labels")
242
+ def constellation_labels(
243
+ self,
244
+ style: LabelStyle = None,
245
+ label_fn: Callable[[Constellation], str] = Constellation.get_label,
246
+ collision_handler: CollisionHandler = None,
247
+ ):
248
+ """
249
+ Plots constellation labels for all constellations that have been plotted. This means you must plot the constellations before plotting their labels.
250
+
251
+ It's good to plot these last because they're area-based labels (vs point-based, like for star names), and area-based labels have more freedom to move around. If you plot area-based labels first, then it would limit the available space for point-based labels.
252
+
253
+ Args:
254
+ style: Styling of the constellation labels. If None, then the plot's style (specified when creating the plot) will be used
255
+ label_fn: Callable for determining the label for each constellation. The default function returns the constellation's name in uppercase.
256
+ collision_handler: An instance of [CollisionHandler][starplot.CollisionHandler] that describes what to do on collisions with other labels, markers, etc. If `None`, then `CollisionHandler(allow_constellation_line_collisions=True)` will be used (**Important: this function does NOT default to the plot's collision handler, since it's the only area-based label function and collisions should be handled differently**).
257
+ """
258
+
259
+ collision_handler = collision_handler or self.area_label_handler
260
+
261
+ hips = []
262
+ for c in self.objects.constellations:
263
+ hips.extend(c.star_hip_ids)
264
+
265
+ all_constellation_stars = Star.find(where=[_.hip.isin(hips)])
266
+
267
+ for constellation in self.objects.constellations:
268
+ constellation_line_stars = [
269
+ s
270
+ for s in all_constellation_stars
271
+ if s.hip in constellation.star_hip_ids
272
+ ]
273
+ if not constellation_line_stars:
274
+ continue
275
+
276
+ if is_wrapped_polygon(constellation.boundary):
277
+ starpoints = []
278
+ ra, dec = zip(*[(s.ra, s.dec) for s in constellation_line_stars])
279
+ new_ra = [r - 360 if r > 300 else r for r in ra]
280
+ starpoints = list(zip(new_ra, dec))
281
+
282
+ else:
283
+ ra, dec = zip(*[(s.ra, s.dec) for s in constellation_line_stars])
284
+ starpoints = list(zip(ra, dec))
285
+
286
+ points_line = MultiPoint(starpoints)
287
+ centroid = points_line.centroid
288
+ text = label_fn(constellation)
289
+
290
+ self.text(
291
+ text,
292
+ centroid.x,
293
+ centroid.y,
294
+ style,
295
+ area=constellation.boundary, # TODO : make this intersection with clip path
296
+ collision_handler=collision_handler,
297
+ gid="constellations-label-name",
298
+ )
@@ -0,0 +1,21 @@
1
+ from matplotlib.patches import Rectangle
2
+ from matplotlib.transforms import Bbox
3
+
4
+
5
+ class DebugPlotterMixin:
6
+ def _debug_bbox(self, bbox, color, width=1):
7
+ bboxe = Bbox([[bbox[0], bbox[1]], [bbox[2], bbox[3]]])
8
+ bboxe = bboxe.transformed(self.ax.transAxes.inverted())
9
+ rect = Rectangle(
10
+ (bboxe.x0, bboxe.y0), # (x, y) position in display pixels
11
+ width=bboxe.width,
12
+ height=bboxe.height,
13
+ transform=self.ax.transAxes,
14
+ fill=False,
15
+ facecolor="none",
16
+ edgecolor=color,
17
+ linewidth=width,
18
+ alpha=1,
19
+ zorder=100_000,
20
+ )
21
+ self.ax.add_patch(rect)