plot-io 0.1.0__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 (65) hide show
  1. plot_io/__init__.py +66 -0
  2. plot_io/_backend.py +114 -0
  3. plot_io/axis.py +574 -0
  4. plot_io/colorbar.py +342 -0
  5. plot_io/decorations.py +496 -0
  6. plot_io/defaults.py +766 -0
  7. plot_io/density.py +97 -0
  8. plot_io/enums.py +63 -0
  9. plot_io/facets.py +184 -0
  10. plot_io/figure.py +331 -0
  11. plot_io/labels.py +63 -0
  12. plot_io/legend.py +498 -0
  13. plot_io/limits.py +92 -0
  14. plot_io/maps/__init__.py +365 -0
  15. plot_io/maps/data.py +333 -0
  16. plot_io/markers.py +117 -0
  17. plot_io/numbers.py +289 -0
  18. plot_io/occupancy.py +219 -0
  19. plot_io/palettes.py +142 -0
  20. plot_io/plot.py +435 -0
  21. plot_io/render.py +1122 -0
  22. plot_io/scales.py +93 -0
  23. plot_io/series/__init__.py +498 -0
  24. plot_io/series/area.py +181 -0
  25. plot_io/series/bar.py +294 -0
  26. plot_io/series/box.py +401 -0
  27. plot_io/series/catdata.py +23 -0
  28. plot_io/series/catpoints.py +182 -0
  29. plot_io/series/contour.py +294 -0
  30. plot_io/series/curves.py +128 -0
  31. plot_io/series/ecdf.py +143 -0
  32. plot_io/series/errorbar.py +111 -0
  33. plot_io/series/errors.py +100 -0
  34. plot_io/series/fill.py +168 -0
  35. plot_io/series/heatmap.py +304 -0
  36. plot_io/series/hexbin.py +216 -0
  37. plot_io/series/hist.py +379 -0
  38. plot_io/series/hist2d.py +163 -0
  39. plot_io/series/kde.py +228 -0
  40. plot_io/series/kde2d.py +153 -0
  41. plot_io/series/line.py +173 -0
  42. plot_io/series/pie.py +304 -0
  43. plot_io/series/points.py +341 -0
  44. plot_io/series/regression.py +250 -0
  45. plot_io/series/scatter.py +103 -0
  46. plot_io/series/stem.py +112 -0
  47. plot_io/series/violin.py +321 -0
  48. plot_io/settings.py +281 -0
  49. plot_io/shapes.py +57 -0
  50. plot_io/sharing.py +160 -0
  51. plot_io/slots.py +52 -0
  52. plot_io/stacking.py +63 -0
  53. plot_io/stats.py +140 -0
  54. plot_io/style.py +71 -0
  55. plot_io/styles/__init__.py +234 -0
  56. plot_io/styles/library.py +732 -0
  57. plot_io/tables.py +141 -0
  58. plot_io/text.py +109 -0
  59. plot_io/ticks.py +284 -0
  60. plot_io/times.py +199 -0
  61. plot_io/values.py +161 -0
  62. plot_io-0.1.0.dist-info/METADATA +102 -0
  63. plot_io-0.1.0.dist-info/RECORD +65 -0
  64. plot_io-0.1.0.dist-info/WHEEL +4 -0
  65. plot_io-0.1.0.dist-info/licenses/LICENSE +28 -0
plot_io/decorations.py ADDED
@@ -0,0 +1,496 @@
1
+ """Decorations: reference lines, spans, text and annotations on a plot.
2
+
3
+ Decorations are not series: they take no palette color, are in the legend only with a
4
+ ``name``, and change the axis limits only with ``extend_limits=True``. They are drawn
5
+ with the series in ``z`` order (spans under the series, lines and text over them by
6
+ default). Positions are in data units (``coords="data"``), fractions of the axes
7
+ (``"axes"``) or of the figure (``"figure"``).
8
+ """
9
+
10
+ import math
11
+ from typing import Any
12
+
13
+ from .settings import (Setting, Settings, boolean, color, number, one_of, table, text)
14
+ from .style import dash_of, resolve_color, with_alpha
15
+ from .text import Font, draw_text, measure
16
+
17
+ __all__ = ["Decoration", "HLine", "VLine", "AxLine", "HSpan", "VSpan", "Text", "Annotate"]
18
+
19
+
20
+ def _dash(v: Any) -> Any:
21
+ if isinstance(v, (list, tuple)):
22
+ return [number(lo=0)(x) for x in v]
23
+ return one_of("solid", "dashed", "dotted", "dashdot")(v)
24
+
25
+
26
+ def _point(v: Any) -> tuple[Any, Any]:
27
+ if not isinstance(v, (tuple, list)) or len(v) != 2:
28
+ raise ValueError(f"expected a point (x, y), got {v!r}")
29
+ return v[0], v[1]
30
+
31
+
32
+ _COORDS = one_of("data", "axes", "figure")
33
+
34
+
35
+ def _arrow_style(v: Any) -> str:
36
+ if v not in ("->", "-|>", "-"):
37
+ raise ValueError(f"expected '->', '-|>' or '-', got {v!r}")
38
+ return v
39
+
40
+
41
+ def _common() -> dict[str, Setting]:
42
+ return dict(
43
+ name=Setting(text, doc="Legend entry; unset: not in the legend."),
44
+ alpha=Setting(number(0, 1), doc="Opacity."),
45
+ z=Setting(number(), doc="Drawing order among the series (default 0)."),
46
+ extend_limits=Setting(boolean, doc="Include the decoration in the axis limits."),
47
+ clip=Setting(boolean, doc="Clip to the axes area."),
48
+ )
49
+
50
+
51
+ def _line() -> dict[str, Setting]:
52
+ return dict(
53
+ color=Setting(color, doc="Line color."),
54
+ line_width=Setting(number(lo=0), inherited=True, doc="Line width (pt).", length=True),
55
+ line_style=Setting(_dash,
56
+ doc="`solid`, `dashed`, `dotted`, `dashdot` or a dash list."),
57
+ )
58
+
59
+
60
+ def _font() -> dict[str, Setting]:
61
+ return dict(
62
+ font_size=Setting(number(positive=True), inherited=True, doc="Font size (pt).",
63
+ length=True),
64
+ font_family=Setting(str, inherited=True, doc="Font family."),
65
+ font_weight=Setting(one_of("normal", "bold"), doc="`normal` or `bold`."),
66
+ text_color=Setting(color, inherited=True, doc="Text color."),
67
+ )
68
+
69
+
70
+ class Decoration(Settings):
71
+ """Base of decorations: behaves like a series towards the axes and the legend."""
72
+
73
+ USES_PALETTE = False
74
+ AXES = True
75
+ DODGE_GROUP = None
76
+ STACK_GROUP = None
77
+ #: The axis roles the decoration's data coordinates are on.
78
+ ROLES: tuple[str, ...] = ()
79
+
80
+ def __init__(self, plot: Any, **settings: Any) -> None:
81
+ self.plot = plot
82
+ super().__init__(**settings)
83
+ plot.decorations.append(self)
84
+
85
+ def _parent(self) -> Settings:
86
+ return self.plot
87
+
88
+ def __repr__(self) -> str:
89
+ return f"{self.kind}()"
90
+
91
+ # -- what the axes need (only with extend_limits) -------------------------------
92
+
93
+ def has_axis(self, role: str) -> bool:
94
+ return role in self.ROLES and self.get("extend_limits")
95
+
96
+ def categories(self, role: str) -> list[Any] | None:
97
+ return None
98
+
99
+ def data_values(self, role: str) -> list[float]:
100
+ return []
101
+
102
+ def values(self, role: str) -> Any:
103
+ from array import array
104
+ return array("d", [float(v) for v in self.data_values(role)
105
+ if isinstance(v, (int, float))])
106
+
107
+ def extent(self, role: str, scale: Any) -> tuple[int, float, float]:
108
+ from . import _backend
109
+ return _backend.extent(self.values(role), scale.function)
110
+
111
+ def inverts(self, role: str) -> bool:
112
+ return False
113
+
114
+ def zero_default(self, role: str) -> bool:
115
+ return False
116
+
117
+ def zero_padding(self, role: str) -> bool:
118
+ return False
119
+
120
+ def color_sources(self) -> list[Any]:
121
+ return []
122
+
123
+ def size_sources(self) -> list[Any]:
124
+ return []
125
+
126
+ def has_own_color(self) -> bool:
127
+ return True
128
+
129
+ def legend_labels(self) -> list[Any]:
130
+ """Legend entries of the object: its ``name``, or none."""
131
+ name = self.get("name")
132
+ return [] if name is None else [name]
133
+
134
+ def legend_glyph_at(self, ctx: Any, i: int, x: float, y: float, w: float, h: float) -> None:
135
+ """The symbol of legend entry ``i`` (series with several entries override this)."""
136
+ self.legend_glyph(ctx, x, y, w, h)
137
+
138
+ def legend_colors_at(self, ctx: Any, i: int) -> tuple[Any, Any]:
139
+ return self.legend_colors(ctx)
140
+
141
+ def legend_points(self) -> Any:
142
+ return None
143
+
144
+ def legend_colors(self, ctx: Any) -> tuple[Any, Any]:
145
+ c = with_alpha(ctx.color(self.get("color")), self.get("alpha"))
146
+ return c, None
147
+
148
+ def legend_glyph(self, ctx: Any, x: float, y: float, w: float, h: float) -> None:
149
+ c = with_alpha(ctx.color(self.get("color")), self.get("alpha"))
150
+ if c is None:
151
+ return
152
+ if "line_style" in self.SETTINGS:
153
+ lw = self.get("line_width")
154
+ ctx.canvas.draw_lines([x, x + w], [y + h / 2, y + h / 2], color=c, width=lw,
155
+ dash=dash_of(self.get("line_style"), lw))
156
+ else:
157
+ ctx.canvas.fill_style = c
158
+ ctx.canvas.fill_rect(x, y, w, h)
159
+
160
+ # -- positions ------------------------------------------------------------------
161
+
162
+ def _pos(self, ctx: Any, role: str, v: Any, coords: str) -> float:
163
+ """Page coordinate of ``v`` along ``role`` (``x`` or ``y``)."""
164
+ x0, y0, x1, y1 = ctx.rect
165
+ if coords == "axes":
166
+ return x0 + float(v) * (x1 - x0) if role == "x" else y1 - float(v) * (y1 - y0)
167
+ if coords == "figure":
168
+ fw, fh = getattr(ctx, "figure_size", (x1, y1))
169
+ return float(v) * fw if role == "x" else (1.0 - float(v)) * fh
170
+ state = ctx.x if role == "x" else ctx.y
171
+ if isinstance(v, str):
172
+ if not state.is_category or v not in state.categories:
173
+ raise ValueError(f"{v!r} is not a category of the {role} axis")
174
+ v = state.categories.index(v)
175
+ mapper = ctx.xmap if role == "x" else ctx.ymap
176
+ return mapper(float(v))
177
+
178
+ def draw(self, ctx: Any) -> None:
179
+ raise NotImplementedError
180
+
181
+
182
+ class _Rule(Decoration):
183
+ """A horizontal or vertical reference line across part of the axes."""
184
+
185
+ VERTICAL = False
186
+ value: Any = None
187
+
188
+ def draw(self, ctx: Any) -> None:
189
+ c = with_alpha(ctx.color(self.get("color")), self.get("alpha"))
190
+ lw = self.get("line_width")
191
+ if c is None:
192
+ return
193
+ x0, y0, x1, y1 = ctx.rect
194
+ lo, hi = self.get("min"), self.get("max")
195
+ if self.VERTICAL:
196
+ p = self._pos(ctx, "x", self.value, "data")
197
+ xs, ys = [p, p], [y1 - lo * (y1 - y0), y1 - hi * (y1 - y0)]
198
+ else:
199
+ p = self._pos(ctx, "y", self.value, "data")
200
+ xs, ys = [x0 + lo * (x1 - x0), x0 + hi * (x1 - x0)], [p, p]
201
+ if not all(math.isfinite(v) for v in xs + ys):
202
+ return
203
+ ctx.canvas.draw_lines(xs, ys, color=c, width=lw, dash=dash_of(self.get("line_style"), lw),
204
+ cap="butt")
205
+
206
+ def data_values(self, role: str) -> list[float]:
207
+ return [self.value]
208
+
209
+
210
+ class HLine(_Rule):
211
+ """A horizontal line at ``y`` from ``min`` to ``max`` (fractions of the axes width)."""
212
+
213
+ SETTINGS = table(
214
+ **_line(),
215
+ min=Setting(number(0, 1), doc="Start, as a fraction of the axes width."),
216
+ max=Setting(number(0, 1), doc="End, as a fraction of the axes width."),
217
+ **_common(),
218
+ )
219
+ ROLES = ("y",)
220
+
221
+ def __init__(self, plot: Any, y: Any, **settings: Any) -> None:
222
+ self.value = y
223
+ super().__init__(plot, **settings)
224
+
225
+
226
+ class VLine(_Rule):
227
+ """A vertical line at ``x`` from ``min`` to ``max`` (fractions of the axes height)."""
228
+
229
+ SETTINGS = HLine.SETTINGS
230
+ ROLES = ("x",)
231
+ VERTICAL = True
232
+
233
+ def __init__(self, plot: Any, x: Any, **settings: Any) -> None:
234
+ self.value = x
235
+ super().__init__(plot, **settings)
236
+
237
+
238
+ class AxLine(Decoration):
239
+ """An infinite straight line through ``point`` and ``point2``, or with ``slope`` (data
240
+ units; linear axes)."""
241
+
242
+ SETTINGS = table(**_line(), **_common())
243
+ ROLES = ("x", "y")
244
+
245
+ def __init__(self, plot: Any, point: Any, slope: Any = None, point2: Any = None,
246
+ **settings: Any) -> None:
247
+ self.p1 = _point(point)
248
+ if (slope is None) == (point2 is None):
249
+ raise ValueError("give either slope or point2")
250
+ self.p2 = _point(point2) if point2 is not None else None
251
+ self.slope = None if slope is None else float(slope)
252
+ super().__init__(plot, **settings)
253
+
254
+ def data_values(self, role: str) -> list[float]:
255
+ i = 0 if role == "x" else 1
256
+ pts = [self.p1] + ([self.p2] if self.p2 is not None else [])
257
+ return [p[i] for p in pts]
258
+
259
+ def draw(self, ctx: Any) -> None:
260
+ c = with_alpha(ctx.color(self.get("color")), self.get("alpha"))
261
+ lw = self.get("line_width")
262
+ if c is None:
263
+ return
264
+ if self.p2 is None:
265
+ for st in (ctx.x, ctx.y):
266
+ if st.scale is None or st.scale.name != "linear":
267
+ raise ValueError("axline with a slope needs linear axes; give point2")
268
+ p2 = (float(self.p1[0]) + 1.0, float(self.p1[1]) + float(self.slope or 0.0))
269
+ else:
270
+ p2 = self.p2
271
+ ax, ay = self._pos(ctx, "x", self.p1[0], "data"), self._pos(ctx, "y", self.p1[1], "data")
272
+ bx, by = self._pos(ctx, "x", p2[0], "data"), self._pos(ctx, "y", p2[1], "data")
273
+ dx, dy = bx - ax, by - ay
274
+ n = math.hypot(dx, dy)
275
+ if not n or not math.isfinite(n):
276
+ return
277
+ x0, y0, x1, y1 = ctx.rect
278
+ far = 2.0 * (math.hypot(x1 - x0, y1 - y0) + abs(ax - x0) + abs(ay - y0)) / n
279
+ ctx.canvas.draw_lines([ax - far * dx, ax + far * dx], [ay - far * dy, ay + far * dy],
280
+ color=c, width=lw, dash=dash_of(self.get("line_style"), lw))
281
+
282
+
283
+ class _Span(Decoration):
284
+ VERTICAL = False
285
+ v0: Any = None
286
+ v1: Any = None
287
+
288
+ def draw(self, ctx: Any) -> None:
289
+ c = with_alpha(ctx.color(self.get("color")), self.get("alpha"))
290
+ if c is None:
291
+ return
292
+ x0, y0, x1, y1 = ctx.rect
293
+ lo, hi = self.get("min"), self.get("max")
294
+ if self.VERTICAL:
295
+ a, b = self._pos(ctx, "x", self.v0, "data"), self._pos(ctx, "x", self.v1, "data")
296
+ r = (min(a, b), y1 - hi * (y1 - y0), abs(b - a), (hi - lo) * (y1 - y0))
297
+ else:
298
+ a, b = self._pos(ctx, "y", self.v0, "data"), self._pos(ctx, "y", self.v1, "data")
299
+ r = (x0 + lo * (x1 - x0), min(a, b), (hi - lo) * (x1 - x0), abs(b - a))
300
+ if not all(math.isfinite(v) for v in r):
301
+ return
302
+ canvas = ctx.canvas
303
+ canvas.begin_path()
304
+ canvas.rect(*r)
305
+ canvas.fill(c)
306
+ lc = ctx.color(self.get("line_color"))
307
+ if lc is not None and self.get("line_width"):
308
+ canvas.line_width = self.get("line_width")
309
+ canvas.stroke(lc)
310
+
311
+ def data_values(self, role: str) -> list[float]:
312
+ return [self.v0, self.v1]
313
+
314
+
315
+ class HSpan(_Span):
316
+ """A horizontal band from ``y0`` to ``y1``, from ``min`` to ``max`` of the axes width."""
317
+
318
+ SETTINGS = table(
319
+ color=Setting(color, doc="Fill color."),
320
+ line_color=Setting(color, doc="Outline color; default none."),
321
+ line_width=Setting(number(lo=0), doc="Outline width (pt).", length=True),
322
+ min=Setting(number(0, 1), doc="Start, as a fraction of the axes width."),
323
+ max=Setting(number(0, 1), doc="End, as a fraction of the axes width."),
324
+ **_common(),
325
+ )
326
+ ROLES = ("y",)
327
+
328
+ def __init__(self, plot: Any, y0: Any, y1: Any, **settings: Any) -> None:
329
+ self.v0, self.v1 = y0, y1
330
+ super().__init__(plot, **settings)
331
+
332
+
333
+ class VSpan(_Span):
334
+ """A vertical band from ``x0`` to ``x1``, from ``min`` to ``max`` of the axes height."""
335
+
336
+ SETTINGS = HSpan.SETTINGS
337
+ ROLES = ("x",)
338
+ VERTICAL = True
339
+
340
+ def __init__(self, plot: Any, x0: Any, x1: Any, **settings: Any) -> None:
341
+ self.v0, self.v1 = x0, x1
342
+ super().__init__(plot, **settings)
343
+
344
+
345
+ class Text(Decoration):
346
+ """Text at ``(x, y)``: a string or rich content."""
347
+
348
+ SETTINGS = table(
349
+ align=Setting(one_of("left", "center", "right"),
350
+ doc="Horizontal alignment on the point."),
351
+ valign=Setting(one_of("baseline", "top", "center", "bottom"),
352
+ doc="Vertical alignment on the point."),
353
+ rotation=Setting(number(), doc="Rotation (degrees, counterclockwise)."),
354
+ coords=Setting(_COORDS, doc="`data`, `axes` (0–1) or `figure` (0–1)."),
355
+ color=Setting(color, doc="Text color; default `text_color`."),
356
+ **_font(),
357
+ **{k: v for k, v in _common().items() if k != "clip"},
358
+ clip=Setting(boolean, doc="Clip to the axes area."),
359
+ )
360
+ ROLES = ("x", "y")
361
+
362
+ def __init__(self, plot: Any, x: Any, y: Any, content: Any, **settings: Any) -> None:
363
+ self.x, self.y, self.content = x, y, content
364
+ super().__init__(plot, **settings)
365
+
366
+ def has_axis(self, role: str) -> bool:
367
+ return super().has_axis(role) and self.get("coords") == "data"
368
+
369
+ def data_values(self, role: str) -> list[float]:
370
+ return [self.x if role == "x" else self.y]
371
+
372
+ def font(self) -> Font:
373
+ c = self.get("color")
374
+ return Font(self.get("font_size"), self.get("font_family"), self.get("font_weight"),
375
+ color=c)
376
+
377
+ def legend_colors(self, ctx: Any) -> tuple[Any, Any]:
378
+ return ctx.color(self.get("color")), None
379
+
380
+ def draw(self, ctx: Any) -> None:
381
+ coords = self.get("coords")
382
+ px, py = self._pos(ctx, "x", self.x, coords), self._pos(ctx, "y", self.y, coords)
383
+ font = self.font()
384
+ font.color = with_alpha(resolve_color(font.color, ctx.palette), self.get("alpha"))
385
+ draw_text(ctx.canvas, self.content, px, py, font, self.get("align"), self.get("valign"),
386
+ self.get("rotation"))
387
+
388
+
389
+ class Annotate(Text):
390
+ """Text at ``xytext`` with an arrow to ``xy``."""
391
+
392
+ SETTINGS = table(
393
+ **{k: v for k, v in Text.SETTINGS.items() if k not in ("align", "valign")},
394
+ align=Setting(one_of("left", "center", "right"),
395
+ doc="Horizontal alignment on `xytext`."),
396
+ valign=Setting(one_of("baseline", "top", "center", "bottom"),
397
+ doc="Vertical alignment on `xytext`."),
398
+ text_coords=Setting(one_of("data", "axes", "figure", "offset"),
399
+ doc="Coordinates of `xytext`: as `coords`, or `offset` (pt from "
400
+ "`xy`, y up); default `coords`."),
401
+ arrow=Setting(boolean, doc="Draw an arrow from the text to `xy`."),
402
+ arrow_style=Setting(_arrow_style,
403
+ doc="`->` (open head), `-|>` (filled head) or `-` (no head)."),
404
+ arrow_color=Setting(color, doc="Arrow color; default the text color."),
405
+ arrow_width=Setting(number(lo=0), doc="Arrow line width (pt).", length=True),
406
+ head_size=Setting(number(lo=0), doc="Arrowhead length (pt).", length=True),
407
+ shrink=Setting(number(lo=0), doc="Gap left at both ends of the arrow (pt).",
408
+ length=True),
409
+ )
410
+
411
+ def __init__(self, plot: Any, content: Any, xy: Any, xytext: Any = None,
412
+ **settings: Any) -> None:
413
+ self.xy = _point(xy)
414
+ self.xytext = _point(xytext) if xytext is not None else None
415
+ x, y = self.xytext if self.xytext is not None else self.xy
416
+ super().__init__(plot, x, y, content, **settings)
417
+
418
+ def data_values(self, role: str) -> list[float]:
419
+ i = 0 if role == "x" else 1
420
+ out = [self.xy[i]]
421
+ if self.xytext is not None and self.get("text_coords") == "data":
422
+ out.append(self.xytext[i])
423
+ return out
424
+
425
+ def draw(self, ctx: Any) -> None:
426
+ coords = self.get("coords")
427
+ tx = self.get("text_coords")
428
+ ax, ay = self._pos(ctx, "x", self.xy[0], coords), self._pos(ctx, "y", self.xy[1], coords)
429
+ if self.xytext is None:
430
+ px, py = ax, ay
431
+ elif tx == "offset":
432
+ px, py = ax + float(self.xytext[0]), ay - float(self.xytext[1])
433
+ else:
434
+ px, py = self._pos(ctx, "x", self.xytext[0], tx), self._pos(ctx, "y", self.xytext[1], tx)
435
+ font = self.font()
436
+ canvas = ctx.canvas
437
+ tc = with_alpha(resolve_color(font.color, ctx.palette), self.get("alpha"))
438
+ font.color = tc
439
+ w, a, d = measure(canvas, self.content, font)
440
+ bx = px + {"left": 0.0, "center": -w / 2, "right": -w}[self.get("align")]
441
+ top = py - {"baseline": a, "top": 0.0, "center": (a + d) / 2, "bottom": a + d}[
442
+ self.get("valign")]
443
+ draw_text(canvas, self.content, px, py, font, self.get("align"), self.get("valign"))
444
+ if self.get("arrow") and self.xytext is not None:
445
+ ac = with_alpha(ctx.color(self.get("arrow_color")), self.get("alpha"))
446
+ self._arrow(canvas, (bx, top, bx + w, top + a + d), ax, ay, ac)
447
+
448
+ def _arrow(self, canvas: Any, box: tuple[float, float, float, float], ax: float, ay: float,
449
+ c: Any) -> None:
450
+ x0, y0, x1, y1 = box
451
+ cx, cy = (x0 + x1) / 2, (y0 + y1) / 2
452
+ dx, dy = ax - cx, ay - cy
453
+ n = math.hypot(dx, dy)
454
+ if not n or c is None:
455
+ return
456
+ ux, uy = dx / n, dy / n
457
+ # leave the text box: smallest t where the ray crosses its edge
458
+ ts = []
459
+ if ux:
460
+ ts.append(((x1 if ux > 0 else x0) - cx) / ux)
461
+ if uy:
462
+ ts.append(((y1 if uy > 0 else y0) - cy) / uy)
463
+ t0 = min(t for t in ts if t >= 0) if ts else 0.0
464
+ shrink = self.get("shrink")
465
+ t0 += shrink
466
+ t1 = n - shrink
467
+ if t1 <= t0:
468
+ return
469
+ sx, sy = cx + ux * t0, cy + uy * t0
470
+ ex, ey = cx + ux * t1, cy + uy * t1
471
+ lw = self.get("arrow_width")
472
+ style = self.get("arrow_style")
473
+ hs = self.get("head_size")
474
+ if style == "-|>":
475
+ ex2, ey2 = ex - ux * hs * 0.9, ey - uy * hs * 0.9
476
+ else:
477
+ ex2, ey2 = ex, ey
478
+ canvas.draw_lines([sx, ex2], [sy, ey2], color=c, width=lw, cap="butt")
479
+ if style == "-" or not hs:
480
+ return
481
+ hw = 0.45 * hs
482
+ px_, py_ = -uy, ux
483
+ bx, by = ex - ux * hs, ey - uy * hs
484
+ canvas.save()
485
+ canvas.begin_path()
486
+ canvas.move_to(bx + px_ * hw, by + py_ * hw)
487
+ canvas.line_to(ex, ey)
488
+ canvas.line_to(bx - px_ * hw, by - py_ * hw)
489
+ if style == "-|>":
490
+ canvas.close_path()
491
+ canvas.fill(c)
492
+ else:
493
+ canvas.line_width = lw
494
+ canvas.line_join = "miter"
495
+ canvas.stroke(c)
496
+ canvas.restore()