drawlib 0.2.3.dev2__py3-none-any.whl → 0.2.3.dev3__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.

Potentially problematic release.


This version of drawlib might be problematic. Click here for more details.

@@ -10,13 +10,18 @@
10
10
 
11
11
  """Canvas's original arrow feature implementation module."""
12
12
 
13
- from typing import Literal, Optional, Tuple, Union
13
+ import math
14
+ from typing import List, Literal, Optional, Tuple, Union
15
+
16
+ from matplotlib.patches import PathPatch
17
+ from matplotlib.path import Path
14
18
 
15
19
  import drawlib.v0_2.private.validators.args as validator
16
20
  from drawlib.v0_2.private.core.model import ShapeStyle, ShapeTextStyle
17
21
  from drawlib.v0_2.private.core.theme import dtheme
18
22
  from drawlib.v0_2.private.core.util import ShapeUtil
19
23
  from drawlib.v0_2.private.core_canvas.base import CanvasBase
24
+ from drawlib.v0_2.private.core_canvas.line import LineArcHelper
20
25
  from drawlib.v0_2.private.util import (
21
26
  error_handler,
22
27
  get_angle,
@@ -137,3 +142,495 @@ class CanvasOriginalArrowFeature(CanvasBase):
137
142
  textsize=textsize,
138
143
  textstyle=textstyle,
139
144
  )
145
+
146
+ @error_handler
147
+ def arrows(
148
+ self,
149
+ xys: List[Tuple[float, float]],
150
+ tail_width: float,
151
+ head_width: float,
152
+ head_length: float,
153
+ head: Literal[
154
+ "->",
155
+ "<-",
156
+ "<->",
157
+ ] = "->",
158
+ r: float = 0,
159
+ style: Union[ShapeStyle, str, None] = None,
160
+ ) -> None:
161
+ """Draw single and double-headed arrow.
162
+
163
+ Args:
164
+ xys: List[Tuple[float, float]]: Arrow points.
165
+ tail_width: float: Width of the arrow tail.
166
+ head_width: float: Width of the arrow head.
167
+ head_length: float: Length of the arrow head.
168
+ head: Literal["->", "<-", "<->"]: Arrow head style ("->", "<-", "<->").
169
+ r (float, optional): Radius for rounded connections (default is 0.0).
170
+ style: Union[ShapeStyle, str, None]: Optional style of the arrow.
171
+
172
+ Returns:
173
+ None
174
+ """
175
+ # matplotlib FancyArrow, FancyArrowPatch seems not good
176
+ # for implement this function.
177
+ # Calculate arrow points pass it to shape().
178
+
179
+ style, _ = ShapeUtil.format_styles(
180
+ style,
181
+ None,
182
+ dtheme.arrowstyles.get,
183
+ dtheme.arrowtextstyles.get,
184
+ )
185
+ validator.validate_shape_args(locals())
186
+
187
+ parallel_xys1 = ArrowsHelper.get_parallel_lines_xys(xys, tail_width / 2)
188
+ parallel_xys2 = ArrowsHelper.get_parallel_lines_xys(xys, tail_width / 2 * -1)
189
+
190
+ arrowhead_start_tail_xy1 = ArrowsHelper.get_point_on_line(parallel_xys1[0], parallel_xys1[1], head_length)
191
+ arrowhead_start_tail_xy2 = ArrowsHelper.get_point_on_line(parallel_xys2[0], parallel_xys2[1], head_length)
192
+ arrowhead_end_tail_xy1 = ArrowsHelper.get_point_on_line(
193
+ parallel_xys1[len(parallel_xys1) - 1],
194
+ parallel_xys1[len(parallel_xys1) - 2],
195
+ head_length,
196
+ )
197
+ arrowhead_end_tail_xy2 = ArrowsHelper.get_point_on_line(
198
+ parallel_xys2[len(parallel_xys1) - 1],
199
+ parallel_xys2[len(parallel_xys1) - 2],
200
+ head_length,
201
+ )
202
+
203
+ xy1, xy2 = ArrowsHelper.get_parallel_line_xys(xys[0], xys[1], head_width / 2)
204
+ arrowhead_start_head_xy1 = ArrowsHelper.get_point_on_line(xy1, xy2, head_length)
205
+ xy1, xy2 = ArrowsHelper.get_parallel_line_xys(xys[0], xys[1], head_width / 2 * -1)
206
+ arrowhead_start_head_xy2 = ArrowsHelper.get_point_on_line(xy1, xy2, head_length)
207
+ xy1, xy2 = ArrowsHelper.get_parallel_line_xys(
208
+ xys[len(xys) - 1], xys[len(xys) - 2], head_width / 2 * -1
209
+ ) # since poinrts are reversed, having * -1 here
210
+ arrowhead_end_head_xy1 = ArrowsHelper.get_point_on_line(xy1, xy2, head_length)
211
+ xy1, xy2 = ArrowsHelper.get_parallel_line_xys(xys[len(xys) - 1], xys[len(xys) - 2], head_width / 2)
212
+ arrowhead_end_head_xy2 = ArrowsHelper.get_point_on_line(xy1, xy2, head_length)
213
+
214
+ points1: List[Tuple[float, float]] = []
215
+ if head == "->":
216
+ points1.append(parallel_xys1[0])
217
+ else:
218
+ points1.append(arrowhead_start_tail_xy1)
219
+ points1.extend(parallel_xys1[1:-1])
220
+ if head == "<-":
221
+ points1.append(parallel_xys1[len(parallel_xys1) - 1])
222
+ else:
223
+ points1.append(arrowhead_end_tail_xy1)
224
+
225
+ points2: List[Tuple[float, float]] = []
226
+ if head == "<-":
227
+ points2.append(parallel_xys2[len(parallel_xys2) - 1])
228
+ else:
229
+ points2.append(arrowhead_end_tail_xy2)
230
+ temp = parallel_xys2[1:-1]
231
+ temp.reverse()
232
+ points2.extend(temp)
233
+ if head == "->":
234
+ points2.append(parallel_xys2[0])
235
+ else:
236
+ points2.append(arrowhead_start_tail_xy2)
237
+
238
+ # apply r here
239
+ if r != 0:
240
+ path_points1: List[
241
+ Union[
242
+ Tuple[float, float],
243
+ Tuple[Tuple[float, float], Tuple[float, float]],
244
+ Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]],
245
+ ]
246
+ ] = ArrowsHelper.get_path_points(points1, r)
247
+ path_points2: List[
248
+ Union[
249
+ Tuple[float, float],
250
+ Tuple[Tuple[float, float], Tuple[float, float]],
251
+ Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]],
252
+ ]
253
+ ] = ArrowsHelper.get_path_points(points2, r)
254
+ else:
255
+ path_points1 = points1 # type: ignore
256
+ path_points2 = points2 # type: ignore
257
+
258
+ if head != "->":
259
+ path_points1.insert(0, xys[0])
260
+ path_points1.insert(1, arrowhead_start_head_xy1)
261
+ if head != "<-":
262
+ path_points1.append(arrowhead_end_head_xy1)
263
+ path_points1.append(xys[len(xys) - 1])
264
+
265
+ if head != "<-":
266
+ path_points2.insert(0, arrowhead_end_head_xy2)
267
+ if head != "->":
268
+ path_points2.append(arrowhead_start_head_xy2)
269
+
270
+ path_points1.extend(path_points2)
271
+
272
+ # create Path
273
+ vertices = [path_points1[0]]
274
+ codes = [Path.MOVETO]
275
+ for p in path_points1[1:]:
276
+ length = len(p)
277
+ if length not in {2, 3}:
278
+ raise ValueError()
279
+
280
+ if not isinstance(p[0], tuple):
281
+ vertices.append(p)
282
+ codes.append(Path.LINETO)
283
+
284
+ elif length == 2:
285
+ vertices.extend([p[0], p[1]]) # type: ignore
286
+ codes.extend([Path.CURVE3] * 2)
287
+
288
+ else:
289
+ vertices.extend([p[0], p[1], p[2]]) # type: ignore
290
+ codes.extend([Path.CURVE4] * 3)
291
+
292
+ vertices.append(path_points1[0])
293
+ codes.append(Path.CLOSEPOLY)
294
+ path = Path(vertices=vertices, codes=codes)
295
+
296
+ # create PathPatch
297
+
298
+ options = ShapeUtil.get_shape_options(style)
299
+ self._artists.append(PathPatch(path=path, **options))
300
+
301
+ @error_handler
302
+ def arrow_arc(
303
+ self,
304
+ xy: Tuple[float, float],
305
+ width: float,
306
+ height: float,
307
+ tail_width: float,
308
+ head_width: float,
309
+ head_angle: float = 10,
310
+ head: Literal["->", "<-", "<->"] = "->",
311
+ from_angle: float = 0,
312
+ to_angle: float = 180,
313
+ style: Union[ShapeStyle, str, None] = None,
314
+ ) -> None:
315
+ """Draw arc arrow.
316
+
317
+ Args:
318
+ xy: Tuple[float, float]: The center point of the circle from which the arc arrow is drawn.
319
+ width: float: The width of the ellipse.
320
+ height: float: The height of the ellipse
321
+ tail_width: float: The width of the tail of the arrow.
322
+ head_width: float: The width of the head of the arrow.
323
+ head_angle: float: The angle of the arrowhead in degrees (default is 10).
324
+ head: Literal["->", "<-", "<->"]: The arrowhead style ("->", "<-", "<->").
325
+ from_angle: float: The starting angle of the arc in degrees (default is 0).
326
+ to_angle: float: The ending angle of the arc in degrees (default is 180).
327
+ style: Union[ShapeStyle, str, None]: Optional shape style.
328
+
329
+ Returns:
330
+ None
331
+ """
332
+ style, _ = ShapeUtil.format_styles(
333
+ style,
334
+ None,
335
+ dtheme.arrowstyles.get,
336
+ dtheme.arrowtextstyles.get,
337
+ )
338
+ validator.validate_shape_args(locals())
339
+
340
+ width_int = width - tail_width
341
+ width_ext = width + tail_width
342
+ height_int = height - tail_width
343
+ height_ext = height + tail_width
344
+
345
+ if head in {"<-", "<->"}:
346
+ from_angle2 = from_angle + head_angle
347
+ from_angle2 %= 360
348
+ else:
349
+ from_angle2 = from_angle
350
+
351
+ if head in {"->", "<->"}:
352
+ to_angle2 = to_angle - head_angle
353
+ to_angle2 %= 360
354
+ else:
355
+ to_angle2 = to_angle
356
+
357
+ if from_angle2 > to_angle2:
358
+ pp1_fa = -1 * (360 - from_angle2)
359
+ pp1_ta = to_angle2
360
+ pp2_fa = to_angle2
361
+ pp2_ta = pp1_fa
362
+ else:
363
+ pp1_fa = from_angle2
364
+ pp1_ta = to_angle2
365
+ pp2_fa = to_angle2
366
+ pp2_ta = from_angle2
367
+
368
+ path_points1 = LineArcHelper.get_ellipse_path_points(
369
+ xy,
370
+ width_int,
371
+ height_int,
372
+ pp1_fa,
373
+ pp1_ta,
374
+ )
375
+ path_points2 = LineArcHelper.get_ellipse_path_points(
376
+ xy,
377
+ width_ext,
378
+ height_ext,
379
+ pp2_fa,
380
+ pp2_ta,
381
+ )
382
+
383
+ if from_angle > to_angle:
384
+ from_angle = -1 * (360 - from_angle)
385
+
386
+ if head in {"<-", "<->"}:
387
+ path_points1.insert(
388
+ 0,
389
+ LineArcHelper.get_point_on_ellipse(
390
+ xy,
391
+ width,
392
+ height,
393
+ from_angle,
394
+ ),
395
+ )
396
+ path_points1.insert(
397
+ 1,
398
+ LineArcHelper.get_point_on_ellipse(
399
+ xy,
400
+ width - head_width,
401
+ height - head_width,
402
+ from_angle2,
403
+ ),
404
+ )
405
+ path_points2.append(
406
+ LineArcHelper.get_point_on_ellipse(
407
+ xy,
408
+ width + head_width,
409
+ height + head_width,
410
+ from_angle2,
411
+ )
412
+ )
413
+
414
+ if head in {"->", "<->"}:
415
+ path_points1.append(
416
+ LineArcHelper.get_point_on_ellipse(
417
+ xy,
418
+ width - head_width,
419
+ height - head_width,
420
+ to_angle2,
421
+ )
422
+ )
423
+ path_points1.append(
424
+ LineArcHelper.get_point_on_ellipse(
425
+ xy,
426
+ width,
427
+ height,
428
+ to_angle,
429
+ )
430
+ )
431
+ path_points2.insert(
432
+ 0,
433
+ LineArcHelper.get_point_on_ellipse(
434
+ xy,
435
+ width + head_width,
436
+ height + head_width,
437
+ to_angle2,
438
+ ),
439
+ )
440
+
441
+ path_points1.extend(path_points2)
442
+
443
+ # create Path
444
+ vertices = [path_points1[0]]
445
+ codes = [Path.MOVETO]
446
+ for p in path_points1[1:]:
447
+ length = len(p)
448
+ if length not in {2, 3}:
449
+ raise ValueError()
450
+
451
+ if not isinstance(p[0], tuple):
452
+ vertices.append(p)
453
+ codes.append(Path.LINETO)
454
+
455
+ elif length == 2:
456
+ vertices.extend([p[0], p[1]]) # type: ignore
457
+ codes.extend([Path.CURVE3] * 2)
458
+
459
+ else:
460
+ vertices.extend([p[0], p[1], p[2]]) # type: ignore
461
+ codes.extend([Path.CURVE4] * 3)
462
+
463
+ vertices.append(path_points1[0])
464
+ codes.append(Path.CLOSEPOLY)
465
+ path = Path(vertices=vertices, codes=codes)
466
+
467
+ # create PathPatch
468
+
469
+ options = ShapeUtil.get_shape_options(style)
470
+ self._artists.append(PathPatch(path=path, **options))
471
+
472
+
473
+ class ArrowsHelper:
474
+ """Internal class"""
475
+
476
+ @classmethod
477
+ def get_path_points(
478
+ cls,
479
+ xys: List[Tuple[float, float]],
480
+ r: float,
481
+ ) -> List[
482
+ Union[
483
+ Tuple[float, float],
484
+ Tuple[Tuple[float, float], Tuple[float, float]],
485
+ Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]],
486
+ ]
487
+ ]:
488
+ """Internal function"""
489
+
490
+ def _get_mid_points(
491
+ a: Tuple[float, float],
492
+ b: Tuple[float, float],
493
+ r: float,
494
+ ) -> Tuple[Tuple[float, float], Tuple[float, float]]:
495
+ # Calculate the vector from A to B
496
+ ab = [b[0] - a[0], b[1] - a[1]]
497
+
498
+ # Calculate the distance from A to B
499
+ ab_distance = math.sqrt(ab[0] ** 2 + ab[1] ** 2)
500
+
501
+ # Normalize the vector AB to get the unit vector
502
+ ab_unit = [ab[0] / ab_distance, ab[1] / ab_distance]
503
+
504
+ # Calculate point C: A + X * unit vector AB
505
+ c = (a[0] + r * ab_unit[0], a[1] + r * ab_unit[1])
506
+
507
+ # Calculate point D: B - X * unit vector AB
508
+ d = (b[0] - r * ab_unit[0], b[1] - r * ab_unit[1])
509
+
510
+ return c, d
511
+
512
+ path_points: List[
513
+ Union[
514
+ Tuple[float, float],
515
+ Tuple[Tuple[float, float], Tuple[float, float]],
516
+ Tuple[Tuple[float, float], Tuple[float, float], Tuple[float, float]],
517
+ ]
518
+ ] = []
519
+
520
+ last_i = len(xys) - 2
521
+ # last_xy = (0, 0)
522
+ for i, xy in enumerate(xys):
523
+ if i == 0:
524
+ _, p = _get_mid_points(xy, xys[i + 1], r)
525
+ path_points.append(p)
526
+ continue
527
+
528
+ if i == last_i:
529
+ p, _ = _get_mid_points(xy, xys[i + 1], r)
530
+ path_points.append((xy, p))
531
+ path_points.append(xys[i + 1])
532
+ break
533
+
534
+ p1, p2 = _get_mid_points(xy, xys[i + 1], r)
535
+ path_points.append((xy, p1))
536
+ path_points.append(p2)
537
+
538
+ path_points.insert(0, xys[0])
539
+ return path_points
540
+
541
+ @classmethod
542
+ def get_point_on_line(
543
+ cls,
544
+ xy1: Tuple[float, float],
545
+ xy2: Tuple[float, float],
546
+ distance: float,
547
+ ) -> Tuple[float, float]:
548
+ """Internal function"""
549
+ x1, y1 = xy1
550
+ x2, y2 = xy2
551
+
552
+ v_length = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
553
+ u_x = (x2 - x1) / v_length
554
+ u_y = (y2 - y1) / v_length
555
+
556
+ x = x1 + distance * u_x
557
+ y = y1 + distance * u_y
558
+
559
+ return (x, y)
560
+
561
+ @classmethod
562
+ def get_parallel_lines_xys(
563
+ cls,
564
+ xys: List[Tuple[float, float]],
565
+ distance: float,
566
+ ) -> List[Tuple[float, float]]:
567
+ """Internal function"""
568
+ parallel_lines: List[Tuple[Tuple[float, float], Tuple[float, float]]] = []
569
+ for i in range(len(xys) - 1):
570
+ xy1, xy2 = cls.get_parallel_line_xys(xys[i], xys[i + 1], distance)
571
+ parallel_lines.append((xy1, xy2))
572
+
573
+ points: List[Tuple[float, float]] = []
574
+ for i in range(len(parallel_lines) - 1):
575
+ xy1, xy2 = parallel_lines[i]
576
+ xy3, xy4 = parallel_lines[i + 1]
577
+ xy = cls.find_lines_intersection(xy1, xy2, xy3, xy4)
578
+ points.append(xy)
579
+
580
+ first_point = parallel_lines[0][0]
581
+ last_point = parallel_lines[len(parallel_lines) - 1][1]
582
+
583
+ points.insert(0, first_point)
584
+ points.append(last_point)
585
+
586
+ return points
587
+
588
+ @classmethod
589
+ def get_parallel_line_xys(
590
+ cls,
591
+ xy1: Tuple[float, float],
592
+ xy2: Tuple[float, float],
593
+ distance: float,
594
+ ) -> Tuple[Tuple[float, float], Tuple[float, float]]:
595
+ """Internal function"""
596
+ x1, y1 = xy1
597
+ x2, y2 = xy2
598
+
599
+ # calc vector
600
+ vx = x2 - x1
601
+ vy = y2 - y1
602
+ length = math.sqrt(vx**2 + vy**2)
603
+ dx = -vy / length * distance
604
+ dy = vx / length * distance
605
+
606
+ # add vector
607
+ x1_prime = x1 + dx
608
+ y1_prime = y1 + dy
609
+ x2_prime = x2 + dx
610
+ y2_prime = y2 + dy
611
+
612
+ return (x1_prime, y1_prime), (x2_prime, y2_prime)
613
+
614
+ @classmethod
615
+ def find_lines_intersection(
616
+ cls,
617
+ xy1: Tuple[float, float],
618
+ xy2: Tuple[float, float],
619
+ xy3: Tuple[float, float],
620
+ xy4: Tuple[float, float],
621
+ ) -> Tuple[float, float]:
622
+ """Internal function"""
623
+ x1, y1 = xy1
624
+ x2, y2 = xy2
625
+ x3, y3 = xy3
626
+ x4, y4 = xy4
627
+
628
+ m1 = (y2 - y1) / (x2 - x1)
629
+ b1 = y1 - m1 * x1
630
+ m2 = (y4 - y3) / (x4 - x3)
631
+ b2 = y3 - m2 * x3
632
+
633
+ x = (b2 - b1) / (m1 - m2)
634
+ y = m1 * x + b1
635
+
636
+ return (x, y)
@@ -0,0 +1,178 @@
1
+ # Copyright (c) 2024 Yuichi Ito (yuichi@yuichi.com)
2
+ #
3
+ # This software is licensed under the Apache License, Version 2.0.
4
+ # For more information, please visit: https://github.com/yuichi110/drawlib
5
+ #
6
+ # This software is provided "as is", without warranty of any kind,
7
+ # express or implied, including but not limited to the warranties of
8
+ # merchantability, fitness for a particular purpose and noninfringement.
9
+
10
+
11
+ """BoxList implementation module."""
12
+
13
+ from typing import Any, List, Literal, Optional, Tuple, Union
14
+
15
+ from drawlib.v0_2.private.core.colors import Colors
16
+ from drawlib.v0_2.private.core.model import ShapeStyle, ShapeTextStyle
17
+ from drawlib.v0_2.private.core.theme import dtheme
18
+ from drawlib.v0_2.private.core_canvas.canvas import rectangle
19
+ from drawlib.v0_2.private.util import error_handler
20
+
21
+
22
+ class BoxList:
23
+ """A class to draw a list of boxes with text, supporting highlighting of certain boxes.
24
+
25
+ Args:
26
+ box_style (Union[str, ShapeStyle, None]): The style for the boxes.
27
+ text_style (Union[str, ShapeTextStyle, None]): The style for the text inside the boxes.
28
+ box_highlight_style (Union[str, ShapeStyle, None]): The style for the highlighted boxes.
29
+ text_highlight_style (Union[str, ShapeTextStyle, None]): The style for the text inside the highlighted boxes.
30
+ """
31
+
32
+ @error_handler
33
+ def __init__(
34
+ self,
35
+ box_style: Union[str, ShapeStyle, None] = None,
36
+ text_style: Union[str, ShapeTextStyle, None] = None,
37
+ box_highlight_style: Union[str, ShapeStyle, None] = None,
38
+ text_highlight_style: Union[str, ShapeTextStyle, None] = None,
39
+ ) -> None:
40
+ """Initialize BoxList.
41
+
42
+ Args:
43
+ box_style (Union[str, ShapeStyle, None]): The style for the boxes.
44
+ text_style (Union[str, ShapeTextStyle, None]): The style for the text inside the boxes.
45
+ box_highlight_style (Union[str, ShapeStyle, None]): The style for the highlighted boxes.
46
+ text_highlight_style (Union[str, ShapeTextStyle, None]):
47
+ The style for the text inside the highlighted boxes.
48
+ """
49
+ if isinstance(box_style, str):
50
+ box_style = dtheme.rectanglestyles.get(box_style)
51
+ if box_style is None:
52
+ box_style = dtheme.rectanglestyles.get()
53
+ box_style.halign = "center"
54
+ box_style.valign = "center"
55
+ self._box_style = box_style
56
+
57
+ if isinstance(text_style, str):
58
+ text_style = dtheme.rectangletextstyles.get(text_style)
59
+ if text_style is None:
60
+ text_style = dtheme.rectangletextstyles.get()
61
+ self._text_style = text_style
62
+
63
+ if isinstance(box_highlight_style, str):
64
+ box_highlight_style = dtheme.rectanglestyles.get(box_highlight_style)
65
+ if box_highlight_style is None:
66
+ box_highlight_style = box_style
67
+ box_highlight_style.halign = "center"
68
+ box_highlight_style.valign = "center"
69
+ self._box_highlight_style = box_highlight_style
70
+
71
+ if isinstance(text_highlight_style, str):
72
+ text_highlight_style = dtheme.rectangletextstyles.get(text_highlight_style)
73
+ if text_highlight_style is None:
74
+ text_highlight_style = text_style
75
+ self._text_highlight_style = text_highlight_style
76
+
77
+ @error_handler
78
+ def draw(
79
+ self,
80
+ xy: Tuple[float, float],
81
+ box_width: float,
82
+ box_height: float,
83
+ items: List[Any],
84
+ highlight_indexs: Optional[List[int]] = None,
85
+ length: Optional[int] = None,
86
+ align: Literal["left", "right", "bottom", "top"] = "left",
87
+ ) -> None:
88
+ """Draws a list of boxes at the specified location.
89
+
90
+ Args:
91
+ xy (Tuple[float, float]): The starting point (x, y) to draw the list of boxes.
92
+ box_width (float): The width of each box.
93
+ box_height (float): The height of each box.
94
+ items (List[Any]): The list of items to be displayed inside the boxes.
95
+ highlight_indexs (Optional[List[int]]):
96
+ The list of indices of the boxes to be highlighted.
97
+ length (Optional[int]): The total number of boxes to be drawn.
98
+ align (Literal["left", "right", "bottom", "top"]):
99
+ The alignment of the boxes relative to the starting point.
100
+
101
+ """
102
+ if highlight_indexs is None:
103
+ highlight_indexs = []
104
+
105
+ if length is not None:
106
+ indexs = range(length)
107
+ else:
108
+ indexs = range(len(items))
109
+
110
+ non_highlight_indexs = [item for item in indexs if item not in highlight_indexs]
111
+ highlight_indexs = [item for item in indexs if item in highlight_indexs]
112
+
113
+ for index in non_highlight_indexs:
114
+ if index <= len(items) - 1:
115
+ text = str(items[index])
116
+ else:
117
+ text = ""
118
+
119
+ self._draw_cell(
120
+ start_xy=xy,
121
+ index=index,
122
+ text=text,
123
+ box_width=box_width,
124
+ box_height=box_height,
125
+ box_style=self._box_style,
126
+ text_style=self._text_style,
127
+ align=align,
128
+ )
129
+
130
+ for index in highlight_indexs:
131
+ if index <= len(items) - 1:
132
+ text = str(items[index])
133
+ else:
134
+ text = ""
135
+
136
+ self._draw_cell(
137
+ start_xy=xy,
138
+ index=index,
139
+ text=text,
140
+ box_width=box_width,
141
+ box_height=box_height,
142
+ box_style=self._box_highlight_style,
143
+ text_style=self._text_highlight_style,
144
+ align=align,
145
+ )
146
+
147
+ @staticmethod
148
+ def _draw_cell(
149
+ start_xy: Tuple[float, float],
150
+ index: int,
151
+ text: str,
152
+ box_width: float,
153
+ box_height: float,
154
+ box_style: ShapeStyle,
155
+ text_style: ShapeTextStyle,
156
+ align: Literal["left", "right", "bottom", "top"],
157
+ ) -> None:
158
+ if align == "left":
159
+ x = start_xy[0] + box_width * (index + 0.5)
160
+ y = start_xy[1] + box_height / 2
161
+ elif align == "right":
162
+ x = start_xy[0] - box_width * (index + 0.5)
163
+ y = start_xy[1] + box_height / 2
164
+ elif align == "bottom":
165
+ x = start_xy[0] + box_width / 2
166
+ y = start_xy[1] + box_height * (index + 0.5)
167
+ else:
168
+ x = start_xy[0] + box_width / 2
169
+ y = start_xy[1] - box_height * (index + 0.5)
170
+
171
+ rectangle(
172
+ xy=(x, y),
173
+ width=box_width,
174
+ height=box_height,
175
+ style=box_style,
176
+ text=text,
177
+ textstyle=text_style,
178
+ )