rgrid-python 4.5.3__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.
grid_py/__init__.py ADDED
@@ -0,0 +1,340 @@
1
+ """
2
+ grid_py — Python port of the R grid graphics package.
3
+
4
+ Provides a complete reimplementation of R's grid graphics system including
5
+ units, viewports, grobs (graphical objects), layouts, and rendering via
6
+ Cairo (pycairo).
7
+ """
8
+
9
+ __version__ = "4.5.3"
10
+
11
+ # --- Utilities ---
12
+ from grid_py._utils import depth, explode, grid_pretty, n2mfrow
13
+
14
+ # --- Justification ---
15
+ from grid_py._just import valid_just, resolve_hjust, resolve_vjust, resolve_raster_size
16
+
17
+ # --- Units ---
18
+ from grid_py._units import (
19
+ Unit, is_unit, unit_type, unit_c, unit_length,
20
+ unit_pmax, unit_pmin, unit_psum, unit_rep,
21
+ string_width, string_height, string_ascent, string_descent,
22
+ absolute_size,
23
+ convert_unit, convert_x, convert_y, convert_width, convert_height,
24
+ )
25
+
26
+ # --- Graphical Parameters ---
27
+ from grid_py._gpar import Gpar, get_gpar
28
+
29
+ # --- Arrow ---
30
+ from grid_py._arrow import Arrow, arrow
31
+
32
+ # --- Paths ---
33
+ from grid_py._path import GPath, VpPath, GridPath, as_path, is_closed, PATH_SEP
34
+
35
+ # --- Layout ---
36
+ from grid_py._layout import (
37
+ GridLayout, layout_region,
38
+ )
39
+
40
+ # --- Patterns, Masks, Clip Paths ---
41
+ from grid_py._patterns import (
42
+ LinearGradient, RadialGradient, Pattern,
43
+ linear_gradient, radial_gradient, pattern,
44
+ )
45
+ from grid_py._mask import GridMask, as_mask, is_mask
46
+ from grid_py._clippath import GridClipPath, as_clip_path, is_clip_path
47
+
48
+ # --- Viewport ---
49
+ from grid_py._viewport import (
50
+ Viewport, VpList, VpStack, VpTree,
51
+ push_viewport, pop_viewport, down_viewport, up_viewport, seek_viewport,
52
+ current_viewport, current_vp_path, current_vp_tree,
53
+ current_transform, current_rotation, current_parent,
54
+ data_viewport, plot_viewport, edit_viewport, show_viewport,
55
+ )
56
+
57
+ # --- State ---
58
+ from grid_py._state import GridState, get_state
59
+
60
+ # --- Display List ---
61
+ from grid_py._display_list import DisplayList
62
+
63
+ # --- Transforms ---
64
+ from grid_py._transforms import (
65
+ group_translate, group_rotate, group_scale, group_shear, group_flip,
66
+ defn_translate, defn_rotate, defn_scale,
67
+ use_translate, use_rotate, use_scale,
68
+ viewport_translate, viewport_rotate, viewport_scale, viewport_transform,
69
+ )
70
+
71
+ # --- Grob ---
72
+ from grid_py._grob import (
73
+ Grob, GTree, GList, GEdit, GEditList,
74
+ grob_tree, grob_name, is_grob,
75
+ get_grob, set_grob, add_grob, remove_grob, edit_grob,
76
+ force_grob, set_children, reorder_grob,
77
+ apply_edit, apply_edits,
78
+ )
79
+
80
+ # --- Primitives ---
81
+ from grid_py._primitives import (
82
+ move_to_grob, grid_move_to,
83
+ line_to_grob, grid_line_to,
84
+ lines_grob, grid_lines,
85
+ polyline_grob, grid_polyline,
86
+ segments_grob, grid_segments,
87
+ arrows_grob, grid_arrows,
88
+ points_grob, grid_points,
89
+ rect_grob, grid_rect,
90
+ roundrect_grob, grid_roundrect,
91
+ circle_grob, grid_circle,
92
+ polygon_grob, grid_polygon,
93
+ path_grob, grid_path,
94
+ text_grob, grid_text,
95
+ raster_grob, grid_raster,
96
+ clip_grob, grid_clip,
97
+ null_grob, grid_null,
98
+ function_grob, grid_function,
99
+ as_path,
100
+ stroke_grob, grid_stroke,
101
+ fill_grob, grid_fill,
102
+ fill_stroke_grob, grid_fill_stroke,
103
+ )
104
+
105
+ # --- Coordinates ---
106
+ from grid_py._coords import (
107
+ GridCoords, GridGrobCoords, GridGTreeCoords,
108
+ grob_coords, grob_points,
109
+ grid_coords, grid_grob_coords, grid_gtree_coords,
110
+ empty_coords, empty_grob_coords, empty_gtree_coords,
111
+ is_empty_coords,
112
+ )
113
+
114
+ # --- Curves ---
115
+ from grid_py._curve import (
116
+ curve_grob, grid_curve,
117
+ xspline_grob, grid_xspline,
118
+ bezier_grob, grid_bezier,
119
+ xspline_points, bezier_points,
120
+ arc_curvature,
121
+ )
122
+
123
+ # --- Groups ---
124
+ from grid_py._group import (
125
+ GroupGrob, DefineGrob, UseGrob,
126
+ group_grob, grid_group,
127
+ define_grob, grid_define,
128
+ use_grob, grid_use,
129
+ )
130
+
131
+ # --- Renderer ---
132
+ from grid_py._renderer_base import GridRenderer
133
+ from grid_py.renderer import CairoRenderer
134
+ from grid_py.renderer_web import WebRenderer
135
+
136
+ # --- Drawing ---
137
+ from grid_py._draw import (
138
+ grid_draw, grid_newpage, grid_refresh,
139
+ grid_record, record_grob,
140
+ grid_delay, delay_grob,
141
+ grid_dl_apply, grid_locator,
142
+ )
143
+
144
+ # --- Edit (display list) ---
145
+ from grid_py._edit import (
146
+ grid_edit, grid_get, grid_set, grid_add, grid_remove,
147
+ grid_gedit, grid_gget, grid_gremove,
148
+ )
149
+
150
+ # --- Listing & Search ---
151
+ from grid_py._ls import (
152
+ grid_ls, grid_grep,
153
+ nested_listing, path_listing, grob_path_listing,
154
+ show_grob, get_names, child_names,
155
+ )
156
+
157
+ # --- Grab ---
158
+ from grid_py._grab import (
159
+ grid_grab, grid_grab_expr,
160
+ grid_force, grid_revert,
161
+ grid_cap, grid_reorder,
162
+ )
163
+
164
+ # --- High-level ---
165
+ from grid_py._highlevel import (
166
+ grid_grill, grid_show_layout, grid_show_viewport,
167
+ grid_abline, grid_plot_and_legend, layout_torture,
168
+ frame_grob, grid_frame, pack_grob, grid_pack, place_grob, grid_place,
169
+ xaxis_grob, grid_xaxis, yaxis_grob, grid_yaxis,
170
+ legend_grob, grid_legend,
171
+ grid_multipanel, grid_panel, grid_strip,
172
+ grid_top_level_vp,
173
+ )
174
+
175
+ # --- Size / Metrics ---
176
+ from grid_py._size import (
177
+ calc_string_metric,
178
+ width_details, height_details, ascent_details, descent_details,
179
+ grob_width, grob_height, grob_x, grob_y,
180
+ grob_ascent, grob_descent,
181
+ )
182
+
183
+ # --- Typeset ---
184
+ from grid_py._typeset import glyph_grob, grid_glyph
185
+
186
+ # --- Deprecated aliases (R compatibility) ---
187
+ convert_native = convert_unit
188
+ grid_convert = convert_unit
189
+ grid_convert_x = convert_x
190
+ grid_convert_y = convert_y
191
+ grid_convert_width = convert_width
192
+ grid_convert_height = convert_height
193
+ from grid_py._units import device_loc, device_dim
194
+
195
+ # R names grid.collection and grid.copy were undocumented stubs
196
+ grid_collection = grid_draw
197
+ grid_copy = grid_draw
198
+
199
+ # grid.display.list / engine.display.list
200
+ def grid_display_list(on: bool = True) -> bool:
201
+ """Enable or disable the display list.
202
+
203
+ Parameters
204
+ ----------
205
+ on : bool
206
+ Whether to enable recording.
207
+
208
+ Returns
209
+ -------
210
+ bool
211
+ Previous state.
212
+ """
213
+ state = get_state()
214
+ prev = state._dl_on
215
+ state._dl_on = on
216
+ return prev
217
+
218
+ engine_display_list = grid_display_list
219
+
220
+
221
+ __all__ = [
222
+ # Utils
223
+ "depth", "explode", "grid_pretty", "n2mfrow",
224
+ # Just
225
+ "valid_just", "resolve_hjust", "resolve_vjust", "resolve_raster_size",
226
+ # Units
227
+ "Unit", "is_unit", "unit_type", "unit_c", "unit_length",
228
+ "unit_pmax", "unit_pmin", "unit_psum", "unit_rep",
229
+ "string_width", "string_height", "string_ascent", "string_descent",
230
+ "absolute_size",
231
+ "convert_unit", "convert_x", "convert_y", "convert_width", "convert_height",
232
+ # Gpar
233
+ "Gpar", "get_gpar",
234
+ # Arrow
235
+ "Arrow", "arrow",
236
+ # Path
237
+ "GPath", "VpPath", "GridPath", "as_path", "is_closed", "PATH_SEP",
238
+ # Layout
239
+ "GridLayout", "layout_region",
240
+ # Patterns
241
+ "LinearGradient", "RadialGradient", "Pattern",
242
+ "linear_gradient", "radial_gradient", "pattern",
243
+ # Mask
244
+ "GridMask", "as_mask", "is_mask",
245
+ # Clip path
246
+ "GridClipPath", "as_clip_path", "is_clip_path",
247
+ # Viewport
248
+ "Viewport", "VpList", "VpStack", "VpTree",
249
+ "push_viewport", "pop_viewport", "down_viewport", "up_viewport", "seek_viewport",
250
+ "current_viewport", "current_vp_path", "current_vp_tree",
251
+ "current_transform", "current_rotation", "current_parent",
252
+ "data_viewport", "plot_viewport", "edit_viewport", "show_viewport",
253
+ # State
254
+ "GridState", "get_state",
255
+ # Display list
256
+ "DisplayList",
257
+ # Transforms
258
+ "group_translate", "group_rotate", "group_scale", "group_shear", "group_flip",
259
+ "defn_translate", "defn_rotate", "defn_scale",
260
+ "use_translate", "use_rotate", "use_scale",
261
+ "viewport_translate", "viewport_rotate", "viewport_scale", "viewport_transform",
262
+ # Grob
263
+ "Grob", "GTree", "GList", "GEdit", "GEditList",
264
+ "grob_tree", "grob_name", "is_grob",
265
+ "get_grob", "set_grob", "add_grob", "remove_grob", "edit_grob",
266
+ "force_grob", "set_children", "reorder_grob",
267
+ "apply_edit", "apply_edits",
268
+ # Primitives
269
+ "move_to_grob", "grid_move_to",
270
+ "line_to_grob", "grid_line_to",
271
+ "lines_grob", "grid_lines",
272
+ "polyline_grob", "grid_polyline",
273
+ "segments_grob", "grid_segments",
274
+ "arrows_grob", "grid_arrows",
275
+ "points_grob", "grid_points",
276
+ "rect_grob", "grid_rect",
277
+ "roundrect_grob", "grid_roundrect",
278
+ "circle_grob", "grid_circle",
279
+ "polygon_grob", "grid_polygon",
280
+ "path_grob", "grid_path",
281
+ "text_grob", "grid_text",
282
+ "raster_grob", "grid_raster",
283
+ "clip_grob", "grid_clip",
284
+ "null_grob", "grid_null",
285
+ "function_grob", "grid_function",
286
+ # Coords
287
+ "GridCoords", "GridGrobCoords", "GridGTreeCoords",
288
+ "grob_coords", "grob_points",
289
+ "grid_coords", "grid_grob_coords", "grid_gtree_coords",
290
+ "empty_coords", "empty_grob_coords", "empty_gtree_coords",
291
+ "is_empty_coords",
292
+ # Curves
293
+ "curve_grob", "grid_curve",
294
+ "xspline_grob", "grid_xspline",
295
+ "bezier_grob", "grid_bezier",
296
+ "xspline_points", "bezier_points",
297
+ "arc_curvature",
298
+ # Groups
299
+ "GroupGrob", "DefineGrob", "UseGrob",
300
+ "group_grob", "grid_group",
301
+ "define_grob", "grid_define",
302
+ "use_grob", "grid_use",
303
+ # Draw
304
+ "grid_draw", "grid_newpage", "grid_refresh",
305
+ "grid_record", "record_grob",
306
+ "grid_delay", "delay_grob",
307
+ "grid_dl_apply", "grid_locator",
308
+ # Edit
309
+ "grid_edit", "grid_get", "grid_set", "grid_add", "grid_remove",
310
+ "grid_gedit", "grid_gget", "grid_gremove",
311
+ # LS
312
+ "grid_ls", "grid_grep",
313
+ "nested_listing", "path_listing", "grob_path_listing",
314
+ "show_grob", "get_names", "child_names",
315
+ # Grab
316
+ "grid_grab", "grid_grab_expr",
317
+ "grid_force", "grid_revert",
318
+ "grid_cap", "grid_reorder",
319
+ # High-level
320
+ "grid_grill", "grid_show_layout", "grid_show_viewport",
321
+ "grid_abline", "grid_plot_and_legend", "layout_torture",
322
+ "frame_grob", "grid_frame", "pack_grob", "grid_pack", "place_grob", "grid_place",
323
+ "xaxis_grob", "grid_xaxis", "yaxis_grob", "grid_yaxis",
324
+ "legend_grob", "grid_legend",
325
+ "grid_multipanel", "grid_panel", "grid_strip",
326
+ "grid_top_level_vp",
327
+ # Size
328
+ "calc_string_metric",
329
+ "grob_width", "grob_height", "grob_x", "grob_y",
330
+ "grob_ascent", "grob_descent",
331
+ # Typeset
332
+ "glyph_grob", "grid_glyph",
333
+ # Deprecated
334
+ "convert_native", "grid_convert",
335
+ "grid_convert_x", "grid_convert_y",
336
+ "grid_convert_width", "grid_convert_height",
337
+ "device_loc", "device_dim",
338
+ "grid_collection", "grid_copy",
339
+ "grid_display_list", "engine_display_list",
340
+ ]
grid_py/_arrow.py ADDED
@@ -0,0 +1,331 @@
1
+ """
2
+ Arrow head specification for grid_py -- Python port of R's ``grid::arrow()``.
3
+
4
+ This module provides the :class:`Arrow` class and the :func:`arrow` factory
5
+ function, which describe the arrow heads that can be attached to line-based
6
+ grobs (segments, lines, curves, etc.).
7
+
8
+ The implementation mirrors the behaviour of R's ``arrow()`` constructor,
9
+ ``length.arrow``, ``rep.arrow``, and ``[.arrow`` method defined in
10
+ ``src/library/grid/R/primitives.R``.
11
+
12
+ Examples
13
+ --------
14
+ >>> from grid_py._arrow import arrow
15
+ >>> a = arrow()
16
+ >>> a
17
+ Arrow(angle=[30.0], length=Unit([0.25], 'inches'), ends=[2], type=[1])
18
+ >>> len(a)
19
+ 1
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ from typing import List, Optional, Sequence, Union
25
+
26
+ import numpy as np
27
+
28
+ from ._units import Unit, unit_rep
29
+
30
+ __all__ = ["Arrow", "arrow"]
31
+
32
+
33
+ def _recycle_unit(u: Unit, n: int) -> Unit:
34
+ """Recycle a Unit to length *n* using indexing with modular indices."""
35
+ lu = len(u)
36
+ if lu == n:
37
+ return u
38
+ indices = [i % lu for i in range(n)]
39
+ return u[indices]
40
+
41
+ # Valid string values for *ends* and *type*, following R's match() semantics.
42
+ _VALID_ENDS = ("first", "last", "both")
43
+ _VALID_TYPES = ("open", "closed")
44
+
45
+
46
+ class Arrow:
47
+ """Description of an arrow head to attach to a line-based grob.
48
+
49
+ Parameters
50
+ ----------
51
+ angle : float or Sequence[float]
52
+ Angle of the arrow head in degrees (the angle between the shaft and
53
+ each edge of the arrow head). Scalar or vector.
54
+ length : Unit, optional
55
+ Length of the arrow head measured along the edges. Must be a
56
+ :class:`Unit` object. Defaults to ``Unit(0.25, "inches")``.
57
+ ends : {"first", "last", "both"} or Sequence[str]
58
+ Which end(s) of the line should receive an arrow head. Encoded
59
+ internally as integers: ``1`` = first, ``2`` = last, ``3`` = both,
60
+ matching R's ``match()`` convention.
61
+ type : {"open", "closed"} or Sequence[str]
62
+ Whether the arrow head is open or closed. Encoded internally as
63
+ integers: ``1`` = open, ``2`` = closed.
64
+
65
+ Raises
66
+ ------
67
+ TypeError
68
+ If *length* is not a :class:`Unit` object.
69
+ ValueError
70
+ If *ends* or *type* contains invalid values.
71
+ """
72
+
73
+ # ------------------------------------------------------------------
74
+ # Construction
75
+ # ------------------------------------------------------------------
76
+
77
+ def __init__(
78
+ self,
79
+ angle: Union[float, int, Sequence[float]] = 30,
80
+ length: Optional[Unit] = None,
81
+ ends: Union[str, Sequence[str]] = "last",
82
+ type: Union[str, Sequence[str]] = "open", # noqa: A002
83
+ ) -> None:
84
+ # --- angle --------------------------------------------------------
85
+ if isinstance(angle, (int, float, np.integer, np.floating)):
86
+ self._angle: np.ndarray = np.asarray([float(angle)], dtype=np.float64)
87
+ else:
88
+ self._angle = np.asarray(angle, dtype=np.float64).ravel()
89
+
90
+ # --- length -------------------------------------------------------
91
+ if length is None:
92
+ length = Unit(0.25, "inches")
93
+ if not isinstance(length, Unit):
94
+ raise TypeError("'length' must be a Unit object")
95
+ self._length: Unit = length
96
+
97
+ # --- ends ---------------------------------------------------------
98
+ self._ends: np.ndarray = self._encode_match(ends, _VALID_ENDS, "ends")
99
+
100
+ # --- type ---------------------------------------------------------
101
+ self._type: np.ndarray = self._encode_match(type, _VALID_TYPES, "type")
102
+
103
+ # ------------------------------------------------------------------
104
+ # Helpers
105
+ # ------------------------------------------------------------------
106
+
107
+ @staticmethod
108
+ def _encode_match(
109
+ value: Union[str, Sequence[str]],
110
+ valid: tuple,
111
+ label: str,
112
+ ) -> np.ndarray:
113
+ """Encode string(s) to 1-based integer codes, like R's ``match()``.
114
+
115
+ Parameters
116
+ ----------
117
+ value : str or Sequence[str]
118
+ Input value(s).
119
+ valid : tuple of str
120
+ Allowed string values.
121
+ label : str
122
+ Name used in error messages.
123
+
124
+ Returns
125
+ -------
126
+ np.ndarray
127
+ 1-based integer codes corresponding to *value*.
128
+
129
+ Raises
130
+ ------
131
+ ValueError
132
+ If any element of *value* is not in *valid*.
133
+ """
134
+ if isinstance(value, str):
135
+ value = [value]
136
+ codes: List[int] = []
137
+ for v in value:
138
+ if v not in valid:
139
+ raise ValueError(
140
+ f"invalid '{label}' argument: {v!r}; "
141
+ f"must be one of {valid}"
142
+ )
143
+ codes.append(valid.index(v) + 1)
144
+ arr = np.asarray(codes, dtype=np.int64)
145
+ if arr.size == 0:
146
+ raise ValueError(f"'{label}' must have length > 0")
147
+ return arr
148
+
149
+ # ------------------------------------------------------------------
150
+ # Properties (read-only access to internal data)
151
+ # ------------------------------------------------------------------
152
+
153
+ @property
154
+ def angle(self) -> np.ndarray:
155
+ """Arrow-head angle(s) in degrees."""
156
+ return self._angle
157
+
158
+ @property
159
+ def length(self) -> Unit:
160
+ """Arrow-head length as a :class:`Unit`."""
161
+ return self._length
162
+
163
+ @property
164
+ def ends(self) -> np.ndarray:
165
+ """Integer code(s) for which end gets an arrow (1=first, 2=last, 3=both)."""
166
+ return self._ends
167
+
168
+ @property
169
+ def type(self) -> np.ndarray:
170
+ """Integer code(s) for arrow type (1=open, 2=closed)."""
171
+ return self._type
172
+
173
+ # ------------------------------------------------------------------
174
+ # length (len) -- mirrors R's length.arrow
175
+ # ------------------------------------------------------------------
176
+
177
+ def __len__(self) -> int:
178
+ """Return the effective vector length of this Arrow.
179
+
180
+ Follows R's ``length.arrow`` which returns the maximum of the
181
+ lengths of all component vectors (angle, length, ends, type).
182
+
183
+ Returns
184
+ -------
185
+ int
186
+ Effective length.
187
+ """
188
+ return int(
189
+ max(
190
+ len(self._angle),
191
+ len(self._length),
192
+ len(self._ends),
193
+ len(self._type),
194
+ )
195
+ )
196
+
197
+ # ------------------------------------------------------------------
198
+ # Subscript -- mirrors R's `[.arrow`
199
+ # ------------------------------------------------------------------
200
+
201
+ def __getitem__(self, index: Union[int, slice, Sequence[int]]) -> "Arrow":
202
+ """Subset an Arrow, recycling components to a common length first.
203
+
204
+ Parameters
205
+ ----------
206
+ index : int, slice, or Sequence[int]
207
+ Index(es) to select.
208
+
209
+ Returns
210
+ -------
211
+ Arrow
212
+ A new :class:`Arrow` with the selected elements.
213
+ """
214
+ maxn = len(self)
215
+
216
+ # Recycle each component to *maxn*.
217
+ angle = np.resize(self._angle, maxn)
218
+ length = _recycle_unit(self._length, maxn)
219
+ ends = np.resize(self._ends, maxn)
220
+ type_ = np.resize(self._type, maxn)
221
+
222
+ # Apply the index.
223
+ new = object.__new__(Arrow)
224
+ new._angle = np.atleast_1d(angle[index])
225
+ new._length = length[index]
226
+ new._ends = np.atleast_1d(ends[index])
227
+ new._type = np.atleast_1d(type_[index])
228
+ return new
229
+
230
+ # ------------------------------------------------------------------
231
+ # rep -- mirrors R's rep.arrow
232
+ # ------------------------------------------------------------------
233
+
234
+ def rep(self, times: int = 1, length_out: Optional[int] = None) -> "Arrow":
235
+ """Repeat the Arrow, recycling components to a common length first.
236
+
237
+ Parameters
238
+ ----------
239
+ times : int, optional
240
+ Number of times to repeat (default ``1``).
241
+ length_out : int, optional
242
+ Desired length of the result. If given, *times* is ignored and
243
+ the output is truncated or recycled to this length.
244
+
245
+ Returns
246
+ -------
247
+ Arrow
248
+ A new :class:`Arrow` with repeated elements.
249
+ """
250
+ maxn = len(self)
251
+
252
+ # First recycle components to the common length.
253
+ angle = np.resize(self._angle, maxn)
254
+ ends = np.resize(self._ends, maxn)
255
+ type_ = np.resize(self._type, maxn)
256
+ length = _recycle_unit(self._length, maxn)
257
+
258
+ # Then tile by *times*.
259
+ angle = np.tile(angle, times)
260
+ ends = np.tile(ends, times)
261
+ type_ = np.tile(type_, times)
262
+ length = unit_rep(length, times)
263
+
264
+ # Trim / recycle to *length_out* if requested.
265
+ if length_out is not None:
266
+ angle = np.resize(angle, length_out)
267
+ ends = np.resize(ends, length_out)
268
+ type_ = np.resize(type_, length_out)
269
+ length = _recycle_unit(length, length_out)
270
+
271
+ new = object.__new__(Arrow)
272
+ new._angle = angle
273
+ new._length = length
274
+ new._ends = ends
275
+ new._type = type_
276
+ return new
277
+
278
+ # ------------------------------------------------------------------
279
+ # repr
280
+ # ------------------------------------------------------------------
281
+
282
+ def __repr__(self) -> str:
283
+ return (
284
+ f"Arrow(angle={self._angle.tolist()}, "
285
+ f"length={self._length!r}, "
286
+ f"ends={self._ends.tolist()}, "
287
+ f"type={self._type.tolist()})"
288
+ )
289
+
290
+
291
+ # ----------------------------------------------------------------------
292
+ # Factory function
293
+ # ----------------------------------------------------------------------
294
+
295
+
296
+ def arrow(
297
+ angle: Union[float, int, Sequence[float]] = 30,
298
+ length: Optional[Unit] = None,
299
+ ends: Union[str, Sequence[str]] = "last",
300
+ type: Union[str, Sequence[str]] = "open", # noqa: A002
301
+ ) -> Arrow:
302
+ """Create an Arrow specification.
303
+
304
+ This is the main user-facing factory function, equivalent to R's
305
+ ``grid::arrow()``.
306
+
307
+ Parameters
308
+ ----------
309
+ angle : float or Sequence[float], optional
310
+ Angle of the arrow head in degrees (default ``30``).
311
+ length : Unit, optional
312
+ Length of the arrow head edges. Defaults to
313
+ ``Unit(0.25, "inches")``.
314
+ ends : {"first", "last", "both"} or Sequence[str], optional
315
+ Which end(s) of the line receive an arrow head (default ``"last"``).
316
+ type : {"open", "closed"} or Sequence[str], optional
317
+ Arrow head style (default ``"open"``).
318
+
319
+ Returns
320
+ -------
321
+ Arrow
322
+ A new :class:`Arrow` instance.
323
+
324
+ Examples
325
+ --------
326
+ >>> from grid_py._arrow import arrow
327
+ >>> a = arrow(angle=45, type="closed")
328
+ >>> a
329
+ Arrow(angle=[45.0], length=Unit([0.25], 'inches'), ends=[2], type=[2])
330
+ """
331
+ return Arrow(angle=angle, length=length, ends=ends, type=type)