plotnine 0.15.0.dev1__py3-none-any.whl → 0.15.0.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.
- plotnine/_mpl/layout_manager/_layout_items.py +219 -118
- plotnine/_mpl/layout_manager/_layout_tree.py +168 -115
- plotnine/_mpl/layout_manager/_spaces.py +22 -9
- plotnine/_mpl/patches.py +1 -1
- plotnine/_mpl/text.py +59 -24
- plotnine/_utils/__init__.py +1 -1
- plotnine/facets/strips.py +5 -2
- plotnine/geoms/geom_bar.py +10 -2
- plotnine/geoms/geom_col.py +6 -0
- plotnine/geoms/geom_violin.py +24 -7
- plotnine/guides/guide.py +2 -2
- plotnine/guides/guide_legend.py +7 -8
- plotnine/iapi.py +4 -2
- plotnine/mapping/_eval_environment.py +85 -0
- plotnine/mapping/aes.py +10 -26
- plotnine/mapping/evaluation.py +7 -65
- plotnine/plot_composition/_compose.py +13 -4
- plotnine/stats/stat_sina.py +33 -0
- plotnine/themes/elements/element_text.py +1 -0
- plotnine/themes/targets.py +1 -1
- plotnine/themes/theme_gray.py +1 -0
- plotnine/themes/themeable.py +5 -5
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/METADATA +1 -1
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/RECORD +27 -26
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/WHEEL +1 -1
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {plotnine-0.15.0.dev1.dist-info → plotnine-0.15.0.dev3.dist-info}/top_level.txt +0 -0
|
@@ -299,40 +299,6 @@ class LayoutItems:
|
|
|
299
299
|
|
|
300
300
|
return chain(major, minor)
|
|
301
301
|
|
|
302
|
-
def axis_text_x_margin(self, ax: Axes) -> Iterator[float]:
|
|
303
|
-
"""
|
|
304
|
-
Return XTicks paddings
|
|
305
|
-
"""
|
|
306
|
-
# In plotnine tick padding are specified as a margin to the
|
|
307
|
-
# the axis_text.
|
|
308
|
-
major, minor = [], []
|
|
309
|
-
if not self._is_blank("axis_text_x"):
|
|
310
|
-
h = self.plot.figure.bbox.height
|
|
311
|
-
major = [
|
|
312
|
-
(t.get_pad() or 0) / h for t in ax.xaxis.get_major_ticks()
|
|
313
|
-
]
|
|
314
|
-
minor = [
|
|
315
|
-
(t.get_pad() or 0) / h for t in ax.xaxis.get_minor_ticks()
|
|
316
|
-
]
|
|
317
|
-
return chain(major, minor)
|
|
318
|
-
|
|
319
|
-
def axis_text_y_margin(self, ax: Axes) -> Iterator[float]:
|
|
320
|
-
"""
|
|
321
|
-
Return YTicks paddings
|
|
322
|
-
"""
|
|
323
|
-
# In plotnine tick padding are specified as a margin to the
|
|
324
|
-
# the axis_text.
|
|
325
|
-
major, minor = [], []
|
|
326
|
-
if not self._is_blank("axis_text_y"):
|
|
327
|
-
w = self.plot.figure.bbox.width
|
|
328
|
-
major = [
|
|
329
|
-
(t.get_pad() or 0) / w for t in ax.yaxis.get_major_ticks()
|
|
330
|
-
]
|
|
331
|
-
minor = [
|
|
332
|
-
(t.get_pad() or 0) / w for t in ax.yaxis.get_minor_ticks()
|
|
333
|
-
]
|
|
334
|
-
return chain(major, minor)
|
|
335
|
-
|
|
336
302
|
def strip_text_x_height(self, position: StripPosition) -> float:
|
|
337
303
|
"""
|
|
338
304
|
Height taken up by the top strips
|
|
@@ -361,9 +327,9 @@ class LayoutItems:
|
|
|
361
327
|
]
|
|
362
328
|
return self.calc.max_width(artists)
|
|
363
329
|
|
|
364
|
-
def
|
|
330
|
+
def axis_ticks_x_max_height_at(self, location: AxesLocation) -> float:
|
|
365
331
|
"""
|
|
366
|
-
Return maximum height[
|
|
332
|
+
Return maximum height[figure space] of x ticks
|
|
367
333
|
"""
|
|
368
334
|
heights = [
|
|
369
335
|
self.calc.tight_height(tick.tick1line)
|
|
@@ -372,22 +338,28 @@ class LayoutItems:
|
|
|
372
338
|
]
|
|
373
339
|
return max(heights) if len(heights) else 0
|
|
374
340
|
|
|
375
|
-
def axis_text_x_max_height(self,
|
|
341
|
+
def axis_text_x_max_height(self, ax: Axes) -> float:
|
|
376
342
|
"""
|
|
377
|
-
Return maximum height[
|
|
343
|
+
Return maximum height[figure space] of x tick labels
|
|
378
344
|
"""
|
|
379
345
|
heights = [
|
|
380
|
-
self.calc.tight_height(label)
|
|
346
|
+
self.calc.tight_height(label) for label in self.axis_text_x(ax)
|
|
347
|
+
]
|
|
348
|
+
return max(heights) if len(heights) else 0
|
|
349
|
+
|
|
350
|
+
def axis_text_x_max_height_at(self, location: AxesLocation) -> float:
|
|
351
|
+
"""
|
|
352
|
+
Return maximum height[figure space] of x tick labels
|
|
353
|
+
"""
|
|
354
|
+
heights = [
|
|
355
|
+
self.axis_text_x_max_height(ax)
|
|
381
356
|
for ax in self._filter_axes(location)
|
|
382
|
-
for label, pad in zip(
|
|
383
|
-
self.axis_text_x(ax), self.axis_text_x_margin(ax)
|
|
384
|
-
)
|
|
385
357
|
]
|
|
386
358
|
return max(heights) if len(heights) else 0
|
|
387
359
|
|
|
388
|
-
def
|
|
360
|
+
def axis_ticks_y_max_width_at(self, location: AxesLocation) -> float:
|
|
389
361
|
"""
|
|
390
|
-
Return maximum width[
|
|
362
|
+
Return maximum width[figure space] of y ticks
|
|
391
363
|
"""
|
|
392
364
|
widths = [
|
|
393
365
|
self.calc.tight_width(tick.tick1line)
|
|
@@ -396,22 +368,28 @@ class LayoutItems:
|
|
|
396
368
|
]
|
|
397
369
|
return max(widths) if len(widths) else 0
|
|
398
370
|
|
|
399
|
-
def axis_text_y_max_width(self,
|
|
371
|
+
def axis_text_y_max_width(self, ax: Axes) -> float:
|
|
372
|
+
"""
|
|
373
|
+
Return maximum width[figure space] of y tick labels
|
|
374
|
+
"""
|
|
375
|
+
widths = [
|
|
376
|
+
self.calc.tight_width(label) for label in self.axis_text_y(ax)
|
|
377
|
+
]
|
|
378
|
+
return max(widths) if len(widths) else 0
|
|
379
|
+
|
|
380
|
+
def axis_text_y_max_width_at(self, location: AxesLocation) -> float:
|
|
400
381
|
"""
|
|
401
|
-
Return maximum width[
|
|
382
|
+
Return maximum width[figure space] of y tick labels
|
|
402
383
|
"""
|
|
403
384
|
widths = [
|
|
404
|
-
self.
|
|
385
|
+
self.axis_text_y_max_width(ax)
|
|
405
386
|
for ax in self._filter_axes(location)
|
|
406
|
-
for label, pad in zip(
|
|
407
|
-
self.axis_text_y(ax), self.axis_text_y_margin(ax)
|
|
408
|
-
)
|
|
409
387
|
]
|
|
410
388
|
return max(widths) if len(widths) else 0
|
|
411
389
|
|
|
412
390
|
def axis_text_y_top_protrusion(self, location: AxesLocation) -> float:
|
|
413
391
|
"""
|
|
414
|
-
Return maximum height[
|
|
392
|
+
Return maximum height[figure space] above the axes of y tick labels
|
|
415
393
|
"""
|
|
416
394
|
extras = []
|
|
417
395
|
for ax in self._filter_axes(location):
|
|
@@ -424,7 +402,7 @@ class LayoutItems:
|
|
|
424
402
|
|
|
425
403
|
def axis_text_y_bottom_protrusion(self, location: AxesLocation) -> float:
|
|
426
404
|
"""
|
|
427
|
-
Return maximum height[
|
|
405
|
+
Return maximum height[figure space] below the axes of y tick labels
|
|
428
406
|
"""
|
|
429
407
|
extras = []
|
|
430
408
|
for ax in self._filter_axes(location):
|
|
@@ -438,7 +416,7 @@ class LayoutItems:
|
|
|
438
416
|
|
|
439
417
|
def axis_text_x_left_protrusion(self, location: AxesLocation) -> float:
|
|
440
418
|
"""
|
|
441
|
-
Return maximum width[
|
|
419
|
+
Return maximum width[figure space] left of the axes of x tick labels
|
|
442
420
|
"""
|
|
443
421
|
extras = []
|
|
444
422
|
for ax in self._filter_axes(location):
|
|
@@ -452,7 +430,7 @@ class LayoutItems:
|
|
|
452
430
|
|
|
453
431
|
def axis_text_x_right_protrusion(self, location: AxesLocation) -> float:
|
|
454
432
|
"""
|
|
455
|
-
Return maximum width[
|
|
433
|
+
Return maximum width[figure space] right of the axes of y tick labels
|
|
456
434
|
"""
|
|
457
435
|
extras = []
|
|
458
436
|
for ax in self._filter_axes(location):
|
|
@@ -470,6 +448,7 @@ class LayoutItems:
|
|
|
470
448
|
theme = self.plot.theme
|
|
471
449
|
plot_title_position = theme.getp("plot_title_position", "panel")
|
|
472
450
|
plot_caption_position = theme.getp("plot_caption_position", "panel")
|
|
451
|
+
justify = TextJustifier(spaces)
|
|
473
452
|
|
|
474
453
|
if self.plot_tag:
|
|
475
454
|
set_plot_tag_position(self.plot_tag, spaces)
|
|
@@ -477,37 +456,124 @@ class LayoutItems:
|
|
|
477
456
|
if self.plot_title:
|
|
478
457
|
ha = theme.getp(("plot_title", "ha"))
|
|
479
458
|
self.plot_title.set_y(spaces.t.y2("plot_title"))
|
|
480
|
-
|
|
481
|
-
self.plot_title, ha,
|
|
459
|
+
justify.horizontally_about(
|
|
460
|
+
self.plot_title, ha, plot_title_position
|
|
482
461
|
)
|
|
483
462
|
|
|
484
463
|
if self.plot_subtitle:
|
|
485
464
|
ha = theme.getp(("plot_subtitle", "ha"))
|
|
486
465
|
self.plot_subtitle.set_y(spaces.t.y2("plot_subtitle"))
|
|
487
|
-
|
|
488
|
-
self.plot_subtitle, ha,
|
|
466
|
+
justify.horizontally_about(
|
|
467
|
+
self.plot_subtitle, ha, plot_title_position
|
|
489
468
|
)
|
|
490
469
|
|
|
491
470
|
if self.plot_caption:
|
|
492
471
|
ha = theme.getp(("plot_caption", "ha"), "right")
|
|
493
472
|
self.plot_caption.set_y(spaces.b.y1("plot_caption"))
|
|
494
|
-
|
|
495
|
-
self.plot_caption, ha,
|
|
473
|
+
justify.horizontally_about(
|
|
474
|
+
self.plot_caption, ha, plot_caption_position
|
|
496
475
|
)
|
|
497
476
|
|
|
498
477
|
if self.axis_title_x:
|
|
499
478
|
ha = theme.getp(("axis_title_x", "ha"), "center")
|
|
500
479
|
self.axis_title_x.set_y(spaces.b.y1("axis_title_x"))
|
|
501
|
-
|
|
480
|
+
justify.horizontally_about(self.axis_title_x, ha, "panel")
|
|
502
481
|
|
|
503
482
|
if self.axis_title_y:
|
|
504
483
|
va = theme.getp(("axis_title_y", "va"), "center")
|
|
505
484
|
self.axis_title_y.set_x(spaces.l.x1("axis_title_y"))
|
|
506
|
-
|
|
485
|
+
justify.vertically_about(self.axis_title_y, va, "panel")
|
|
507
486
|
|
|
508
487
|
if self.legends:
|
|
509
488
|
set_legends_position(self.legends, spaces)
|
|
510
489
|
|
|
490
|
+
self._adjust_axis_text_x(justify)
|
|
491
|
+
self._adjust_axis_text_y(justify)
|
|
492
|
+
|
|
493
|
+
def _adjust_axis_text_x(self, justify: TextJustifier):
|
|
494
|
+
"""
|
|
495
|
+
Adjust x-axis text, justifying vertically as necessary
|
|
496
|
+
"""
|
|
497
|
+
|
|
498
|
+
def to_vertical_axis_dimensions(value: float, ax: Axes) -> float:
|
|
499
|
+
"""
|
|
500
|
+
Convert value in figure dimensions to axis dimensions
|
|
501
|
+
"""
|
|
502
|
+
_, H = self.plot.figure.bbox.size
|
|
503
|
+
h = ax.get_window_extent().height
|
|
504
|
+
return value * H / h
|
|
505
|
+
|
|
506
|
+
if self._is_blank("axis_text_x"):
|
|
507
|
+
return
|
|
508
|
+
|
|
509
|
+
va = self.plot.theme.getp(("axis_text_x", "va"), "top")
|
|
510
|
+
|
|
511
|
+
for ax in self.plot.axs:
|
|
512
|
+
texts = list(self.axis_text_x(ax))
|
|
513
|
+
axis_text_row_height = to_vertical_axis_dimensions(
|
|
514
|
+
self.axis_text_x_max_height(ax), ax
|
|
515
|
+
)
|
|
516
|
+
for text in texts:
|
|
517
|
+
height = to_vertical_axis_dimensions(
|
|
518
|
+
self.calc.tight_height(text), ax
|
|
519
|
+
)
|
|
520
|
+
justify.vertically(
|
|
521
|
+
text, va, -axis_text_row_height, 0, height=height
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
def _adjust_axis_text_y(self, justify: TextJustifier):
|
|
525
|
+
"""
|
|
526
|
+
Adjust x-axis text, justifying horizontally as necessary
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
def to_horizontal_axis_dimensions(value: float, ax: Axes) -> float:
|
|
530
|
+
"""
|
|
531
|
+
Convert value in figure dimensions to axis dimensions
|
|
532
|
+
|
|
533
|
+
Matplotlib expects x position of y-axis text is in transAxes,
|
|
534
|
+
but all our layout measurements are in transFigure.
|
|
535
|
+
|
|
536
|
+
---------------------
|
|
537
|
+
| |
|
|
538
|
+
| ----------- |
|
|
539
|
+
| X | | |
|
|
540
|
+
| X | | |
|
|
541
|
+
| X | | |
|
|
542
|
+
| X | | |
|
|
543
|
+
| X | | |
|
|
544
|
+
| X | | |
|
|
545
|
+
| 0-----------1 |
|
|
546
|
+
| axes |
|
|
547
|
+
| |
|
|
548
|
+
0---------------------1
|
|
549
|
+
figure
|
|
550
|
+
|
|
551
|
+
We do not set the transform to transFigure because, then we need
|
|
552
|
+
to calculate the position in transFigure; accounting for all the
|
|
553
|
+
space wherever the panel may be.
|
|
554
|
+
"""
|
|
555
|
+
W, _ = self.plot.figure.bbox.size
|
|
556
|
+
w = ax.get_window_extent().width
|
|
557
|
+
return value * W / w
|
|
558
|
+
|
|
559
|
+
if self._is_blank("axis_text_y"):
|
|
560
|
+
return
|
|
561
|
+
|
|
562
|
+
ha = self.plot.theme.getp(("axis_text_y", "ha"), "right")
|
|
563
|
+
|
|
564
|
+
for ax in self.plot.axs:
|
|
565
|
+
texts = list(self.axis_text_y(ax))
|
|
566
|
+
axis_text_col_width = to_horizontal_axis_dimensions(
|
|
567
|
+
self.axis_text_y_max_width(ax), ax
|
|
568
|
+
)
|
|
569
|
+
for text in texts:
|
|
570
|
+
width = to_horizontal_axis_dimensions(
|
|
571
|
+
self.calc.tight_width(text), ax
|
|
572
|
+
)
|
|
573
|
+
justify.horizontally(
|
|
574
|
+
text, ha, -axis_text_col_width, 0, width=width
|
|
575
|
+
)
|
|
576
|
+
|
|
511
577
|
|
|
512
578
|
def _text_is_visible(text: Text) -> bool:
|
|
513
579
|
"""
|
|
@@ -516,54 +582,47 @@ def _text_is_visible(text: Text) -> bool:
|
|
|
516
582
|
return text.get_visible() and text._text # type: ignore
|
|
517
583
|
|
|
518
584
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
ha: str | float,
|
|
522
|
-
spaces: LayoutSpaces,
|
|
523
|
-
how: Literal["panel", "plot"] = "panel",
|
|
524
|
-
):
|
|
585
|
+
@dataclass
|
|
586
|
+
class TextJustifier:
|
|
525
587
|
"""
|
|
526
|
-
|
|
588
|
+
Justify Text
|
|
527
589
|
|
|
528
|
-
|
|
529
|
-
|
|
590
|
+
The justification methods reinterpret alignment values to be justification
|
|
591
|
+
about a span.
|
|
530
592
|
"""
|
|
531
|
-
if isinstance(ha, str):
|
|
532
|
-
lookup = {
|
|
533
|
-
"left": 0.0,
|
|
534
|
-
"center": 0.5,
|
|
535
|
-
"right": 1.0,
|
|
536
|
-
}
|
|
537
|
-
rel = lookup[ha]
|
|
538
|
-
else:
|
|
539
|
-
rel = ha
|
|
540
|
-
|
|
541
|
-
if how == "panel":
|
|
542
|
-
left = spaces.l.left
|
|
543
|
-
right = spaces.r.right
|
|
544
|
-
else:
|
|
545
|
-
left = spaces.l.plot_left
|
|
546
|
-
right = spaces.r.plot_right
|
|
547
|
-
|
|
548
|
-
width = spaces.items.calc.width(text)
|
|
549
|
-
x = rel_position(rel, width, left, right)
|
|
550
|
-
text.set_x(x)
|
|
551
|
-
text.set_horizontalalignment("left")
|
|
552
593
|
|
|
594
|
+
spaces: LayoutSpaces
|
|
553
595
|
|
|
554
|
-
def
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
596
|
+
def horizontally(
|
|
597
|
+
self,
|
|
598
|
+
text: Text,
|
|
599
|
+
ha: str | float,
|
|
600
|
+
left: float,
|
|
601
|
+
right: float,
|
|
602
|
+
width: float | None = None,
|
|
603
|
+
):
|
|
604
|
+
"""
|
|
605
|
+
Horizontally Justify text between left and right
|
|
606
|
+
"""
|
|
607
|
+
lookup = {"left": 0.0, "center": 0.5, "right": 1.0}
|
|
608
|
+
rel = lookup.get(ha, ha) # pyright: ignore[reportCallIssue, reportArgumentType]
|
|
609
|
+
if width is None:
|
|
610
|
+
width = self.spaces.items.calc.width(text)
|
|
611
|
+
x = rel_position(rel, width, left, right)
|
|
612
|
+
text.set_x(x)
|
|
613
|
+
text.set_horizontalalignment("left")
|
|
614
|
+
|
|
615
|
+
def vertically(
|
|
616
|
+
self,
|
|
617
|
+
text: Text,
|
|
618
|
+
va: str | float,
|
|
619
|
+
bottom: float,
|
|
620
|
+
top: float,
|
|
621
|
+
height: float | None = None,
|
|
622
|
+
):
|
|
623
|
+
"""
|
|
624
|
+
Vertically Justify text between bottom and top
|
|
625
|
+
"""
|
|
567
626
|
lookup = {
|
|
568
627
|
"top": 1.0,
|
|
569
628
|
"center": 0.5,
|
|
@@ -571,21 +630,63 @@ def vertically_align_text(
|
|
|
571
630
|
"center_baseline": 0.5,
|
|
572
631
|
"bottom": 0.0,
|
|
573
632
|
}
|
|
574
|
-
rel = lookup[
|
|
575
|
-
else:
|
|
576
|
-
rel = va
|
|
633
|
+
rel = lookup.get(va, va) # pyright: ignore[reportCallIssue, reportArgumentType]
|
|
577
634
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
635
|
+
if height is None:
|
|
636
|
+
height = self.spaces.items.calc.height(text)
|
|
637
|
+
y = rel_position(rel, height, bottom, top)
|
|
638
|
+
text.set_y(y)
|
|
639
|
+
text.set_verticalalignment("bottom")
|
|
640
|
+
|
|
641
|
+
def horizontally_across_panel(self, text: Text, ha: str | float):
|
|
642
|
+
"""
|
|
643
|
+
Horizontally Justify text accross the panel(s) width
|
|
644
|
+
"""
|
|
645
|
+
self.horizontally(text, ha, self.spaces.l.left, self.spaces.r.right)
|
|
646
|
+
|
|
647
|
+
def horizontally_across_plot(self, text: Text, ha: str | float):
|
|
648
|
+
"""
|
|
649
|
+
Horizontally Justify text across the plot's width
|
|
650
|
+
"""
|
|
651
|
+
self.horizontally(
|
|
652
|
+
text, ha, self.spaces.l.plot_left, self.spaces.r.plot_right
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
def vertically_along_panel(self, text: Text, va: str | float):
|
|
656
|
+
"""
|
|
657
|
+
Horizontally Justify text along the panel(s) height
|
|
658
|
+
"""
|
|
659
|
+
self.vertically(text, va, self.spaces.b.bottom, self.spaces.t.top)
|
|
660
|
+
|
|
661
|
+
def vertically_along_plot(self, text: Text, va: str | float):
|
|
662
|
+
"""
|
|
663
|
+
Vertically Justify text along the plot's height
|
|
664
|
+
"""
|
|
665
|
+
self.vertically(
|
|
666
|
+
text, va, self.spaces.b.plot_bottom, self.spaces.t.plot_top
|
|
667
|
+
)
|
|
584
668
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
669
|
+
def horizontally_about(
|
|
670
|
+
self, text: Text, ratio: float, how: Literal["panel", "plot"]
|
|
671
|
+
):
|
|
672
|
+
"""
|
|
673
|
+
Horizontally Justify text across the panel or plot
|
|
674
|
+
"""
|
|
675
|
+
if how == "panel":
|
|
676
|
+
self.horizontally_across_panel(text, ratio)
|
|
677
|
+
else:
|
|
678
|
+
self.horizontally_across_plot(text, ratio)
|
|
679
|
+
|
|
680
|
+
def vertically_about(
|
|
681
|
+
self, text: Text, ratio: float, how: Literal["panel", "plot"]
|
|
682
|
+
):
|
|
683
|
+
"""
|
|
684
|
+
Vertically Justify text along the panel or plot
|
|
685
|
+
"""
|
|
686
|
+
if how == "panel":
|
|
687
|
+
self.vertically_along_panel(text, ratio)
|
|
688
|
+
else:
|
|
689
|
+
self.vertically_along_plot(text, ratio)
|
|
589
690
|
|
|
590
691
|
|
|
591
692
|
def set_legends_position(legends: legend_artists, spaces: LayoutSpaces):
|