• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sandialabs / toyplot / 9897288170

11 Jul 2024 07:20PM UTC coverage: 94.629% (+0.01%) from 94.618%
9897288170

push

github

tshead
Temporarily limit the build to numpy < 2.0.0.

5391 of 5697 relevant lines covered (94.63%)

3.78 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.01
/toyplot/coordinates.py
1
# Copyright 2014, Sandia Corporation. Under the terms of Contract
2
# DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains certain
3
# rights in this software.
4

5
"""Classes and functions for working with coordinate systems.
6
"""
7

8

9
import collections
4✔
10
import itertools
4✔
11
import warnings
4✔
12

13
import numpy
4✔
14

15
import toyplot.broadcast
4✔
16
import toyplot.color
4✔
17
import toyplot.data
4✔
18
import toyplot.format
4✔
19
import toyplot.layout
4✔
20
import toyplot.locator
4✔
21
import toyplot.mark
4✔
22
import toyplot.projection
4✔
23
import toyplot.require
4✔
24

25
##########################################################################
26
# Helpers
27

28
def _mark_exportable(table, column, exportable=True):
4✔
29
    table.metadata(column)["toyplot:exportable"] = exportable
4✔
30

31

32
def _opposite_location(location):
4✔
33
    return "above" if location == "below" else "below"
4✔
34

35

36
def _create_far_property():
4✔
37
    def getter(self):
4✔
38
        """Specifies the distance from the axis, in the opposite direction as `location`.
39
        """
40
        return self._far
4✔
41

42
    def setter(self, value):
4✔
43
        if value is None:
4✔
44
            self._far = None
×
45
        else:
46
            self._far = toyplot.units.convert(value, target="px", default="px")
4✔
47

48
    return property(getter, setter)
4✔
49

50

51
def _create_line_style_property():
4✔
52
    def getter(self):
4✔
53
        """Dictionary of CSS property-value pairs.
54

55
        Use the *style* property to control the appearance of the line.  The
56
        following CSS properties are allowed:
57

58
        * opacity
59
        * stroke
60
        * stroke-dasharray
61
        * stroke-opacity
62
        * stroke-width
63

64
        Note that when assigning to the *style* property, the properties you
65
        supply are merged with the existing properties.
66
        """
67
        return self._style
4✔
68

69
    def setter(self, value):
4✔
70
        self._style = toyplot.style.combine(
4✔
71
            self._style,
72
            toyplot.style.require(value, allowed=toyplot.style.allowed.line),
73
            )
74

75
    return property(getter, setter)
4✔
76

77

78
def _create_location_property():
4✔
79
    def getter(self):
4✔
80
        return self._location
4✔
81

82
    def setter(self, value):
4✔
83
        self._location = toyplot.require.value_in(value, [None, "above", "below"])
4✔
84

85
    return property(getter, setter)
4✔
86

87

88
def _create_near_property():
4✔
89
    def getter(self):
4✔
90
        """Specifies the distance from the axis, in the same direction as `location`.
91
        """
92
        return self._near
4✔
93

94
    def setter(self, value):
4✔
95
        if value is None:
4✔
96
            self._near = None
×
97
        else:
98
            self._near = toyplot.units.convert(value, target="px", default="px")
4✔
99

100
    return property(getter, setter)
4✔
101

102

103
def _create_offset_property():
4✔
104
    def getter(self):
4✔
105
        """Specifies the position relative to the axis.  Increasing values of
106
        *offset* move the position further away from the axis, whether the
107
        location is "above" or "below".
108

109
        :getter: Returns the offset in CSS pixels.
110

111
        :setter: Sets the offset using a number, string, or (number, string) tuple.  Assumes CSS pixels if units aren't provided.  See :ref:`units` for details.
112
        """
113
        return self._offset
4✔
114

115
    def setter(self, value):
4✔
116
        if value is None:
4✔
117
            self._offset = value
×
118
        else:
119
            self._offset = toyplot.units.convert(value, target="px", default="px")
4✔
120

121
    return property(getter, setter)
4✔
122

123

124
def _create_show_property():
4✔
125
    def getter(self):
4✔
126
        return self._show
4✔
127

128
    def setter(self, value):
4✔
129
        if not isinstance(value, bool):
4✔
130
            raise ValueError("A boolean value is required.")
×
131
        self._show = value
4✔
132

133
    return property(getter, setter)
4✔
134

135

136
def _create_text_property():
4✔
137
    def getter(self):
4✔
138
        """The text to be displayed, or None.
139
        """
140
        return self._text
4✔
141

142
    def setter(self, value):
4✔
143
        self._text = value
4✔
144

145
    return property(getter, setter)
4✔
146

147

148
def _create_text_style_property():
4✔
149
    def getter(self):
4✔
150
        """Dictionary of CSS property-value pairs.
151

152
        Use the *style* property to control the text appearance.  The
153
        following CSS properties are allowed:
154

155
        * alignment-baseline
156
        * baseline-shift
157
        * fill
158
        * fill-opacity
159
        * font-size
160
        * font-weight
161
        * opacity
162
        * stroke
163
        * stroke-opacity
164
        * stroke-width
165
        * text-anchor
166
        * -toyplot-anchor-shift
167

168
        Note that when assigning to the *style* property, the properties you
169
        supply are merged with the existing properties.
170
        """
171
        return self._style
4✔
172

173
    def setter(self, value):
4✔
174
        self._style = toyplot.style.combine(
4✔
175
            self._style,
176
            toyplot.style.require(value, allowed=toyplot.style.allowed.text),
177
            )
178

179
    return property(getter, setter)
4✔
180

181

182
def _create_projection(scale, domain_min, domain_max, range_min, range_max):
4✔
183
    if isinstance(scale, toyplot.projection.Projection):
4✔
184
        return scale
×
185
    if scale == "linear":
4✔
186
        return toyplot.projection.linear(domain_min, domain_max, range_min, range_max)
4✔
187
    scale, base = scale
4✔
188
    return toyplot.projection.log(base, domain_min, domain_max, range_min, range_max)
4✔
189

190

191
##########################################################################
192
# Axis
193

194
class Axis(object):
4✔
195
    """One dimensional axis that can be used to create coordinate systems.
196
    """
197
    class DomainHelper(object):
4✔
198
        """Controls domain related behavior for this axis."""
199
        def __init__(self, domain_min, domain_max):
4✔
200
            self._min = domain_min
4✔
201
            self._max = domain_max
4✔
202
            self._show = True
4✔
203

204
        @property
4✔
205
        def min(self):
4✔
206
            """Specify an explicit domain minimum for this axis.  By default
207
            the implicit domain minimum is computed from visible data.
208
            """
209
            return self._min
4✔
210

211
        @min.setter
4✔
212
        def min(self, value):
4✔
213
            self._min = value
4✔
214

215
        @property
4✔
216
        def max(self):
4✔
217
            """Specify an explicit domain maximum for this axis.  By default
218
            the implicit domain maximum is computed from visible data.
219
            """
220
            return self._max
4✔
221

222
        @max.setter
4✔
223
        def max(self, value):
4✔
224
            self._max = value
4✔
225

226
        @property
4✔
227
        def show(self):
4✔
228
            """Control whether the domain should be made visible using the
229
            axis spine.
230
            """
231
            return self._show
4✔
232

233
        @show.setter
4✔
234
        def show(self, value):
4✔
235
            toyplot.log.warning("Altering <axis>.domain.show is experimental.")
×
236
            self._show = True if value else False
×
237

238

239
    class InteractiveCoordinatesLabelHelper(object):
4✔
240
        """Controls the appearance and behavior of interactive coordinate labels."""
241
        def __init__(self):
4✔
242
            self._show = True
4✔
243
            self._style = {
4✔
244
                "fill": "slategray",
245
                "font-size": "10px",
246
                "font-weight": "normal",
247
                "stroke": "none",
248
                "text-anchor": "middle",
249
                }
250

251
        show = _create_show_property()
4✔
252
        style = _create_text_style_property()
4✔
253

254

255
    class InteractiveCoordinatesTickHelper(object):
4✔
256
        """Controls the appearance and behavior of interactive coordinate ticks."""
257
        def __init__(self):
4✔
258
            self._show = True
4✔
259
            self._style = {
4✔
260
                "stroke":"slategray",
261
                "stroke-width": 1.0,
262
                }
263

264
        show = _create_show_property()
4✔
265
        style = _create_line_style_property()
4✔
266

267

268
    class InteractiveCoordinatesHelper(object):
4✔
269
        """Controls the appearance and behavior of interactive coordinates."""
270
        def __init__(self):
4✔
271
            self._label = toyplot.coordinates.Axis.InteractiveCoordinatesLabelHelper()
4✔
272
            self._location = None
4✔
273
            self._show = True
4✔
274
            self._tick = toyplot.coordinates.Axis.InteractiveCoordinatesTickHelper()
4✔
275

276
        @property
4✔
277
        def label(self):
4✔
278
            """:class:`toyplot.coordinates.Axis.InteractiveCoordinatesLabelHelper` instance."""
279
            return self._label
4✔
280

281
        location = _create_location_property()
4✔
282
        """Controls the position of interactive coordinates relative to the axis.
2✔
283

284
        Allowed values are "above" (force coordinates to appear above the axis), "below"
285
        (the opposite), or `None` (the default - display interactive coordinates opposite
286
        tick labels).
287
        """
288
        show = _create_show_property()
4✔
289
        """Set `False` to disable showing interactive coordinates for this axis."""
2✔
290

291
        @property
4✔
292
        def tick(self):
4✔
293
            """:class:`toyplot.coordinates.Axis.InteractiveCoordinatesTickHelper` instance."""
294
            return self._tick
4✔
295

296

297
    class InteractiveHelper(object):
4✔
298
        """Controls interactive behavior for this axis."""
299
        def __init__(self):
4✔
300
            self._coordinates = toyplot.coordinates.Axis.InteractiveCoordinatesHelper()
4✔
301

302
        @property
4✔
303
        def coordinates(self):
4✔
304
            """:class:`toyplot.coordinates.Axis.InteractiveCoordinatesHelper` instance."""
305
            return self._coordinates
4✔
306

307

308
    class LabelHelper(object):
4✔
309
        """Controls the appearance and behavior of an axis label."""
310
        def __init__(self, text, style):
4✔
311
            self._location = None
4✔
312
            self._offset = None
4✔
313
            self._style = {}
4✔
314
            self._text = None
4✔
315

316
            self.style = {
4✔
317
                "font-size": "12px",
318
                "font-weight": "bold",
319
                "stroke": "none",
320
                "text-anchor":"middle",
321
                }
322
            self.style = style
4✔
323
            self.text = text
4✔
324

325
        location = _create_location_property()
4✔
326
        offset = _create_offset_property()
4✔
327
        style = _create_text_style_property()
4✔
328
        text = _create_text_property()
4✔
329

330

331
    class SpineHelper(object):
4✔
332
        """Controls the appearance and behavior of an axis spine."""
333
        def __init__(self):
4✔
334
            self._position = "low"
4✔
335
            self._show = True
4✔
336
            self._style = {}
4✔
337

338
        @property
4✔
339
        def position(self):
4✔
340
            return self._position
4✔
341

342
        @position.setter
4✔
343
        def position(self, value):
4✔
344
            self._position = value
4✔
345

346
        show = _create_show_property()
4✔
347
        style = _create_line_style_property()
4✔
348

349

350
    class PerTickHelper(object):
4✔
351
        """Controls the appearanace and behavior of individual axis ticks."""
352
        class TickProxy(object):
4✔
353
            def __init__(self, tick, allowed):
4✔
354
                self._tick = tick
4✔
355
                self._allowed = allowed
4✔
356

357
            @property
4✔
358
            def style(self):
4✔
359
                return self._tick.get("style", {})
4✔
360

361
            @style.setter
4✔
362
            def style(self, value):
4✔
363
                self._tick["style"] = toyplot.style.combine(
4✔
364
                    self._tick.get("style"),
365
                    toyplot.style.require(value, allowed=self._allowed),
366
                    )
367

368
        def __init__(self, allowed):
4✔
369
            self._indices = collections.defaultdict(dict)
4✔
370
            self._values = collections.defaultdict(dict)
4✔
371
            self._allowed = allowed
4✔
372

373
        def __call__(self, index=None, value=None):
4✔
374
            if index is None and value is None:
4✔
375
                raise ValueError("Must specify tick index or value.") # pragma: no cover
376
            if index is not None and value is not None:
4✔
377
                raise ValueError("Must specify either index or value, not both.") # pragma: no cover
378
            if index is not None:
4✔
379
                return Axis.PerTickHelper.TickProxy(self._indices[index], self._allowed)
4✔
380
            elif value is not None:
4✔
381
                return Axis.PerTickHelper.TickProxy(self._values[value], self._allowed)
4✔
382

383
        def styles(self, values):
4✔
384
            results = [self._indices[index].get("style", None) if index in self._indices else None for index in range(len(values))]
4✔
385
            for value in self._values:
4✔
386
                deltas = numpy.abs(values - value)
4✔
387
                results[numpy.argmin(deltas)] = self._values[value].get("style", None)
4✔
388
            return results
4✔
389

390

391
    class TicksHelper(object):
4✔
392
        """Controls the appearance and behavior of axis ticks."""
393
        def __init__(self, locator, angle):
4✔
394
            self._far = None
4✔
395
            self._labels = Axis.TickLabelsHelper(angle)
4✔
396
            self._location = None
4✔
397
            self._locator = locator
4✔
398
            self._near = None
4✔
399
            self._show = False
4✔
400
            self._style = {}
4✔
401
            self._tick = Axis.PerTickHelper(toyplot.style.allowed.line)
4✔
402

403
        far = _create_far_property()
4✔
404
        location = _create_location_property()
4✔
405
        """Controls the position of ticks (and labels) relative to the axis.
2✔
406

407
        Allowed values are "above" (force labels to appear above the axis), "below"
408
        (the opposite), or `None` (use default, context-sensitive behavior).
409
        """
410
        near = _create_near_property()
4✔
411
        show = _create_show_property()
4✔
412
        style = _create_line_style_property()
4✔
413

414
        @property
4✔
415
        def labels(self):
4✔
416
            return self._labels
4✔
417

418
        @property
4✔
419
        def locator(self):
4✔
420
            return self._locator
4✔
421

422
        @locator.setter
4✔
423
        def locator(self, value):
4✔
424
            self._locator = value
4✔
425

426
        @property
4✔
427
        def tick(self):
4✔
428
            return self._tick
4✔
429

430

431
    class TickLabelsHelper(object):
4✔
432
        """Controls the appearance and behavior of axis tick labels."""
433
        def __init__(self, angle):
4✔
434
            self._angle = angle
4✔
435
            self._label = Axis.PerTickHelper(toyplot.style.allowed.text)
4✔
436
            self._offset = None
4✔
437
            self._show = True
4✔
438
            self._style = {
4✔
439
                "font-size": "10px",
440
                "font-weight": "normal",
441
                "stroke": "none",
442
                }
443

444
        style = _create_text_style_property()
4✔
445
        offset = _create_offset_property()
4✔
446
        show = _create_show_property()
4✔
447

448
        @property
4✔
449
        def angle(self):
4✔
450
            return self._angle
4✔
451

452
        @angle.setter
4✔
453
        def angle(self, value):
4✔
454
            self._angle = value
4✔
455

456
        @property
4✔
457
        def label(self):
4✔
458
            return self._label
4✔
459

460
    def __init__(
4✔
461
            self,
462
            label=None,
463
            domain_min=None,
464
            domain_max=None,
465
            scale="linear",
466
            show=True,
467
            tick_angle=0,
468
            tick_locator=None,
469
        ):
470
        self._finalized = None
4✔
471
        self._data_max = None
4✔
472
        self._data_min = None
4✔
473
        self._display_max = None
4✔
474
        self._display_min = None
4✔
475
        self._domain = Axis.DomainHelper(domain_min, domain_max)
4✔
476
        self._interactive = Axis.InteractiveHelper()
4✔
477
        self._label = Axis.LabelHelper(label, style={})
4✔
478
        self._scale = None
4✔
479
        self._show = show
4✔
480
        self._spine = Axis.SpineHelper()
4✔
481
        self._tick_labels = []
4✔
482
        self._tick_locations = []
4✔
483
        self._tick_titles = []
4✔
484
        self._ticks = Axis.TicksHelper(tick_locator, tick_angle)
4✔
485
        self.scale = scale
4✔
486

487
    show = _create_show_property()
4✔
488

489
    @property
4✔
490
    def interactive(self):
4✔
491
        """:class:`toyplot.coordinates.Axis.InteractiveHelper` instance."""
492
        return self._interactive
4✔
493

494
    @property
4✔
495
    def domain(self):
4✔
496
        """:class:`toyplot.coordinates.Axis.DomainHelper` instance."""
497
        return self._domain
4✔
498

499
    @property
4✔
500
    def label(self):
4✔
501
        """:class:`toyplot.coordinates.Axis.LabelHelper` instance."""
502
        return self._label
4✔
503

504
    @property
4✔
505
    def scale(self):
4✔
506
        return self._scale
4✔
507

508
    @scale.setter
4✔
509
    def scale(self, value):
4✔
510
        if value == "linear":
4✔
511
            self._scale = "linear"
4✔
512
            return
4✔
513
        elif value in ["log", "log10"]:
4✔
514
            self._scale = ("log", 10)
4✔
515
            return
4✔
516
        elif value == "log2":
4✔
517
            self._scale = ("log", 2)
4✔
518
            return
4✔
519
        elif isinstance(value, tuple) and len(value) == 2:
4✔
520
            scale, base = value
4✔
521
            if scale == "log":
4✔
522
                self._scale = ("log", base)
4✔
523
                return
4✔
524
        raise ValueError(
×
525
            """Scale must be "linear", "log", "log10", "log2" or a ("log", base) tuple.""")
526

527
    @property
4✔
528
    def spine(self):
4✔
529
        """:class:`toyplot.coordinates.Axis.SpineHelper` instance."""
530
        return self._spine
4✔
531

532
    @property
4✔
533
    def ticks(self):
4✔
534
        """:class:`toyplot.coordinates.Axis.TicksHelper` instance."""
535
        return self._ticks
4✔
536

537
    def _update_domain(self, values, display=True, data=True):
4✔
538
        if display:
4✔
539
            self._display_min, self._display_max = toyplot.data.minimax(itertools.chain([self._display_min, self._display_max], values))
4✔
540

541
        if data:
4✔
542
            self._data_min, self._data_max = toyplot.data.minimax(itertools.chain([self._data_min, self._data_max], values))
4✔
543

544
    def _locator(self):
4✔
545
        if self.ticks.locator is not None:
4✔
546
            return self.ticks.locator
4✔
547
        if self.scale == "linear":
4✔
548
            return toyplot.locator.Extended()
4✔
549
        if isinstance(self.scale, toyplot.projection.Projection):
4✔
550
            return toyplot.locator.Null()
×
551
        else:
552
            scale, base = self.scale
4✔
553
            if scale == "log":
4✔
554
                return toyplot.locator.Log(base=base)
4✔
555
        raise RuntimeError("Unable to create an appropriate locator.") # pragma: no cover
556

557
    def _finalize(
4✔
558
            self,
559
            x1,
560
            x2,
561
            y1,
562
            y2,
563
            offset,
564
            domain_min,
565
            domain_max,
566
            tick_locations,
567
            tick_labels,
568
            tick_titles,
569
            default_tick_location,
570
            default_ticks_near,
571
            default_ticks_far,
572
            default_label_location,
573
        ):
574
        if self._finalized is None:
4✔
575
            self._x1 = x1
4✔
576
            self._x2 = x2
4✔
577
            self._y1 = y1
4✔
578
            self._y2 = y2
4✔
579
            self._offset = offset
4✔
580
            self._domain_min = domain_min
4✔
581
            self._domain_max = domain_max
4✔
582
            self._tick_locations = tick_locations
4✔
583
            self._tick_labels = tick_labels
4✔
584
            self._tick_titles = tick_titles
4✔
585
            self._tick_location = self.ticks.location if self.ticks.location is not None else default_tick_location
4✔
586
            self._ticks_near = self.ticks.near if self.ticks.near is not None else default_ticks_near
4✔
587
            self._ticks_far = self.ticks.far if self.ticks.far is not None else default_ticks_far
4✔
588
            self._tick_labels_location = self._tick_location
4✔
589
            self._tick_labels_offset = self.ticks.labels.offset if self.ticks.labels.offset is not None else 6
4✔
590
            self._label_location = self.label.location if self.label.location is not None else default_label_location
4✔
591
            self._label_offset = self.label.offset if self.label.offset is not None else 22
4✔
592
            self._interactive_coordinates_location = self.interactive.coordinates.location if self.interactive.coordinates.location is not None else _opposite_location(self._tick_labels_location)
4✔
593

594
            endpoints = numpy.row_stack(((x1, y1), (x2, y2)))
4✔
595
            length = numpy.linalg.norm(endpoints[1] - endpoints[0])
4✔
596
            self.projection = _create_projection(
4✔
597
                scale=self.scale,
598
                domain_min=domain_min,
599
                domain_max=domain_max,
600
                range_min=0.0,
601
                range_max=length,
602
                )
603

604
            self._finalized = self
4✔
605
        return self._finalized
4✔
606

607

608
##########################################################################
609
# Cartesian
610

611
class Cartesian(object):
4✔
612
    """Standard two-dimensional Cartesian coordinate system.
613

614
    Do not create Cartesian instances directly.  Use factory methods such
615
    as :meth:`toyplot.canvas.Canvas.cartesian` instead.
616
    """
617

618
    class LabelHelper(object):
4✔
619
        """Controls the appearance and behavior of a Cartesian coordinate system label."""
620
        def __init__(self, text, style):
4✔
621
            self._style = {}
4✔
622
            self._text = None
4✔
623

624
            self.style = {
4✔
625
                "font-size": "14px",
626
                "font-weight": "bold",
627
                "stroke": "none",
628
                "text-anchor":"middle",
629
                "-toyplot-vertical-align":"bottom",
630
                }
631
            self.style = style
4✔
632
            self.text = text
4✔
633
            self.offset = 8
4✔
634

635
        style = _create_text_style_property()
4✔
636
        text = _create_text_property()
4✔
637
        offset = _create_offset_property()
4✔
638

639
    def __init__(
4✔
640
            self,
641
            aspect,
642
            hyperlink,
643
            label,
644
            padding,
645
            palette,
646
            scenegraph,
647
            show,
648
            xaxis,
649
            xlabel,
650
            xmax,
651
            xmax_range,
652
            xmin,
653
            xmin_range,
654
            xscale,
655
            xshow,
656
            xticklocator,
657
            yaxis,
658
            ylabel,
659
            ymax,
660
            ymax_range,
661
            ymin,
662
            ymin_range,
663
            yscale,
664
            yshow,
665
            yticklocator,
666
            ):
667

668
        if palette is None:
4✔
669
            palette = toyplot.color.Palette()
4✔
670

671
        self._finalized = None
4✔
672

673
        self._xmin_range = xmin_range
4✔
674
        self._xmax_range = xmax_range
4✔
675
        self._ymin_range = ymin_range
4✔
676
        self._ymax_range = ymax_range
4✔
677

678
        self._aspect = None
4✔
679
        self.aspect = aspect
4✔
680

681
        self._hyperlink = None
4✔
682
        self.hyperlink = hyperlink
4✔
683

684
        self._expand_domain_range_x = None
4✔
685
        self._expand_domain_range_y = None
4✔
686
        self._expand_domain_range_left = None
4✔
687
        self._expand_domain_range_right = None
4✔
688
        self._expand_domain_range_top = None
4✔
689
        self._expand_domain_range_bottom = None
4✔
690
        self._padding = toyplot.units.convert(padding, target="px", default="px")
4✔
691

692
        self._palette = palette
4✔
693
        self._bar_colors = itertools.cycle(self._palette)
4✔
694
        self._ellipse_colors = itertools.cycle(self._palette)
4✔
695
        self._fill_colors = itertools.cycle(self._palette)
4✔
696
        self._graph_colors = itertools.cycle(self._palette)
4✔
697
        self._plot_colors = itertools.cycle(self._palette)
4✔
698
        self._scatterplot_colors = itertools.cycle(self._palette)
4✔
699
        self._rect_colors = itertools.cycle(self._palette)
4✔
700
        self._text_colors = itertools.cycle(self._palette)
4✔
701

702
        self._show = show
4✔
703

704
        self.label = Cartesian.LabelHelper(
4✔
705
            text=label,
706
            style={},
707
            )
708

709
        if xaxis is None:
4✔
710
            xaxis = Axis()
4✔
711
        xaxis.show = xshow
4✔
712
        xaxis.label.text = xlabel
4✔
713
        xaxis.domain.min = xmin
4✔
714
        xaxis.domain.max = xmax
4✔
715
        xaxis.ticks.locator = xticklocator
4✔
716
        xaxis.scale = xscale
4✔
717

718
        if yaxis is None:
4✔
719
            yaxis = Axis()
4✔
720
        yaxis.show = yshow
4✔
721
        yaxis.label.text = ylabel
4✔
722
        yaxis.domain.min = ymin
4✔
723
        yaxis.domain.max = ymax
4✔
724
        yaxis.ticks.locator = yticklocator
4✔
725
        yaxis.scale = yscale
4✔
726

727
        self.x = xaxis
4✔
728
        self.y = yaxis
4✔
729

730
        self._scenegraph = scenegraph
4✔
731

732
    @property
4✔
733
    def aspect(self):
4✔
734
        """Control the mapping from domains to ranges.
735

736
        By default, each axis maps its domain to its range separately, which is
737
        what is usually expected from a plot.  Sometimes, both axes have the same
738
        domain.  In this case, it is desirable that both axes are mapped to a consistent
739
        range to avoid "squashing" or "stretching" the data.  To do so, set `aspect`
740
        to "fit-range".
741
        """
742
        return self._aspect
×
743

744
    @aspect.setter
4✔
745
    def aspect(self, value):
4✔
746
        if value not in [None, "fit-range"]:
4✔
747
            raise ValueError("Unknown aspect value: %s" % value) # pragma: no cover
748
        self._aspect = value
4✔
749

750
    @property
4✔
751
    def hyperlink(self):
4✔
752
        """Specify a URI that will be hyperlinked from the axes range."""
753
        return self._hyperlink
×
754

755
    @hyperlink.setter
4✔
756
    def hyperlink(self, value):
4✔
757
        self._hyperlink = toyplot.require.hyperlink(value)
4✔
758

759
    @property
4✔
760
    def show(self):
4✔
761
        """Control axis visibility.
762

763
        Use the `show` property to hide all visible parts of the axes: labels,
764
        spines, ticks, tick labels, etc.  Note that this does not affect
765
        visibility of the axes contents, just the axes themselves.
766
        """
767
        return self._show
4✔
768

769
    @show.setter
4✔
770
    def show(self, value):
4✔
771
        self._show = value
4✔
772

773
    @property
4✔
774
    def padding(self):
4✔
775
        """Control the default distance between axis spines and data.
776

777
        By default, axis spines are offset slightly from the data, to avoid
778
        visual clutter and overlap.  Use `padding` to change this offset.
779
        The default units are CSS pixels, but you may specify the padding
780
        using any :ref:`units` you like.
781
        """
782
        return self._padding
4✔
783

784
    @padding.setter
4✔
785
    def padding(self, value):
4✔
786
        self._padding = toyplot.units.convert(value, target="px", default="px")
4✔
787

788
    def _set_xmin_range(self, value):
4✔
789
        self._xmin_range = value
4✔
790
    xmin_range = property(fset=_set_xmin_range)
4✔
791

792
    def _set_xmax_range(self, value):
4✔
793
        self._xmax_range = value
4✔
794
    xmax_range = property(fset=_set_xmax_range)
4✔
795

796
    def _set_ymin_range(self, value):
4✔
797
        self._ymin_range = value
4✔
798
    ymin_range = property(fset=_set_ymin_range)
4✔
799

800
    def _set_ymax_range(self, value):
4✔
801
        self._ymax_range = value
4✔
802
    ymax_range = property(fset=_set_ymax_range)
4✔
803

804
    def _finalize(self):
4✔
805
        if self._finalized is None:
4✔
806
            # Begin with the implicit domain defined by our children.
807
            for child in self._scenegraph.targets(self.x, "map"):
4✔
808
                child = child._finalize()
4✔
809
                if child is not None:
4✔
810
                    self.x._update_domain(child.domain("x"), display=True, data=not child.annotation)
4✔
811

812
                    (x, y), (left, right, top, bottom) = child.extents(["x", "y"])
4✔
813
                    self._expand_domain_range_x = x if self._expand_domain_range_x is None else numpy.concatenate((self._expand_domain_range_x, x))
4✔
814
                    self._expand_domain_range_left = left if self._expand_domain_range_left is None else numpy.concatenate((self._expand_domain_range_left, left))
4✔
815
                    self._expand_domain_range_right = right if self._expand_domain_range_right is None else numpy.concatenate((self._expand_domain_range_right, right))
4✔
816

817
            for child in self._scenegraph.targets(self.y, "map"):
4✔
818
                child = child._finalize()
4✔
819
                if child is not None:
4✔
820
                    self.y._update_domain(child.domain("y"), display=True, data=not child.annotation)
4✔
821

822
                    (x, y), (left, right, top, bottom) = child.extents(["x", "y"])
4✔
823
                    self._expand_domain_range_y = y if self._expand_domain_range_y is None else numpy.concatenate((self._expand_domain_range_y, y))
4✔
824
                    self._expand_domain_range_top = top if self._expand_domain_range_top is None else numpy.concatenate((self._expand_domain_range_top, top))
4✔
825
                    self._expand_domain_range_bottom = bottom if self._expand_domain_range_bottom is None else numpy.concatenate((self._expand_domain_range_bottom, bottom))
4✔
826

827
            # Begin with the implicit domain defined by our data.
828
            xdomain_min = self.x._display_min
4✔
829
            xdomain_max = self.x._display_max
4✔
830
            ydomain_min = self.y._display_min
4✔
831
            ydomain_max = self.y._display_max
4✔
832

833
            # If there is no implicit domain (we don't have any data), default
834
            # to the origin.
835
            if xdomain_min is None:
4✔
836
                xdomain_min = 0
4✔
837
            if xdomain_max is None:
4✔
838
                xdomain_max = 0
4✔
839
            if ydomain_min is None:
4✔
840
                ydomain_min = 0
4✔
841
            if ydomain_max is None:
4✔
842
                ydomain_max = 0
4✔
843

844
            # Ensure that the domain is never empty.
845
            if xdomain_min == xdomain_max:
4✔
846
                xdomain_min -= 0.5
4✔
847
                xdomain_max += 0.5
4✔
848
            if ydomain_min == ydomain_max:
4✔
849
                ydomain_min -= 0.5
4✔
850
                ydomain_max += 0.5
4✔
851

852
            # Optionally expand the domain in range-space (used to make room for text).
853
            if self._expand_domain_range_x is not None:
4✔
854
                x_projection = _create_projection(
4✔
855
                    self.x.scale,
856
                    domain_min=xdomain_min,
857
                    domain_max=xdomain_max,
858
                    range_min=self._xmin_range,
859
                    range_max=self._xmax_range,
860
                    )
861

862
                range_x = x_projection(self._expand_domain_range_x)
4✔
863
                range_left = range_x + self._expand_domain_range_left
4✔
864
                range_right = range_x + self._expand_domain_range_right
4✔
865

866
                domain_left = x_projection.inverse(range_left)
4✔
867
                domain_right = x_projection.inverse(range_right)
4✔
868

869
                xdomain_min, xdomain_max = toyplot.data.minimax([xdomain_min, xdomain_max, domain_left, domain_right])
4✔
870

871
            if self._expand_domain_range_y is not None:
4✔
872
                y_projection = _create_projection(
4✔
873
                    self.y.scale,
874
                    domain_min=ydomain_min,
875
                    domain_max=ydomain_max,
876
                    range_min=self._ymax_range,
877
                    range_max=self._ymin_range,
878
                    )
879

880
                range_y = y_projection(self._expand_domain_range_y)
4✔
881
                range_top = range_y + self._expand_domain_range_top
4✔
882
                range_bottom = range_y + self._expand_domain_range_bottom
4✔
883

884
                domain_top = y_projection.inverse(range_top)
4✔
885
                domain_bottom = y_projection.inverse(range_bottom)
4✔
886

887
                ydomain_min, ydomain_max = toyplot.data.minimax([ydomain_min, ydomain_max, domain_top, domain_bottom])
4✔
888

889
            # Optionally expand the domain to match the aspect ratio of the range.
890
            if self._aspect == "fit-range":
4✔
891
                dwidth = (xdomain_max - xdomain_min)
4✔
892
                dheight = (ydomain_max - ydomain_min)
4✔
893
                daspect = dwidth / dheight
4✔
894
                raspect = (self._xmax_range - self._xmin_range) / (self._ymax_range - self._ymin_range)
4✔
895

896
                if daspect < raspect:
4✔
897
                    offset = ((dwidth * (raspect / daspect)) - dwidth) * 0.5
4✔
898
                    xdomain_min -= offset
4✔
899
                    xdomain_max += offset
4✔
900
                elif daspect > raspect:
4✔
901
                    offset = ((dheight * (daspect / raspect)) - dheight) * 0.5
4✔
902
                    ydomain_min -= offset
4✔
903
                    ydomain_max += offset
4✔
904

905
            # Allow users to override the domain.
906
            if self.x.domain.min is not None:
4✔
907
                xdomain_min = self.x.domain.min
4✔
908
            if self.x.domain.max is not None:
4✔
909
                xdomain_max = self.x.domain.max
4✔
910
            if self.y.domain.min is not None:
4✔
911
                ydomain_min = self.y.domain.min
4✔
912
            if self.y.domain.max is not None:
4✔
913
                ydomain_max = self.y.domain.max
4✔
914

915
            # Ensure that the domain is never empty.
916
            if xdomain_min == xdomain_max:
4✔
917
                xdomain_min -= 0.5
×
918
                xdomain_max += 0.5
×
919
            if ydomain_min == ydomain_max:
4✔
920
                ydomain_min -= 0.5
×
921
                ydomain_max += 0.5
×
922

923
            # Calculate tick locations and labels.
924
            xtick_locations = []
4✔
925
            xtick_labels = []
4✔
926
            xtick_titles = []
4✔
927
            if self.show and self.x.show:
4✔
928
                xtick_locations, xtick_labels, xtick_titles = self.x._locator().ticks(xdomain_min, xdomain_max)
4✔
929
            ytick_locations = []
4✔
930
            ytick_labels = []
4✔
931
            ytick_titles = []
4✔
932
            if self.show and self.y.show:
4✔
933
                ytick_locations, ytick_labels, ytick_titles = self.y._locator().ticks(ydomain_min, ydomain_max)
4✔
934

935
            # Allow tick locations to grow (never shrink) the domain.
936
            if len(xtick_locations):
4✔
937
                xdomain_min = numpy.amin((xdomain_min, xtick_locations[0]))
4✔
938
                xdomain_max = numpy.amax((xdomain_max, xtick_locations[-1]))
4✔
939
            if len(ytick_locations):
4✔
940
                ydomain_min = numpy.amin((ydomain_min, ytick_locations[0]))
4✔
941
                ydomain_max = numpy.amax((ydomain_max, ytick_locations[-1]))
4✔
942

943
            # Create projections for each axis.
944
            self._x_projection = _create_projection(
4✔
945
                scale=self.x.scale,
946
                domain_min=xdomain_min,
947
                domain_max=xdomain_max,
948
                range_min=self._xmin_range,
949
                range_max=self._xmax_range,
950
                )
951
            self._y_projection = _create_projection(
4✔
952
                scale=self.y.scale,
953
                domain_min=ydomain_min,
954
                domain_max=ydomain_max,
955
                range_min=self._ymax_range,
956
                range_max=self._ymin_range,
957
                )
958

959
            # Finalize positions for all axis components.
960
            if self.x.spine.position == "low":
4✔
961
                x_offset = self.padding
4✔
962
                x_spine_y = self._ymax_range
4✔
963
                x_ticks_near = 0
4✔
964
                x_ticks_far = 5
4✔
965
                x_tick_location = "below"
4✔
966
                x_label_location = "below"
4✔
967
            elif self.x.spine.position == "high":
4✔
968
                x_offset = -self.padding # pylint: disable=invalid-unary-operand-type
4✔
969
                x_spine_y = self._ymin_range
4✔
970
                x_ticks_near = 5
4✔
971
                x_ticks_far = 0
4✔
972
                x_tick_location = "above"
4✔
973
                x_label_location = "above"
4✔
974
            else:
975
                x_offset = 0
4✔
976
                x_spine_y = self._y_projection(self.x.spine.position)
4✔
977
                x_ticks_near = 3
4✔
978
                x_ticks_far = 3
4✔
979
                x_tick_location = "below"
4✔
980
                x_label_location = "below"
4✔
981

982
            if self.y.spine._position == "low":
4✔
983
                y_offset = -self.padding # pylint: disable=invalid-unary-operand-type
4✔
984
                y_spine_x = self._xmin_range
4✔
985
                y_ticks_near = 0
4✔
986
                y_ticks_far = 5
4✔
987
                y_tick_location = "above"
4✔
988
                y_label_location = "above"
4✔
989
            elif self.y.spine._position == "high":
4✔
990
                y_offset = self.padding
4✔
991
                y_spine_x = self._xmax_range
4✔
992
                y_ticks_near = 0
4✔
993
                y_ticks_far = 5
4✔
994
                y_tick_location = "below"
4✔
995
                y_label_location = "below"
4✔
996
            else:
997
                y_offset = 0
4✔
998
                y_spine_x = self._x_projection(self.y.spine._position)
4✔
999
                y_ticks_near = 3
4✔
1000
                y_ticks_far = 3
4✔
1001
                y_tick_location = "below"
4✔
1002
                y_label_location = "below"
4✔
1003

1004
            # Finalize the axes.
1005
            self.x._finalize(
4✔
1006
                x1=self._xmin_range,
1007
                x2=self._xmax_range,
1008
                y1=x_spine_y,
1009
                y2=x_spine_y,
1010
                offset=x_offset,
1011
                domain_min=xdomain_min,
1012
                domain_max=xdomain_max,
1013
                tick_locations=xtick_locations,
1014
                tick_labels=xtick_labels,
1015
                tick_titles=xtick_titles,
1016
                default_tick_location=x_tick_location,
1017
                default_ticks_far=x_ticks_far,
1018
                default_ticks_near=x_ticks_near,
1019
                default_label_location=x_label_location,
1020
                )
1021
            self.y._finalize(
4✔
1022
                x1=y_spine_x,
1023
                x2=y_spine_x,
1024
                y1=self._ymax_range,
1025
                y2=self._ymin_range,
1026
                offset=y_offset,
1027
                domain_min=ydomain_min,
1028
                domain_max=ydomain_max,
1029
                tick_locations=ytick_locations,
1030
                tick_labels=ytick_labels,
1031
                tick_titles=ytick_titles,
1032
                default_tick_location=y_tick_location,
1033
                default_ticks_far=y_ticks_far,
1034
                default_ticks_near=y_ticks_near,
1035
                default_label_location=y_label_location,
1036
                )
1037
            self._finalized = self
4✔
1038

1039
        return self._finalized
4✔
1040

1041
    def project(self, axis, values):
4✔
1042
        """Project a set of domain values to coordinate system range values.
1043

1044
        Note that this API is intended for advanced users creating their own
1045
        custom marks, end-users should never need to use it.
1046

1047
        Parameters
1048
        ----------
1049
        axis: "x" or "y", required
1050
            The axis to be projected
1051
        values: array-like, required
1052
            The values to be projected
1053

1054
        Returns
1055
        -------
1056
        projected: :class:`numpy.ndarray`
1057
            The projected values.
1058
        """
1059
        if axis == "x":
4✔
1060
            return self._x_projection(values)
4✔
1061
        elif axis == "y":
4✔
1062
            return self._y_projection(values)
4✔
1063
        raise ValueError("Unexpected axis: %s" % axis)
×
1064

1065
    def add_mark(self, mark):
4✔
1066
        """Add a mark to the axes.
1067

1068
        This is only of use when creating your own custom Toyplot marks.  It is
1069
        not intended for end-users.
1070

1071
        Example
1072
        -------
1073
        To add your own custom mark to a set of axes::
1074

1075
            mark = axes.add(MyCustomMark())
1076

1077
        Parameters
1078
        ----------
1079
        mark: :class:`toyplot.mark.Mark`, required
1080

1081
        Returns
1082
        -------
1083
        mark: :class:`toyplot.mark.Mark`
1084
        """
1085

1086
        self._scenegraph.add_edge(self, "render", mark)
4✔
1087
        self._scenegraph.add_edge(self.x, "map", mark)
4✔
1088
        self._scenegraph.add_edge(self.y, "map", mark)
4✔
1089

1090
        return mark
4✔
1091

1092
    def bars(
4✔
1093
            self,
1094
            a,
1095
            b=None,
1096
            c=None,
1097
            along="x",
1098
            baseline="stacked",
1099
            color=None,
1100
            filename=None,
1101
            hyperlink=None,
1102
            opacity=1.0,
1103
            style=None,
1104
            title=None,
1105
            ):
1106
        """Add stacked bars to the axes.
1107

1108
        This command generates one-or-more series of stacked bars.  For
1109
        convenience, you can call it with many different types of input.  To
1110
        generate a single series of :math:`M` bars, pass an optional vector of
1111
        :math:`M` bar positions plus a vector of :math:`M` bar magnitudes:
1112

1113
        >>> axes.bars(magnitudes)
1114
        >>> axes.bars(centers, magnitudes)
1115
        >>> axes.bars(minpos, maxpos, magnitudes)
1116

1117
        To generate :math:`N` stacked series of :math:`M` bars, pass an optional
1118
        vector of :math:`M` bar positions plus an :math:`M \\times N` matrix of
1119
        bar magnitudes:
1120

1121
        >>> axes.bars(magnitudes)
1122
        >>> axes.bars(centers, magnitudes)
1123
        >>> axes.bars(minpos, maxpos, magnitudes)
1124

1125
        As a convenience for working with :func:`numpy.histogram`, you may pass
1126
        a 2-tuple containing :math:`M` counts and :math:`M+1` bin edges:
1127

1128
        >>> axes.bars((counts, edges))
1129

1130
        Alternatively, you can generate :math:`N-1` stacked series of :math:`M`
1131
        bars by passing an optional vector of :math:`M` bar positions plus an
1132
        :math:`M \\times N` matrix of bar *boundaries*:
1133

1134
        >>> axes.bars(boundaries, baseline=None)
1135
        >>> axes.bars(centers, boundaries, baseline=None)
1136
        >>> axes.bars(minpos, maxpos, boundaries, baseline=None)
1137

1138
        Parameters
1139
        ----------
1140
        a, b, c: array-like series data.
1141
        along: string, "x" or "y", optional
1142
          Specify "x" (the default) for vertical bars, or "y" for horizontal bars.
1143
        baseline: array-like, "stacked", "symmetrical", "wiggle", or None
1144
        color: array-like set of colors, optional
1145
          Specify a single color for all bars, one color per series, or one color per bar.
1146
          Color values can be explicit toyplot colors, or scalar values to be mapped
1147
          to colors using the `colormap` or `palette` parameter.
1148
        opacity: array-like set of opacities, optional
1149
          Specify a single opacity for all bars, one opacity per series, or one opacity per bar.
1150
        title: array-like set of strings, optional
1151
          Specify a single title, one title per series, or one title per bar.
1152
        hyperlink: array-like set of strings, optional
1153
          Specify a single hyperlink, one hyperlink per series, or one hyperlink per bar.
1154
        style: dict, optional
1155
          Collection of CSS styles to be applied globally.
1156

1157
        Returns
1158
        -------
1159
        bars: :class:`toyplot.mark.BarBoundaries` or :class:`toyplot.mark.BarMagnitudes`
1160
        """
1161
        along = toyplot.require.value_in(along, ["x", "y"])
4✔
1162

1163
        if baseline is None:
4✔
1164
            if a is not None and b is not None and c is not None:
4✔
1165
                a = toyplot.require.scalar_vector(a)
4✔
1166
                b = toyplot.require.scalar_vector(b, len(a))
4✔
1167
                c = toyplot.require.scalar_array(c)
4✔
1168
                if c.ndim == 1:
4✔
1169
                    c = toyplot.require.scalar_vector(c, len(a))
4✔
1170
                    series = numpy.ma.column_stack(
4✔
1171
                        (numpy.repeat(0, len(c)), c))
1172
                elif c.ndim == 2:
4✔
1173
                    series = toyplot.require.scalar_matrix(c)
4✔
1174
                position = numpy.ma.column_stack((a, b))
4✔
1175
            elif a is not None and b is not None:
4✔
1176
                a = toyplot.require.scalar_vector(a)
4✔
1177
                b = toyplot.require.scalar_array(b)
4✔
1178
                if b.ndim == 1:
4✔
1179
                    b = toyplot.require.scalar_vector(b, len(a))
4✔
1180
                    series = numpy.ma.column_stack(
4✔
1181
                        (numpy.repeat(0, len(b)), b))
1182
                elif b.ndim == 2:
4✔
1183
                    series = toyplot.require.scalar_matrix(b)
4✔
1184
                position = numpy.concatenate(
4✔
1185
                    (a[0:1] - (a[1:2] - a[0:1]) * 0.5, (a[:-1] + a[1:]) * 0.5, a[-1:] + (a[-1:] - a[-2:-1]) * 0.5))
1186
                position = numpy.ma.column_stack((position[:-1], position[1:]))
4✔
1187
            else:
1188
                a = toyplot.require.scalar_array(a)
4✔
1189
                if a.ndim == 1:
4✔
1190
                    a = toyplot.require.scalar_vector(a)
4✔
1191
                    series = numpy.ma.column_stack(
4✔
1192
                        (numpy.repeat(0, len(a)), a))
1193
                elif a.ndim == 2:
4✔
1194
                    series = toyplot.require.scalar_matrix(a)
4✔
1195
                position = numpy.ma.column_stack((numpy.arange(series.shape[0]) - 0.5, numpy.arange(series.shape[0]) + 0.5))
4✔
1196

1197
            default_color = [next(self._bar_colors)
4✔
1198
                             for i in range(series.shape[1] - 1)]
1199
            color = toyplot.color.broadcast(
4✔
1200
                colors=color,
1201
                shape=(series.shape[0], series.shape[1] - 1),
1202
                default=default_color,
1203
                )
1204
            opacity = toyplot.broadcast.scalar(
4✔
1205
                opacity, (series.shape[0], series.shape[1] - 1))
1206
            title = toyplot.broadcast.pyobject(
4✔
1207
                title, (series.shape[0], series.shape[1] - 1))
1208
            hyperlink = toyplot.broadcast.pyobject(
4✔
1209
                hyperlink, (series.shape[0], series.shape[1] - 1))
1210
            style = toyplot.style.combine(
4✔
1211
                {"stroke": "white", "stroke-width": 1.0},
1212
                toyplot.style.require(style, allowed=toyplot.style.allowed.fill),
1213
                )
1214

1215
            if along == "x":
4✔
1216
                coordinate_axes = ["x", "y"]
4✔
1217
            elif along == "y":
4✔
1218
                coordinate_axes = ["y", "x"]
4✔
1219

1220
            table = toyplot.data.Table()
4✔
1221
            table["left"] = position.T[0]
4✔
1222
            _mark_exportable(table, "left")
4✔
1223
            table["right"] = position.T[1]
4✔
1224
            _mark_exportable(table, "right")
4✔
1225
            boundary_keys = []
4✔
1226
            fill_keys = []
4✔
1227
            opacity_keys = []
4✔
1228
            title_keys = []
4✔
1229
            hyperlink_keys = []
4✔
1230

1231
            boundary_keys.append("boundary0")
4✔
1232
            table[boundary_keys[-1]] = series.T[0]
4✔
1233

1234
            for index, (boundary_column, fill_column, opacity_column, title_column, hyperlink_column) in enumerate(
4✔
1235
                    zip(series.T[1:], color.T, opacity.T, title.T, hyperlink.T)):
1236
                boundary_keys.append("boundary" + str(index + 1))
4✔
1237
                fill_keys.append("fill" + str(index))
4✔
1238
                opacity_keys.append("opacity" + str(index))
4✔
1239
                title_keys.append("title" + str(index))
4✔
1240
                hyperlink_keys.append("hyperlink" + str(index))
4✔
1241
                table[boundary_keys[-1]] = boundary_column
4✔
1242
                _mark_exportable(table, boundary_keys[-1])
4✔
1243
                table[fill_keys[-1]] = fill_column
4✔
1244
                table[opacity_keys[-1]] = opacity_column
4✔
1245
                table[title_keys[-1]] = title_column
4✔
1246
                table[hyperlink_keys[-1]] = hyperlink_column
4✔
1247

1248
            return self.add_mark(
4✔
1249
                toyplot.mark.BarBoundaries(
1250
                    coordinate_axes=coordinate_axes,
1251
                    table=table,
1252
                    left="left",
1253
                    right="right",
1254
                    boundaries=boundary_keys,
1255
                    fill=fill_keys,
1256
                    opacity=opacity_keys,
1257
                    title=title_keys,
1258
                    hyperlink=hyperlink_keys,
1259
                    style=style,
1260
                    filename=filename,
1261
                    ))
1262
        else:  # baseline is not None
1263
            if a is not None and b is not None and c is not None:
4✔
1264
                a = toyplot.require.scalar_vector(a)
4✔
1265
                b = toyplot.require.scalar_vector(b, len(a))
4✔
1266
                c = toyplot.require.scalar_array(c)
4✔
1267
                if c.ndim == 1:
4✔
1268
                    c = toyplot.require.scalar_vector(c, len(a))
4✔
1269
                    series = numpy.ma.column_stack((c,))
4✔
1270
                elif c.ndim == 2:
4✔
1271
                    series = toyplot.require.scalar_matrix(c, rows=len(a))
4✔
1272
                position = numpy.ma.column_stack((a, b))
4✔
1273
            elif a is not None and b is not None:
4✔
1274
                a = toyplot.require.scalar_vector(a)
4✔
1275
                b = toyplot.require.scalar_array(b)
4✔
1276
                if b.ndim == 1:
4✔
1277
                    b = toyplot.require.scalar_vector(b, len(a))
4✔
1278
                    series = numpy.ma.column_stack((b,))
4✔
1279
                elif b.ndim == 2:
4✔
1280
                    series = toyplot.require.scalar_matrix(b, rows=len(a))
4✔
1281
                position = numpy.concatenate(
4✔
1282
                    (a[0:1] - (a[1:2] - a[0:1]) * 0.5, (a[:-1] + a[1:]) * 0.5, a[-1:] + (a[-1:] - a[-2:-1]) * 0.5))
1283
                position = numpy.ma.column_stack((position[:-1], position[1:]))
4✔
1284
            elif a is not None:
4✔
1285
                if isinstance(a, tuple) and len(a) == 2:
4✔
1286
                    counts, edges = a
4✔
1287
                    position = numpy.ma.column_stack((edges[:-1], edges[1:]))
4✔
1288
                    series = numpy.ma.column_stack((toyplot.require.scalar_vector(counts, len(position)), ))
4✔
1289
                else:
1290
                    a = toyplot.require.scalar_array(a)
4✔
1291
                    if a.ndim == 1:
4✔
1292
                        series = numpy.ma.column_stack((a,))
4✔
1293
                    elif a.ndim == 2:
4✔
1294
                        series = a
4✔
1295
                    position = numpy.ma.column_stack((numpy.arange(series.shape[0]) - 0.5, numpy.arange(series.shape[0]) + 0.5))
4✔
1296

1297
            default_color = [next(self._bar_colors)
4✔
1298
                             for i in range(series.shape[1])]
1299
            color = toyplot.color.broadcast(
4✔
1300
                colors=color,
1301
                shape=series.shape,
1302
                default=default_color,
1303
                )
1304
            opacity = toyplot.broadcast.scalar(opacity, series.shape)
4✔
1305
            title = toyplot.broadcast.pyobject(title, series.shape)
4✔
1306
            hyperlink = toyplot.broadcast.pyobject(hyperlink, series.shape)
4✔
1307
            style = toyplot.style.combine(
4✔
1308
                {"stroke": "white", "stroke-width": 1.0},
1309
                toyplot.style.require(style, allowed=toyplot.style.allowed.fill),
1310
                )
1311

1312
            if isinstance(baseline, str):
4✔
1313
                baseline = toyplot.require.value_in(baseline, ["stacked", "symmetric", "wiggle"])
4✔
1314
                if baseline == "stacked":
4✔
1315
                    baseline = numpy.zeros(series.shape[0])
4✔
1316
                elif baseline == "symmetric":
4✔
1317
                    baseline = -0.5 * numpy.sum(series, axis=1)
4✔
1318
                elif baseline == "wiggle":
4✔
1319
                    n = series.shape[1]
4✔
1320
                    baseline = numpy.zeros(series.shape[0])
4✔
1321
                    for i in range(n):
4✔
1322
                        for j in range(i):
4✔
1323
                            baseline += series.T[j]
4✔
1324
                    baseline *= -(1.0 / (n + 1))
4✔
1325

1326
            if along == "x":
4✔
1327
                coordinate_axes = ["x", "y"]
4✔
1328
            elif along == "y":
4✔
1329
                coordinate_axes = ["y", "x"]
4✔
1330

1331
            table = toyplot.data.Table()
4✔
1332
            table["left"] = position.T[0]
4✔
1333
            _mark_exportable(table, "left")
4✔
1334
            table["right"] = position.T[1]
4✔
1335
            _mark_exportable(table, "right")
4✔
1336
            table["baseline"] = baseline
4✔
1337
            _mark_exportable(table, "baseline")
4✔
1338
            magnitude_keys = []
4✔
1339
            fill_keys = []
4✔
1340
            opacity_keys = []
4✔
1341
            title_keys = []
4✔
1342
            hyperlink_keys = []
4✔
1343
            for index, (magnitude_column, fill_column, opacity_column, title_column, hyperlink_column) in enumerate(
4✔
1344
                    zip(series.T, color.T, opacity.T, title.T, hyperlink.T)):
1345
                magnitude_keys.append("magnitude" + str(index))
4✔
1346
                fill_keys.append("fill" + str(index))
4✔
1347
                opacity_keys.append("opacity" + str(index))
4✔
1348
                title_keys.append("title" + str(index))
4✔
1349
                hyperlink_keys.append("hyperlink" + str(index))
4✔
1350
                table[magnitude_keys[-1]] = magnitude_column
4✔
1351
                _mark_exportable(table, magnitude_keys[-1])
4✔
1352
                table[fill_keys[-1]] = fill_column
4✔
1353
                table[opacity_keys[-1]] = opacity_column
4✔
1354
                table[title_keys[-1]] = title_column
4✔
1355
                table[hyperlink_keys[-1]] = hyperlink_column
4✔
1356

1357
            return self.add_mark(
4✔
1358
                toyplot.mark.BarMagnitudes(
1359
                    coordinate_axes=coordinate_axes,
1360
                    table=table,
1361
                    left="left",
1362
                    right="right",
1363
                    baseline="baseline",
1364
                    magnitudes=magnitude_keys,
1365
                    fill=fill_keys,
1366
                    opacity=opacity_keys,
1367
                    title=title_keys,
1368
                    hyperlink=hyperlink_keys,
1369
                    style=style,
1370
                    filename=filename,
1371
                    ))
1372

1373
    def color_scale(
4✔
1374
            self,
1375
            colormap,
1376
            label=None,
1377
            tick_locator=None,
1378
            width=10,
1379
            padding=10,
1380
        ):
1381
        """Add a color scale to the axes.
1382

1383
        The color scale displays a mapping from scalar values to colors, for
1384
        the given colormap.  Note that the supplied colormap must have an
1385
        explicitly defined domain (specified when the colormap was created),
1386
        otherwise the mapping would be undefined.
1387

1388
        Parameters
1389
        ----------
1390
        colormap: :class:`toyplot.color.Map`, required
1391
          Colormap to be displayed.
1392
        label: string, optional
1393
          Human-readable label placed below the axis.
1394
        ticklocator: :class:`toyplot.locator.TickLocator`, optional
1395
          Controls the placement and formatting of axis ticks and tick labels.
1396

1397
        Returns
1398
        -------
1399
        axes: :class:`toyplot.coordinates.Numberline`
1400
        """
1401

1402
        axis = self._scenegraph.source("render", self).color_scale(
4✔
1403
            colormap=colormap,
1404
            x1=self._xmax_range + width + self._padding,
1405
            x2=self._xmax_range + width + self._padding,
1406
            y1=self._ymax_range,
1407
            y2=self._ymin_range,
1408
            width=width,
1409
            padding=padding,
1410
            show=True,
1411
            label=label,
1412
            ticklocator=tick_locator,
1413
            scale="linear",
1414
            )
1415
        return axis
4✔
1416

1417
    def ellipse(
4✔
1418
            self,
1419
            x,
1420
            y,
1421
            rx,
1422
            ry,
1423
            angle=None,
1424
            color=None,
1425
            opacity=1.0,
1426
            title=None,
1427
            style=None,
1428
            filename=None,
1429
        ):
1430
        """Add ellipses to the axes.
1431

1432
        This command creates a single series of one-or-more ellipses.  To create
1433
        one ellipse, pass scalar values for the center and x and y radiuses:
1434

1435
        >>> axes.ellipse(xcenter, ycenter, xradius, yradius)
1436

1437
        You may also specify an optional angle, measured in degrees, that will
1438
        be used to rotate the ellipse counter-clockwise around its center:
1439

1440
        >>> axes.ellipse(xcenter, ycenter, xradius, yradius, angle)
1441

1442
        To create :math:`M` ellipses, pass size-:math:`M` vectors for each
1443
        of the parameters:
1444

1445
        >>> axes.ellipse(xcenters, ycenters, xradiuses, yradiuses)
1446
        >>> axes.ellipse(xcenters, ycenters, xradiuses, yradiuses, angles)
1447

1448
        Parameters
1449
        ----------
1450
        x, y: array-like series of center coordinates.
1451
        rx, ry: array-like series of x and y radiuses.
1452
        angle: array-like series of rotation angles, optional.
1453
        color: array-like series of colors, optional.
1454
            Specify a single color for all ellipses, or one color per ellipse.
1455
            Color values can be explicit Toyplot colors, scalar values to be
1456
            mapped to colors with a default colormap, or a (scalar, colormap)
1457
            tuple containing scalar values to be mapped to colors with the given
1458
            colormap.
1459
        opacity: array-like set of opacities, optional.
1460
            Specify a single opacity for all ellipses, or one opacity per ellipse.
1461
        title: array like set of strings, optional.
1462
            Specify a single title for all ellipses, or one title per ellipse.
1463
        style: dict, optional
1464
            Collection of CSS styles to be applied to every ellipse.
1465
        filename: string, optional
1466
            Specify a default filename to be used if the end-viewer decides to export
1467
            the plot data.
1468

1469
        Returns
1470
        -------
1471
        mark: :class:`toyplot.mark.Ellipse` containing the mark data.
1472
        """
1473

1474
        if angle is None:
4✔
1475
            angle = numpy.zeros_like(x)
4✔
1476

1477
        table = toyplot.data.Table()
4✔
1478
        table["x"] = toyplot.require.scalar_vector(x)
4✔
1479
        table["y"] = toyplot.require.scalar_vector(y, length=table.shape[0])
4✔
1480
        table["rx"] = toyplot.require.scalar_vector(rx, length=table.shape[0])
4✔
1481
        table["ry"] = toyplot.require.scalar_vector(ry, length=table.shape[0])
4✔
1482
        table["angle"] = toyplot.require.scalar_vector(angle, length=table.shape[0])
4✔
1483
        table["opacity"] = toyplot.broadcast.scalar(opacity, table.shape[0])
4✔
1484
        table["title"] = toyplot.broadcast.pyobject(title, table.shape[0])
4✔
1485
        style = toyplot.style.combine(
4✔
1486
            {"stroke": "none"},
1487
            toyplot.style.require(style, allowed=toyplot.style.allowed.fill),
1488
            )
1489

1490
        default_color = [next(self._rect_colors)]
4✔
1491
        table["toyplot:fill"] = toyplot.color.broadcast(
4✔
1492
            colors=color,
1493
            shape=(table.shape[0], 1),
1494
            default=default_color,
1495
            )[:, 0]
1496

1497
        coordinate_axes = ["x", "y"]
4✔
1498

1499
        return self.add_mark(
4✔
1500
            toyplot.mark.Ellipse(
1501
                coordinate_axes,
1502
                table=table,
1503
                x=["x"],
1504
                y=["y"],
1505
                rx=["rx"],
1506
                ry=["ry"],
1507
                angle=["angle"],
1508
                fill=["toyplot:fill"],
1509
                opacity=["opacity"],
1510
                title=["title"],
1511
                style=style,
1512
                filename=filename,
1513
                ))
1514

1515
    def fill(
4✔
1516
            self,
1517
            a,
1518
            b=None,
1519
            c=None,
1520
            along="x",
1521
            baseline=None,
1522
            color=None,
1523
            opacity=1.0,
1524
            title=None,
1525
            style=None,
1526
            annotation=False,
1527
            filename=None,
1528
        ):
1529
        """Fill multiple regions separated by curves.
1530

1531
        Parameters
1532
        ----------
1533
        a, b, c: array-like sets of coordinates
1534
          If `a`, `b`, and `c` are provided, they specify the X coordinates, bottom
1535
          coordinates, and top coordinates of the region respectively.  If only `a`
1536
          and `b` are provided, they specify the X coordinates and top
1537
          coordinates, with the bottom coordinates lying on the X axis.  If only `a` is
1538
          provided, it specifies the top coordinates, with the bottom coordinates lying
1539
          on the X axis and the X coordinates ranging from [0, N).
1540
        title: string, optional
1541
          Human-readable title for the mark.  The SVG / HTML backends render the
1542
          title as a tooltip.
1543
        style: dict, optional
1544
          Collection of CSS styles to apply to the mark.  See
1545
          :class:`toyplot.mark.FillBoundaries` for a list of useful styles.
1546
        annotation: boolean, optional
1547
          Set to True if this mark should be considered an annotation.
1548

1549
        Returns
1550
        -------
1551
        mark: :class:`toyplot.mark.FillBoundaries` or :class:`toyplot.mark.FillMagnitudes`
1552
        """
1553
        along = toyplot.require.value_in(along, ["x", "y"])
4✔
1554

1555
        if baseline is None:
4✔
1556
            if a is not None and b is not None and c is not None:
4✔
1557
                position = toyplot.require.scalar_vector(a)
4✔
1558
                bottom = toyplot.require.scalar_vector(b, len(position))
4✔
1559
                top = toyplot.require.scalar_vector(c, len(position))
4✔
1560
                series = numpy.ma.column_stack((bottom, top))
4✔
1561
            elif a is not None and b is not None:
4✔
1562
                position = toyplot.require.scalar_vector(a)
4✔
1563
                b = toyplot.require.scalar_array(b)
4✔
1564
                if b.ndim == 1:
4✔
1565
                    bottom = numpy.ma.repeat(0, len(a))
4✔
1566
                    top = toyplot.require.scalar_vector(b, len(a))
4✔
1567
                    series = numpy.ma.column_stack((bottom, top))
4✔
1568
                elif b.ndim == 2:
4✔
1569
                    series = toyplot.require.scalar_matrix(b)
4✔
1570
            else:
1571
                a = toyplot.require.scalar_array(a)
4✔
1572
                if a.ndim == 1:
4✔
1573
                    bottom = numpy.ma.repeat(0, len(a))
4✔
1574
                    top = toyplot.require.scalar_vector(a)
4✔
1575
                    series = numpy.ma.column_stack((bottom, top))
4✔
1576
                    position = numpy.ma.arange(series.shape[0])
4✔
1577
                elif a.ndim == 2:
4✔
1578
                    series = toyplot.require.scalar_matrix(a)
4✔
1579
                    position = numpy.ma.arange(series.shape[0])
4✔
1580

1581
            default_color = [next(self._fill_colors)
4✔
1582
                             for i in range(series.shape[1] - 1)]
1583
            color = toyplot.color.broadcast(
4✔
1584
                colors=color,
1585
                shape=(series.shape[1] - 1,),
1586
                default=default_color,
1587
                )
1588
            opacity = toyplot.broadcast.scalar(opacity, series.shape[1] - 1)
4✔
1589
            title = toyplot.broadcast.pyobject(title, series.shape[1] - 1)
4✔
1590
            style = toyplot.style.combine(
4✔
1591
                {"stroke": "none"},
1592
                toyplot.style.require(style, allowed=toyplot.style.allowed.fill),
1593
                )
1594

1595
            if along == "x":
4✔
1596
                coordinate_axes = ["x", "y"]
4✔
1597
            elif along == "y":
4✔
1598
                coordinate_axes = ["y", "x"]
4✔
1599

1600
            table = toyplot.data.Table()
4✔
1601
            table[coordinate_axes[0]] = position
4✔
1602
            _mark_exportable(table, coordinate_axes[0])
4✔
1603
            boundaries = []
4✔
1604
            for index, column in enumerate(series.T):
4✔
1605
                key = coordinate_axes[1] + str(index)
4✔
1606
                table[key] = column
4✔
1607
                _mark_exportable(table, key)
4✔
1608
                boundaries.append(key)
4✔
1609

1610
            return self.add_mark(
4✔
1611
                toyplot.mark.FillBoundaries(
1612
                    coordinate_axes=coordinate_axes,
1613
                    table=table,
1614
                    position=[coordinate_axes[0]],
1615
                    boundaries=boundaries,
1616
                    fill=color,
1617
                    opacity=opacity,
1618
                    title=title,
1619
                    style=style,
1620
                    annotation=annotation,
1621
                    filename=filename,
1622
                    ))
1623
        else:  # baseline is not None
1624
            if a is not None and b is not None:
4✔
1625
                b = toyplot.require.scalar_array(b)
4✔
1626
                if b.ndim == 1:
4✔
1627
                    series = numpy.ma.column_stack((b,))
4✔
1628
                elif b.ndim == 2:
4✔
1629
                    series = b
4✔
1630
                position = toyplot.require.scalar_vector(a, series.shape[0])
4✔
1631
            else:
1632
                a = toyplot.require.scalar_array(a)
4✔
1633
                if a.ndim == 1:
4✔
1634
                    series = numpy.ma.column_stack((a,))
4✔
1635
                elif a.ndim == 2:
4✔
1636
                    series = a
4✔
1637
                position = numpy.ma.arange(series.shape[0])
4✔
1638

1639
            default_color = [next(self._fill_colors)
4✔
1640
                             for i in range(series.shape[1])]
1641
            color = toyplot.color.broadcast(
4✔
1642
                colors=color,
1643
                shape=(series.shape[1],),
1644
                default=default_color,
1645
                )
1646
            opacity = toyplot.broadcast.scalar(opacity, series.shape[1])
4✔
1647
            title = toyplot.broadcast.pyobject(title, series.shape[1])
4✔
1648
            style = toyplot.style.combine(
4✔
1649
                {"stroke": "none"},
1650
                toyplot.style.require(style, allowed=toyplot.style.allowed.fill),
1651
                )
1652

1653
            if isinstance(baseline, str):
4✔
1654
                baseline = toyplot.require.value_in(baseline, ["stacked", "symmetric", "wiggle"])
4✔
1655
                if baseline == "stacked":
4✔
1656
                    baseline = numpy.ma.zeros(series.shape[0])
4✔
1657
                elif baseline == "symmetric":
4✔
1658
                    baseline = -0.5 * numpy.ma.sum(series, axis=1)
4✔
1659
                elif baseline == "wiggle":
4✔
1660
                    n = series.shape[1]
4✔
1661
                    baseline = numpy.ma.zeros(series.shape[0])
4✔
1662
                    for i in range(n):
4✔
1663
                        for j in range(i):
4✔
1664
                            baseline += series.T[j]
4✔
1665
                    baseline *= -(1.0 / (n + 1))
4✔
1666

1667
            if along == "x":
4✔
1668
                coordinate_axes = ["x", "y"]
4✔
1669
            elif along == "y":
4✔
1670
                coordinate_axes = ["y", "x"]
4✔
1671

1672
            table = toyplot.data.Table()
4✔
1673
            table[coordinate_axes[0]] = position
4✔
1674
            _mark_exportable(table, coordinate_axes[0])
4✔
1675
            table["baseline"] = baseline
4✔
1676
            magnitudes = []
4✔
1677
            for index, column in enumerate(series.T):
4✔
1678
                key = coordinate_axes[1] + str(index)
4✔
1679
                table[key] = column
4✔
1680
                _mark_exportable(table, key)
4✔
1681
                magnitudes.append(key)
4✔
1682

1683
            return self.add_mark(
4✔
1684
                toyplot.mark.FillMagnitudes(
1685
                    coordinate_axes=coordinate_axes,
1686
                    table=table,
1687
                    position=[coordinate_axes[0]],
1688
                    baseline=["baseline"],
1689
                    magnitudes=magnitudes,
1690
                    fill=color,
1691
                    opacity=opacity,
1692
                    title=title,
1693
                    style=style,
1694
                    annotation=annotation,
1695
                    filename=filename,
1696
                    ))
1697

1698
    def graph(
1699
            self,
1700
            a,
1701
            b=None,
1702
            c=None,
1703
            olayout=None,
1704
            layout=None,
1705
            along="x",
1706
            ecolor=None,
1707
            efilename=None,
1708
            eopacity=1.0,
1709
            estyle=None,
1710
            ewidth=1.0,
1711
            hmarker=None,
1712
            mmarker=None,
1713
            mposition=0.5,
1714
            tmarker=None,
1715
            varea=None,
1716
            vcolor=None,
1717
            vcoordinates=None,
1718
            vfilename=None,
1719
            vlabel=None,
1720
            vlshow=True,
1721
            vlstyle=None,
1722
            vmarker="o",
1723
            vopacity=1.0,
1724
            vsize=None,
1725
            vstyle=None,
1726
            vtitle=None,
1727
        ): # pragma: no cover
1728
        """Add a graph plot to the axes.
1729

1730
        Parameters
1731
        ----------
1732

1733
        Returns
1734
        -------
1735
        plot: :class:`toyplot.mark.Graph`
1736
        """
1737
        layout = toyplot.layout.graph(a, b, c, olayout=olayout, layout=layout, vcoordinates=vcoordinates)
1738

1739
        along = toyplot.require.value_in(along, ["x", "y"])
1740

1741
        if vlabel is None:
1742
            vlabel = layout.vids
1743
        elif vlabel == False:
1744
            vlabel = [""] * layout.vcount
1745

1746
        default_color = [next(self._graph_colors)]
1747
        vcolor = toyplot.color.broadcast(
1748
            colors=vcolor,
1749
            shape=layout.vcount,
1750
            default=default_color,
1751
            )
1752

1753
        vmarker = toyplot.broadcast.pyobject(vmarker, layout.vcount)
1754

1755
        if varea is None and vsize is None:
1756
            vsize = toyplot.broadcast.scalar(4, layout.vcount)
1757
        elif varea is None and vsize is not None:
1758
            vsize = toyplot.broadcast.scalar(vsize, layout.vcount)
1759
        elif varea is not None and vsize is None:
1760
            vsize = numpy.sqrt(toyplot.broadcast.scalar(varea, layout.vcount))
1761
        else:
1762
            toyplot.log.warning("Graph vsize parameter overrides varea.")
1763
            vsize = toyplot.broadcast.scalar(vsize, layout.vcount)
1764

1765
        vopacity = toyplot.broadcast.scalar(vopacity, layout.vcount)
1766
        vtitle = toyplot.broadcast.pyobject(vtitle, layout.vcount)
1767
        vstyle = toyplot.style.require(vstyle, allowed=toyplot.style.allowed.marker)
1768
        vlstyle = toyplot.style.combine(
1769
            {
1770
                "font-size": "12px",
1771
                "font-weight": "normal",
1772
                "stroke": "none",
1773
                "text-anchor": "middle",
1774
                "-toyplot-vertical-align": "middle",
1775
            },
1776
            toyplot.style.require(vlstyle, allowed=toyplot.style.allowed.text),
1777
            )
1778

1779
        ecolor = toyplot.color.broadcast(
1780
            colors=ecolor,
1781
            shape=layout.ecount,
1782
            default=default_color,
1783
            )
1784
        ewidth = toyplot.broadcast.scalar(ewidth, layout.ecount)
1785
        eopacity = toyplot.broadcast.scalar(eopacity, layout.ecount)
1786
        estyle = toyplot.style.require(estyle, allowed=toyplot.style.allowed.line)
1787

1788
        hmarker = toyplot.broadcast.pyobject(hmarker, layout.ecount)
1789
        mmarker = toyplot.broadcast.pyobject(mmarker, layout.ecount)
1790
        mposition = toyplot.broadcast.scalar(mposition, layout.ecount)
1791
        tmarker = toyplot.broadcast.pyobject(tmarker, layout.ecount)
1792

1793
        if along == "x":
1794
            coordinate_axes = ["x", "y"]
1795
        elif along == "y":
1796
            coordinate_axes = ["y", "x"]
1797

1798
        vtable = toyplot.data.Table()
1799
        vtable["id"] = layout.vids
1800
        for axis, coordinates in zip(coordinate_axes, layout.vcoordinates.T):
1801
            vtable[axis] = coordinates
1802
            _mark_exportable(vtable, axis)
1803
        vtable["label"] = vlabel
1804
        vtable["marker"] = vmarker
1805
        vtable["size"] = vsize
1806
        vtable["color"] = vcolor
1807
        vtable["opacity"] = vopacity
1808
        vtable["title"] = vtitle
1809

1810
        etable = toyplot.data.Table()
1811
        etable["source"] = layout.edges.T[0]
1812
        _mark_exportable(etable, "source")
1813
        etable["target"] = layout.edges.T[1]
1814
        _mark_exportable(etable, "target")
1815
        etable["shape"] = layout.eshapes
1816
        etable["color"] = ecolor
1817
        etable["width"] = ewidth
1818
        etable["opacity"] = eopacity
1819
        etable["hmarker"] = hmarker
1820
        etable["mmarker"] = mmarker
1821
        etable["mposition"] = mposition
1822
        etable["tmarker"] = tmarker
1823

1824
        return self.add_mark(
1825
            toyplot.mark.Graph(
1826
                coordinate_axes=coordinate_axes,
1827
                ecolor=["color"],
1828
                ecoordinates=layout.ecoordinates,
1829
                efilename=efilename,
1830
                eopacity=["opacity"],
1831
                eshape=["shape"],
1832
                esource=["source"],
1833
                estyle=estyle,
1834
                etable=etable,
1835
                etarget=["target"],
1836
                ewidth=["width"],
1837
                hmarker=["hmarker"],
1838
                mmarker=["mmarker"],
1839
                mposition=["mposition"],
1840
                tmarker=["tmarker"],
1841
                vcolor=["color"],
1842
                vcoordinates=coordinate_axes,
1843
                vfilename=vfilename,
1844
                vid=["id"],
1845
                vlabel=["label"],
1846
                vlshow=vlshow,
1847
                vlstyle=vlstyle,
1848
                vmarker=["marker"],
1849
                vopacity=["opacity"],
1850
                vsize=["size"],
1851
                vstyle=vstyle,
1852
                vtable=vtable,
1853
                vtitle=["title"],
1854
                ))
1855

1856
    def hlines(
4✔
1857
            self,
1858
            y,
1859
            color=None,
1860
            opacity=1.0,
1861
            title=None,
1862
            style=None,
1863
            annotation=True,
1864
        ):
1865
        """Add horizontal line(s) to the axes.
1866

1867
        Horizontal lines are convenient because they're guaranteed to fill the axes from
1868
        left to right regardless of the axes size.
1869

1870
        Parameters
1871
        ----------
1872
        y: array-like set of Y coordinates
1873
          One horizontal line will be drawn through each Y coordinate provided.
1874
        title: string, optional
1875
          Human-readable title for the mark.  The SVG / HTML backends render the
1876
          title as a tooltip.
1877
        style: dict, optional
1878
          Collection of CSS styles to apply to the mark.  See
1879
          :class:`toyplot.mark.AxisLines` for a list of useful styles.
1880
        annotation: boolean, optional
1881
          Set to True if this mark should be considered an annotation.
1882

1883
        Returns
1884
        -------
1885
        hlines: :class:`toyplot.mark.AxisLines`
1886
        """
1887
        table = toyplot.data.Table()
4✔
1888
        table["y"] = toyplot.require.scalar_vector(y)
4✔
1889
        _mark_exportable(table, "y")
4✔
1890
        color = toyplot.color.broadcast(
4✔
1891
            colors=color,
1892
            shape=(table.shape[0], 1),
1893
            default=toyplot.color.black,
1894
            )
1895
        table["color"] = color[:, 0]
4✔
1896
        table["opacity"] = toyplot.broadcast.scalar(opacity, table.shape[0])
4✔
1897
        table["title"] = toyplot.broadcast.pyobject(title, table.shape[0])
4✔
1898
        style = toyplot.style.require(style, allowed=toyplot.style.allowed.line)
4✔
1899

1900
        return self.add_mark(
4✔
1901
            toyplot.mark.AxisLines(
1902
                coordinate_axes=["y"],
1903
                table=table,
1904
                coordinates=["y"],
1905
                stroke=["color"],
1906
                opacity=["opacity"],
1907
                title=["title"],
1908
                style=style,
1909
                annotation=annotation,
1910
                ))
1911

1912
    def plot(
4✔
1913
            self,
1914
            a,
1915
            b=None,
1916
            along="x",
1917
            color=None,
1918
            stroke_width=2.0,
1919
            opacity=1.0,
1920
            title=None,
1921
            marker=None,
1922
            area=None,
1923
            size=None,
1924
            mfill=None,
1925
            mopacity=1.0,
1926
            mtitle=None,
1927
            style=None,
1928
            mstyle=None,
1929
            mlstyle=None,
1930
            filename=None,
1931
        ):
1932
        """Add bivariate line plots to the axes.
1933

1934
        Parameters
1935
        ----------
1936
        a, b: array-like sets of coordinates
1937
          If `a` and `b` are provided, they specify the first and second
1938
          coordinates respectively of each point in the plot.  If only `a` is provided, it
1939
          provides second coordinates, and the first coordinates will range from [0, N).
1940
        along: string, optional
1941
          Controls the mapping from coordinates to axes.  When set to "x" (the default),
1942
          first and second coordinates map to the X and Y axes.  When set to "y", the
1943
          coordinates are reversed.
1944
        color: array-like, optional
1945
          Overrides the default per-series colors provided by the axis palette.  Specify
1946
          one color, or one-color-per-series.  Colors may be CSS colors, toyplot colors,
1947
          or scalar values that will be mapped to colors using `colormap` or `palette`.
1948
        stroke_width: array-like, optional
1949
          Overrides the default stroke width of the plots.  Specify one width in drawing
1950
          units, or one-width-per-series.
1951
        stroke_opacity: array-like, optional
1952
          Overrides the default opacity of the plots.  Specify one opacity, or one-opacity-per-series.
1953
        marker: array-like, optional
1954
          Allows markers to be rendered for each plot datum. Specify one marker,
1955
          one-marker-per-series, or one-marker-per-datum.  Markers can use the
1956
          string marker type as a shortcut, or a full marker specification.
1957
        size: array-like, optional
1958
          Controls marker sizes.  Specify one size, one-size-per-series, or one-size-per-datum.
1959
        fill: array-like, optional
1960
          Override the fill color for markers, which defaults to the per-series color specified
1961
          by `color`.  Specify one color, one-color-per-series, or one-color-per-datum.  Colors
1962
          may be CSS colors, toyplot colors, or scalar values that will be mapped to colors using
1963
          `fill_colormap` or `fill_palette`.
1964
        opacity: array-like, optional
1965
          Overrides the default opacity of the markers.  Specify one opacity, one-opacity-per-series,
1966
          or one-opacity-per-datum.
1967
        title: array-like, optional
1968
          Human-readable title for the data series.  The SVG / HTML backends render the
1969
          title using tooltips.  Specify one title or one-title-per-series.
1970
        style: dict, optional
1971
          Collection of CSS styles applied to all plots.
1972
        mstyle: dict, optional
1973
          Collection of CSS styles applied to all markers.
1974
        mlstyle: dict, optional
1975
          Collection of CSS styles applied to all marker labels.
1976

1977
        Returns
1978
        -------
1979
        mark: :class:`toyplot.mark.Plot`
1980
        """
1981
        along = toyplot.require.value_in(along, ["x", "y"])
4✔
1982

1983
        if a is not None and b is not None:
4✔
1984
            position = toyplot.require.scalar_vector(a)
4✔
1985
            b = toyplot.require.scalar_array(b)
4✔
1986
            if b.ndim == 1:
4✔
1987
                b = toyplot.require.scalar_vector(b, len(position))
4✔
1988
                series = numpy.ma.column_stack((b,))
4✔
1989
            elif b.ndim == 2:
4✔
1990
                series = toyplot.require.scalar_matrix(b, rows=len(position))
4✔
1991
        else:
1992
            a = toyplot.require.scalar_array(a)
4✔
1993
            if a.ndim == 1:
4✔
1994
                series = numpy.ma.column_stack((a,))
4✔
1995
                position = numpy.ma.arange(series.shape[0])
4✔
1996
            elif a.ndim == 2:
4✔
1997
                series = a
4✔
1998
                position = numpy.ma.arange(series.shape[0])
4✔
1999

2000
        default_color = [next(self._plot_colors) for i in range(series.shape[1])]
4✔
2001
        stroke = toyplot.color.broadcast(
4✔
2002
            colors=color,
2003
            shape=(series.shape[1],),
2004
            default=default_color,
2005
            )
2006
        stroke_width = toyplot.broadcast.scalar(stroke_width, series.shape[1])
4✔
2007
        stroke_opacity = toyplot.broadcast.scalar(
4✔
2008
            opacity, series.shape[1])
2009
        stroke_title = toyplot.broadcast.pyobject(title, series.shape[1])
4✔
2010
        marker = toyplot.broadcast.pyobject(marker, series.shape)
4✔
2011

2012
        if area is None and size is None:
4✔
2013
            msize = toyplot.broadcast.scalar(4, series.shape)
4✔
2014
        elif area is None and size is not None:
4✔
2015
            msize = toyplot.broadcast.scalar(size, series.shape)
4✔
2016
        elif area is not None and size is None:
×
2017
            msize = numpy.sqrt(toyplot.broadcast.scalar(area, series.shape))
×
2018
        else:
2019
            toyplot.log.warning("Plot size parameter overrides area.")
×
2020
            msize = toyplot.broadcast.scalar(size, series.shape)
×
2021

2022
        mfill = toyplot.color.broadcast(
4✔
2023
            colors=mfill,
2024
            shape=series.shape,
2025
            default=stroke,
2026
            )
2027
        mstroke = toyplot.color.broadcast(colors=mfill, shape=series.shape)
4✔
2028
        mopacity = toyplot.broadcast.scalar(mopacity, series.shape)
4✔
2029
        mtitle = toyplot.broadcast.pyobject(mtitle, series.shape)
4✔
2030
        style = toyplot.style.require(style, allowed=toyplot.style.allowed.line)
4✔
2031
        mstyle = toyplot.style.require(mstyle, allowed=toyplot.style.allowed.marker)
4✔
2032
        mlstyle = toyplot.style.require(mlstyle, allowed=toyplot.style.allowed.text)
4✔
2033

2034
        if along == "x":
4✔
2035
            coordinate_axes = ["x", "y"]
4✔
2036
        elif along == "y":
4✔
2037
            coordinate_axes = ["y", "x"]
4✔
2038

2039
        table = toyplot.data.Table()
4✔
2040
        table[coordinate_axes[0]] = position
4✔
2041
        _mark_exportable(table, coordinate_axes[0])
4✔
2042
        series_keys = []
4✔
2043
        marker_keys = []
4✔
2044
        msize_keys = []
4✔
2045
        mfill_keys = []
4✔
2046
        mstroke_keys = []
4✔
2047
        mopacity_keys = []
4✔
2048
        mtitle_keys = []
4✔
2049
        for index, (series_column, marker_column, msize_column, mfill_column, mstroke_column, mopacity_column, mtitle_column) in enumerate(
4✔
2050
                zip(series.T, marker.T, msize.T, mfill.T, mstroke.T, mopacity.T, mtitle.T)):
2051
            series_keys.append(coordinate_axes[1] + str(index))
4✔
2052
            marker_keys.append("marker" + str(index))
4✔
2053
            msize_keys.append("size" + str(index))
4✔
2054
            mfill_keys.append("fill" + str(index))
4✔
2055
            mstroke_keys.append("stroke" + str(index))
4✔
2056
            mopacity_keys.append("opacity" + str(index))
4✔
2057
            mtitle_keys.append("title" + str(index))
4✔
2058
            table[series_keys[-1]] = series_column
4✔
2059
            _mark_exportable(table, series_keys[-1])
4✔
2060
            table[marker_keys[-1]] = marker_column
4✔
2061
            table[msize_keys[-1]] = msize_column
4✔
2062
            table[mfill_keys[-1]] = mfill_column
4✔
2063
            table[mstroke_keys[-1]] = mstroke_column
4✔
2064
            table[mopacity_keys[-1]] = mopacity_column
4✔
2065
            table[mtitle_keys[-1]] = mtitle_column
4✔
2066

2067
        return self.add_mark(
4✔
2068
            toyplot.mark.Plot(
2069
                coordinate_axes=coordinate_axes,
2070
                table=table,
2071
                coordinates=[coordinate_axes[0]],
2072
                series=series_keys,
2073
                stroke=stroke,
2074
                stroke_width=stroke_width,
2075
                stroke_opacity=stroke_opacity,
2076
                stroke_title=stroke_title,
2077
                marker=marker_keys,
2078
                msize=msize_keys,
2079
                mfill=mfill_keys,
2080
                mstroke=mstroke_keys,
2081
                mopacity=mopacity_keys,
2082
                mtitle=mtitle_keys,
2083
                style=style,
2084
                mstyle=mstyle,
2085
                mlstyle=mlstyle,
2086
                filename=filename,
2087
                ))
2088

2089

2090
    def rectangle(
4✔
2091
            self,
2092
            a,
2093
            b,
2094
            c,
2095
            d,
2096
            along="x",
2097
            color=None,
2098
            filename=None,
2099
            opacity=1.0,
2100
            style=None,
2101
            title=None,
2102
        ):
2103
        table = toyplot.data.Table()
4✔
2104
        table["left"] = toyplot.require.scalar_vector(a)
4✔
2105
        table["right"] = toyplot.require.scalar_vector(
4✔
2106
            b, length=table.shape[0])
2107
        table["top"] = toyplot.require.scalar_vector(c, length=table.shape[0])
4✔
2108
        table["bottom"] = toyplot.require.scalar_vector(
4✔
2109
            d, length=table.shape[0])
2110
        table["opacity"] = toyplot.broadcast.scalar(opacity, table.shape[0])
4✔
2111
        table["title"] = toyplot.broadcast.pyobject(title, table.shape[0])
4✔
2112
        style = toyplot.style.combine(
4✔
2113
            {"stroke": "none"},
2114
            toyplot.style.require(style, allowed=toyplot.style.allowed.fill),
2115
            )
2116

2117
        default_color = [next(self._rect_colors)]
4✔
2118
        table["toyplot:fill"] = toyplot.color.broadcast(
4✔
2119
            colors=color,
2120
            shape=(table.shape[0], 1),
2121
            default=default_color,
2122
            )[:, 0]
2123

2124
        if along == "x":
4✔
2125
            coordinate_axes = ["x", "y"]
4✔
2126
        elif along == "y":
4✔
2127
            coordinate_axes = ["y", "x"]
4✔
2128

2129
        return self.add_mark(
4✔
2130
            toyplot.mark.Range(
2131
                coordinate_axes=coordinate_axes,
2132
                coordinates=["left", "right", "top", "bottom"],
2133
                filename=filename,
2134
                fill=["toyplot:fill"],
2135
                opacity=["opacity"],
2136
                style=style,
2137
                table=table,
2138
                title=["title"],
2139
                ))
2140

2141

2142
    def scatterplot(
4✔
2143
            self,
2144
            a,
2145
            b=None,
2146
            along="x",
2147
            area=None,
2148
            color=None,
2149
            filename=None,
2150
            hyperlink=None,
2151
            marker="o",
2152
            mlstyle=None,
2153
            mstyle=None,
2154
            opacity=1.0,
2155
            size=None,
2156
            title=None,
2157
        ):
2158
        """Add a bivariate plot to the axes.
2159

2160
        Parameters
2161
        ----------
2162
        a, b: array-like sets of coordinates
2163
          If `a` and `b` are provided, they specify the X coordinates and Y
2164
          coordinates of each point in the plot.  If only `a` is provided, it
2165
          specifies the Y coordinates, and the X coordinates will range from [0, N).
2166
        title: string, optional
2167
          Human-readable title for the mark.  The SVG / HTML backends render the
2168
          title as a tooltip.
2169
        style: dict, optional
2170
          Collection of CSS styles to apply across all datums.
2171

2172
        Returns
2173
        -------
2174
        plot: :class:`toyplot.mark.Plot`
2175
        """
2176
        along = toyplot.require.value_in(along, ["x", "y"])
4✔
2177

2178
        if a is not None and b is not None:
4✔
2179
            position = toyplot.require.scalar_vector(a)
4✔
2180
            b = numpy.ma.array(b).astype("float64")
4✔
2181
            if b.ndim == 0:
4✔
2182
                b = toyplot.require.scalar_vector(b, len(position))
4✔
2183
                series = numpy.ma.column_stack((b,))
4✔
2184
            elif b.ndim == 1:
4✔
2185
                b = toyplot.require.scalar_vector(b, len(position))
4✔
2186
                series = numpy.ma.column_stack((b,))
4✔
2187
            elif b.ndim == 2:
4✔
2188
                series = toyplot.require.scalar_matrix(b, rows=len(position))
4✔
2189
        else:
2190
            a = numpy.ma.array(a).astype("float64")
4✔
2191
            if a.ndim == 1:
4✔
2192
                series = numpy.ma.column_stack((a,))
4✔
2193
                position = numpy.ma.arange(series.shape[0])
4✔
2194
            elif a.ndim == 2:
4✔
2195
                series = a
4✔
2196
                position = numpy.ma.arange(series.shape[0])
4✔
2197

2198
        default_color = [next(self._scatterplot_colors) for i in range(series.shape[1])]
4✔
2199
        mfill = toyplot.color.broadcast(
4✔
2200
            colors=color,
2201
            shape=series.shape,
2202
            default=default_color,
2203
            )
2204
        marker = toyplot.broadcast.pyobject(marker, series.shape)
4✔
2205

2206
        if area is None and size is None:
4✔
2207
            msize = toyplot.broadcast.scalar(4, series.shape)
4✔
2208
        elif area is None and size is not None:
4✔
2209
            msize = toyplot.broadcast.scalar(size, series.shape)
4✔
2210
        elif area is not None and size is None:
×
2211
            msize = numpy.sqrt(toyplot.broadcast.scalar(area, series.shape))
×
2212
        else:
2213
            toyplot.log.warning("Size parameter overrides area.")
×
2214
            msize = toyplot.broadcast.scalar(size, series.shape)
×
2215

2216
        mstroke = toyplot.color.broadcast(colors=mfill, shape=series.shape)
4✔
2217
        mopacity = toyplot.broadcast.scalar(opacity, series.shape)
4✔
2218
        mtitle = toyplot.broadcast.pyobject(title, series.shape)
4✔
2219
        mhyperlink = toyplot.broadcast.pyobject(hyperlink, series.shape)
4✔
2220
        mstyle = toyplot.style.require(mstyle, allowed=toyplot.style.allowed.marker)
4✔
2221
        mlstyle = toyplot.style.require(mlstyle, allowed=toyplot.style.allowed.text)
4✔
2222

2223
        if along == "x":
4✔
2224
            coordinate_axes = ["x", "y"]
4✔
2225
        elif along == "y":
4✔
2226
            coordinate_axes = ["y", "x"]
4✔
2227

2228
        table = toyplot.data.Table()
4✔
2229
        table[coordinate_axes[0]] = position
4✔
2230
        _mark_exportable(table, coordinate_axes[0])
4✔
2231
        coordinate_keys = []
4✔
2232
        marker_keys = []
4✔
2233
        msize_keys = []
4✔
2234
        mfill_keys = []
4✔
2235
        mstroke_keys = []
4✔
2236
        mopacity_keys = []
4✔
2237
        mtitle_keys = []
4✔
2238
        mhyperlink_keys = []
4✔
2239
        for index, (series_column, marker_column, msize_column, mfill_column, mstroke_column, mopacity_column, mtitle_column, mhyperlink_column) in enumerate(
4✔
2240
                zip(series.T, marker.T, msize.T, mfill.T, mstroke.T, mopacity.T, mtitle.T, mhyperlink.T)):
2241
            coordinate_keys.append(coordinate_axes[0])
4✔
2242
            coordinate_keys.append(coordinate_axes[1] + str(index))
4✔
2243
            marker_keys.append("marker" + str(index))
4✔
2244
            msize_keys.append("size" + str(index))
4✔
2245
            mfill_keys.append("fill" + str(index))
4✔
2246
            mstroke_keys.append("stroke" + str(index))
4✔
2247
            mopacity_keys.append("opacity" + str(index))
4✔
2248
            mtitle_keys.append("title" + str(index))
4✔
2249
            mhyperlink_keys.append("hyperlink" + str(index))
4✔
2250
            table[coordinate_keys[-1]] = series_column
4✔
2251
            _mark_exportable(table, coordinate_keys[-1])
4✔
2252
            table[marker_keys[-1]] = marker_column
4✔
2253
            table[msize_keys[-1]] = msize_column
4✔
2254
            table[mfill_keys[-1]] = mfill_column
4✔
2255
            table[mstroke_keys[-1]] = mstroke_column
4✔
2256
            table[mopacity_keys[-1]] = mopacity_column
4✔
2257
            table[mtitle_keys[-1]] = mtitle_column
4✔
2258
            table[mhyperlink_keys[-1]] = mhyperlink_column
4✔
2259

2260
        return self.add_mark(
4✔
2261
            toyplot.mark.Point(
2262
                coordinate_axes=coordinate_axes,
2263
                coordinates=coordinate_keys,
2264
                filename=filename,
2265
                marker=marker_keys,
2266
                mfill=mfill_keys,
2267
                mhyperlink=mhyperlink_keys,
2268
                mlstyle=mlstyle,
2269
                mopacity=mopacity_keys,
2270
                msize=msize_keys,
2271
                mstroke=mstroke_keys,
2272
                mstyle=mstyle,
2273
                mtitle=mtitle_keys,
2274
                table=table,
2275
                ))
2276

2277
    def share(
4✔
2278
            self,
2279
            axis="x",
2280
            hyperlink=None,
2281
            palette=None,
2282
            xlabel=None,
2283
            xmax=None,
2284
            xmin=None,
2285
            xscale="linear",
2286
            xticklocator=None,
2287
            ylabel=None,
2288
            ymax=None,
2289
            ymin=None,
2290
            yscale="linear",
2291
            yticklocator=None,
2292
        ):
2293
        """Create a Cartesian coordinate system with a shared axis.
2294

2295
        Parameters
2296
        ----------
2297
        axis: string, optional
2298
            The axis that will be shared.  Allowed values are "x" and "y".
2299
        xmin, xmax, ymin, ymax: float, optional
2300
          Used to explicitly override the axis domain (normally, the domain is
2301
          implicitly defined by any marks added to the axes).
2302
        xlabel, ylabel: string, optional
2303
          Human-readable axis labels.
2304
        xticklocator, yticklocator: :class:`toyplot.locator.TickLocator`, optional
2305
          Controls the placement and formatting of axis ticks and tick labels.
2306
        xscale, yscale: "linear", "log", "log10", "log2", or a ("log", <base>) tuple, optional
2307
          Specifies the mapping from data to canvas coordinates along an axis.
2308

2309
        Returns
2310
        -------
2311
        axes: :class:`toyplot.coordinates.Cartesian`
2312
        """
2313

2314
        shared = Cartesian(
4✔
2315
            aspect=self._aspect,
2316
            hyperlink=hyperlink,
2317
            label=None,
2318
            padding=self._padding,
2319
            palette=palette,
2320
            scenegraph=self._scenegraph,
2321
            show=True,
2322
            xaxis=self.x if axis == "x" else None,
2323
            xlabel=xlabel,
2324
            xmax=xmax,
2325
            xmax_range=self._xmax_range,
2326
            xmin=xmin,
2327
            xmin_range=self._xmin_range,
2328
            xscale=xscale,
2329
            xshow=True,
2330
            xticklocator=xticklocator,
2331
            yaxis=self.y if axis == "y" else None,
2332
            ylabel=ylabel,
2333
            ymax=ymax,
2334
            ymax_range=self._ymax_range,
2335
            ymin=ymin,
2336
            ymin_range=self._ymin_range,
2337
            yscale=yscale,
2338
            yshow=True,
2339
            yticklocator=yticklocator,
2340
            )
2341

2342
        shared.x.spine.position = "high" if axis == "y" else "low"
4✔
2343
        shared.y.spine.position = "high" if axis == "x" else "low"
4✔
2344

2345
        self.hyperlink = None
4✔
2346

2347
        for parent in self._scenegraph.sources("render", self):
4✔
2348
            self._scenegraph.add_edge(parent, "render", shared)
4✔
2349

2350
        return shared
4✔
2351

2352
    def text(
4✔
2353
            self,
2354
            a,
2355
            b,
2356
            text,
2357
            angle=0,
2358
            color=None,
2359
            opacity=1.0,
2360
            title=None,
2361
            style=None,
2362
            filename=None,
2363
            annotation=True,
2364
        ):
2365
        """Add text to the axes.
2366

2367
        Parameters
2368
        ----------
2369
        a, b: float
2370
          Coordinates of the text anchor.
2371
        text: string
2372
          The text to be displayed.
2373
        title: string, optional
2374
          Human-readable title for the mark.  The SVG / HTML backends render the
2375
          title as a tooltip.
2376
        style: dict, optional
2377
          Collection of CSS styles to apply to the mark.  See
2378
          :class:`toyplot.mark.Text` for a list of useful styles.
2379
        annotation: boolean, optional
2380
          Set to True if this mark should be considered an annotation.
2381

2382
        Returns
2383
        -------
2384
        text: :class:`toyplot.mark.Text`
2385
        """
2386
        table = toyplot.data.Table()
4✔
2387
        table["x"] = toyplot.require.scalar_vector(a)
4✔
2388
        _mark_exportable(table, "x")
4✔
2389
        table["y"] = toyplot.require.scalar_vector(b, table.shape[0])
4✔
2390
        _mark_exportable(table, "y")
4✔
2391
        table["text"] = toyplot.broadcast.pyobject(text, table.shape[0])
4✔
2392
        _mark_exportable(table, "text")
4✔
2393
        table["angle"] = toyplot.broadcast.scalar(angle, table.shape[0])
4✔
2394
        table["opacity"] = toyplot.broadcast.scalar(opacity, table.shape[0])
4✔
2395
        table["title"] = toyplot.broadcast.pyobject(title, table.shape[0])
4✔
2396
        style = toyplot.style.require(style, allowed=toyplot.style.allowed.text)
4✔
2397

2398
        default_color = [next(self._text_colors)]
4✔
2399

2400
        color = toyplot.color.broadcast(
4✔
2401
            colors=color,
2402
            shape=(table.shape[0], 1),
2403
            default=default_color,
2404
            )
2405
        table["fill"] = color[:, 0]
4✔
2406

2407
        return self.add_mark(
4✔
2408
            toyplot.mark.Text(
2409
                coordinate_axes=["x", "y"],
2410
                table=table,
2411
                coordinates=["x", "y"],
2412
                text=["text"],
2413
                angle=["angle"],
2414
                fill=["fill"],
2415
                opacity=["opacity"],
2416
                title=["title"],
2417
                style=style,
2418
                annotation=annotation,
2419
                filename=filename,
2420
                ))
2421

2422
    def vlines(
4✔
2423
            self,
2424
            x,
2425
            color=None,
2426
            opacity=1.0,
2427
            title=None,
2428
            style=None,
2429
            annotation=True,
2430
        ):
2431
        """Add vertical line(s) to the axes.
2432

2433
        Vertical lines are convenient because they're guaranteed to fill the axes from
2434
        top to bottom regardless of the axes size.
2435

2436
        Parameters
2437
        ----------
2438
        x: array-like set of X coordinates
2439
          One vertical line will be drawn through each X coordinate provided.
2440
        title: string, optional
2441
          Human-readable title for the mark.  The SVG / HTML backends render the
2442
          title as a tooltip.
2443
        style: dict, optional
2444
          Collection of CSS styles to apply to the mark.  See
2445
          :class:`toyplot.mark.AxisLines` for a list of useful styles.
2446
        annotation: boolean, optional
2447
          Set to True if this mark should be considered an annotation.
2448

2449
        Returns
2450
        -------
2451
        mark: :class:`toyplot.mark.AxisLines`
2452
        """
2453
        table = toyplot.data.Table()
4✔
2454
        table["x"] = toyplot.require.scalar_vector(x)
4✔
2455
        _mark_exportable(table, "x")
4✔
2456
        color = toyplot.color.broadcast(
4✔
2457
            colors=color,
2458
            shape=(table.shape[0], 1),
2459
            default=toyplot.color.black,
2460
            )
2461
        table["color"] = color[:, 0]
4✔
2462
        table["opacity"] = toyplot.broadcast.scalar(opacity, table.shape[0])
4✔
2463
        table["title"] = toyplot.broadcast.pyobject(title, table.shape[0])
4✔
2464
        style = toyplot.style.require(style, allowed=toyplot.style.allowed.line)
4✔
2465

2466
        return self.add_mark(
4✔
2467
            toyplot.mark.AxisLines(
2468
                coordinate_axes=["x"],
2469
                table=table,
2470
                coordinates=["x"],
2471
                stroke=["color"],
2472
                opacity=["opacity"],
2473
                title=["title"],
2474
                style=style,
2475
                annotation=annotation,
2476
                ))
2477

2478

2479
##########################################################################
2480
# Numberline
2481

2482
class Numberline(object):
4✔
2483
    """Standard one-dimensional coordinate system / numberline.
2484

2485
    Do not create Numberline instances directly.  Use factory methods such
2486
    as :meth:`toyplot.canvas.Canvas.numberline` instead.
2487
    """
2488
    def __init__(
4✔
2489
            self,
2490
            x1,
2491
            y1,
2492
            x2,
2493
            y2,
2494
            padding,
2495
            palette,
2496
            spacing,
2497
            min,
2498
            max,
2499
            show,
2500
            label,
2501
            ticklocator,
2502
            scale,
2503
            scenegraph,
2504
        ):
2505

2506
        if palette is None:
4✔
2507
            palette = toyplot.color.Palette()
4✔
2508

2509
        self._finalized = None
4✔
2510

2511
        self._axis = Axis(
4✔
2512
            show=show,
2513
            label=label,
2514
            domain_min=min,
2515
            domain_max=max,
2516
            tick_locator=ticklocator,
2517
            tick_angle=0,
2518
            scale=scale,
2519
            )
2520
        self._child_offset = {}
4✔
2521
        self._palette = palette
4✔
2522
        self._scenegraph = scenegraph
4✔
2523
        self._scatterplot_colors = itertools.cycle(self._palette)
4✔
2524
        self._range_colors = itertools.cycle(self._palette)
4✔
2525
        self._child_style = {}
4✔
2526
        self._child_width = {}
4✔
2527
        self._x1 = x1
4✔
2528
        self._x2 = x2
4✔
2529
        self._y1 = y1
4✔
2530
        self._y2 = y2
4✔
2531

2532
        self.padding = padding
4✔
2533
        self.spacing = spacing
4✔
2534

2535
    @property
4✔
2536
    def axis(self):
4✔
2537
        """:class:`toyplot.coordinates.Axis` instance that provides the numberline
2538
        coordinate system."""
2539
        return self._axis
4✔
2540

2541
    @property
4✔
2542
    def show(self):
4✔
2543
        """Control axis visibility.
2544

2545
        Use the `show` property to hide all visible parts of the axis: label,
2546
        spine, ticks, tick labels, etc.  Note that this does not affect
2547
        visibility of the numberline contents, just the axis.
2548
        """
2549
        return self.axis.show
×
2550

2551
    @show.setter
4✔
2552
    def show(self, value):
4✔
2553
        self.axis.show = value
×
2554

2555
    @property
4✔
2556
    def padding(self):
4✔
2557
        """Control the default distance between the axis spine and data.
2558

2559
        By default, the axis spine is offset slightly from the data, to avoid
2560
        visual clutter and overlap.  Use `padding` to change this offset.
2561
        The default units are CSS pixels, but you may specify the padding
2562
        using any :ref:`units` you like.
2563
        """
2564
        return self._padding
4✔
2565

2566
    @padding.setter
4✔
2567
    def padding(self, value):
4✔
2568
        self._padding = toyplot.units.convert(value, target="px", default="px")
4✔
2569

2570
    @property
4✔
2571
    def spacing(self):
4✔
2572
        """Control the default distance between data added to the numberline.
2573

2574
        The default units are CSS pixels, but you may specify the spacing
2575
        using any :ref:`units` you like.
2576
        """
2577
        return self._spacing
×
2578

2579
    @spacing.setter
4✔
2580
    def spacing(self, value):
4✔
2581
        self._spacing = toyplot.units.convert(value, target="px", default="px")
4✔
2582

2583
    def _update_domain(self, values, display=True, data=True):
4✔
2584
        self.axis._update_domain([values], display=display, data=data)
4✔
2585

2586
    def add_mark(self, mark):
4✔
2587
        """Add a mark to the axes.
2588

2589
        This is only of use when creating your own custom Toyplot marks.  It is
2590
        not intended for end-users.
2591

2592
        Example
2593
        -------
2594
        To add your own custom mark to a set of axes::
2595

2596
            mark = axes.add(MyCustomMark())
2597

2598
        Parameters
2599
        ----------
2600
        mark: :class:`toyplot.mark.Mark`, required
2601

2602
        Returns
2603
        -------
2604
        mark: :class:`toyplot.mark.Mark`
2605
        """
2606

2607
        if not isinstance(mark, toyplot.mark.Mark):
4✔
2608
            raise ValueError("Expected toyplot.mark.Mark, received %s" % type(mark))
×
2609
        self._scenegraph.add_edge(self, "render", mark)
4✔
2610
        return mark
4✔
2611

2612

2613
    def _default_offset(self, offset):
4✔
2614
        if offset is None:
4✔
2615
            offset = len(self._scenegraph.targets(self, "render")) * self._spacing
4✔
2616
        return offset
4✔
2617

2618

2619
    def colormap(self, colormap, offset=None, width=10, style=None):
4✔
2620
        if not isinstance(colormap, toyplot.color.Map):
4✔
2621
            raise ValueError("A toyplot.color.Map instance is required.") # pragma: no cover
2622
        if colormap.domain.min is None or colormap.domain.max is None:
4✔
2623
            raise ValueError("Cannot create color scale without explicit colormap domain.") # pragma: no cover
2624

2625
        offset = self._default_offset(offset)
4✔
2626

2627
        self._update_domain(numpy.array([colormap.domain.min, colormap.domain.max]), display=True, data=True)
4✔
2628
        self._child_offset[colormap] = offset
4✔
2629
        self._child_width[colormap] = width
4✔
2630
        self._child_style[colormap] = style
4✔
2631

2632
        self._scenegraph.add_edge(self, "render", colormap)
4✔
2633

2634

2635
    def range(
4✔
2636
            self,
2637
            start,
2638
            end,
2639
            color=None,
2640
            filename=None,
2641
            offset=None,
2642
            opacity=1.0,
2643
            style=None,
2644
            title=None,
2645
            width=10,
2646
        ):
2647

2648
        offset = self._default_offset(offset)
4✔
2649

2650
        table = toyplot.data.Table()
4✔
2651
        table["start"] = toyplot.require.scalar_vector(start)
4✔
2652
        table["end"] = toyplot.require.scalar_vector(
4✔
2653
            end, length=table.shape[0])
2654
        table["opacity"] = toyplot.broadcast.scalar(opacity, table.shape[0])
4✔
2655
        table["title"] = toyplot.broadcast.pyobject(title, table.shape[0])
4✔
2656
        style = toyplot.style.combine(
4✔
2657
            {"stroke": "none"},
2658
            toyplot.style.require(style, allowed=toyplot.style.allowed.fill),
2659
            )
2660

2661
        default_color = [next(self._range_colors)]
4✔
2662
        table["toyplot:fill"] = toyplot.color.broadcast(
4✔
2663
            colors=color,
2664
            shape=(table.shape[0], 1),
2665
            default=default_color,
2666
            )[:, 0]
2667

2668
        mark = self.add_mark(
4✔
2669
            toyplot.mark.Range(
2670
                coordinate_axes=["axis"],
2671
                coordinates=["start", "end"],
2672
                filename=filename,
2673
                fill=["toyplot:fill"],
2674
                opacity=["opacity"],
2675
                style=style,
2676
                table=table,
2677
                title=["title"],
2678
                ))
2679

2680
        self._child_offset[mark] = offset
4✔
2681
        self._child_width[mark] = width
4✔
2682

2683
        self._update_domain(numpy.column_stack((table["start"], table["end"])), display=True, data=True)
4✔
2684

2685
        return mark
4✔
2686

2687

2688
    def scatterplot(
4✔
2689
            self,
2690
            coordinates,
2691
            area=None,
2692
            color=None,
2693
            filename=None,
2694
            hyperlink=None,
2695
            marker="o",
2696
            mlstyle=None,
2697
            mstyle=None,
2698
            offset=None,
2699
            opacity=1.0,
2700
            size=None,
2701
            title=None,
2702
            ):
2703
        """Add a univariate plot to the axes.
2704

2705
        Parameters
2706
        ----------
2707
        coordinate: array-like one-dimensional coordinates
2708
        title: string, optional
2709
          Human-readable title for the mark.  The SVG / HTML backends render the
2710
          title as a tooltip.
2711
        style: dict, optional
2712
          Collection of CSS styles to apply across all datums.
2713

2714
        Returns
2715
        -------
2716
        plot: :class:`toyplot.mark.Plot`
2717
        """
2718
        coordinates = numpy.ma.array(coordinates).astype("float64")
4✔
2719
        if coordinates.ndim == 1:
4✔
2720
            coordinates = numpy.ma.column_stack((coordinates,))
4✔
2721
        elif coordinates.ndim == 2:
×
2722
            pass
2723

2724
        default_color = [next(self._scatterplot_colors) for i in range(coordinates.shape[1])]
4✔
2725
        mfill = toyplot.color.broadcast(
4✔
2726
            colors=color,
2727
            shape=coordinates.shape,
2728
            default=default_color,
2729
            )
2730
        marker = toyplot.broadcast.pyobject(marker, coordinates.shape)
4✔
2731

2732
        if area is None and size is None:
4✔
2733
            msize = toyplot.broadcast.scalar(4, coordinates.shape)
4✔
2734
        elif area is None and size is not None:
4✔
2735
            msize = toyplot.broadcast.scalar(size, coordinates.shape)
4✔
2736
        elif area is not None and size is None:
×
2737
            msize = numpy.sqrt(toyplot.broadcast.scalar(area, coordinates.shape))
×
2738
        else:
2739
            toyplot.log.warning("Size parameter overrides area.")
×
2740
            msize = toyplot.broadcast.scalar(size, coordinates.shape)
×
2741

2742
        mstroke = toyplot.color.broadcast(colors=mfill, shape=coordinates.shape)
4✔
2743
        mopacity = toyplot.broadcast.scalar(opacity, coordinates.shape)
4✔
2744
        mtitle = toyplot.broadcast.pyobject(title, coordinates.shape)
4✔
2745
        mhyperlink = toyplot.broadcast.pyobject(hyperlink, coordinates.shape)
4✔
2746
        mstyle = toyplot.style.require(mstyle, allowed=toyplot.style.allowed.marker)
4✔
2747
        mlstyle = toyplot.style.require(mlstyle, allowed=toyplot.style.allowed.text)
4✔
2748

2749
        self._update_domain(coordinates)
4✔
2750
        coordinate_axes = ["x"]
4✔
2751

2752
        table = toyplot.data.Table()
4✔
2753
        coordinate_keys = []
4✔
2754
        marker_keys = []
4✔
2755
        msize_keys = []
4✔
2756
        mfill_keys = []
4✔
2757
        mstroke_keys = []
4✔
2758
        mopacity_keys = []
4✔
2759
        mtitle_keys = []
4✔
2760
        mhyperlink_keys = []
4✔
2761
        for index, (coordinate_column, marker_column, msize_column, mfill_column, mstroke_column, mopacity_column, mtitle_column, mhyperlink_column) in enumerate(
4✔
2762
                zip(coordinates.T, marker.T, msize.T, mfill.T, mstroke.T, mopacity.T, mtitle.T, mhyperlink.T)):
2763
            coordinate_keys.append(coordinate_axes[0] + str(index))
4✔
2764
            marker_keys.append("marker" + str(index))
4✔
2765
            msize_keys.append("size" + str(index))
4✔
2766
            mfill_keys.append("fill" + str(index))
4✔
2767
            mstroke_keys.append("stroke" + str(index))
4✔
2768
            mopacity_keys.append("opacity" + str(index))
4✔
2769
            mtitle_keys.append("title" + str(index))
4✔
2770
            mhyperlink_keys.append("hyperlink" + str(index))
4✔
2771
            table[coordinate_keys[-1]] = coordinate_column
4✔
2772
            _mark_exportable(table, coordinate_keys[-1])
4✔
2773
            table[marker_keys[-1]] = marker_column
4✔
2774
            table[msize_keys[-1]] = msize_column
4✔
2775
            table[mfill_keys[-1]] = mfill_column
4✔
2776
            table[mstroke_keys[-1]] = mstroke_column
4✔
2777
            table[mopacity_keys[-1]] = mopacity_column
4✔
2778
            table[mtitle_keys[-1]] = mtitle_column
4✔
2779
            table[mhyperlink_keys[-1]] = mhyperlink_column
4✔
2780

2781
        offset = self._default_offset(offset)
4✔
2782

2783
        mark = toyplot.mark.Point(
4✔
2784
            coordinate_axes=coordinate_axes,
2785
            coordinates=coordinate_keys,
2786
            filename=filename,
2787
            marker=marker_keys,
2788
            mfill=mfill_keys,
2789
            mhyperlink=mhyperlink_keys,
2790
            mlstyle=mlstyle,
2791
            mopacity=mopacity_keys,
2792
            msize=msize_keys,
2793
            mstroke=mstroke_keys,
2794
            mstyle=mstyle,
2795
            mtitle=mtitle_keys,
2796
            table=table,
2797
            )
2798

2799
        self.add_mark(mark)
4✔
2800
        self._child_offset[mark] = offset
4✔
2801

2802
        return mark
4✔
2803

2804
    def _finalize(self):
4✔
2805
        if self._finalized is None:
4✔
2806
            # Begin with the implicit domain defined by our data.
2807
            domain_min = self.axis._display_min
4✔
2808
            domain_max = self.axis._display_max
4✔
2809

2810
            # If there is no implicit domain (we don't have any data), default
2811
            # to the origin.
2812
            if domain_min is None:
4✔
2813
                domain_min = 0
4✔
2814
            if domain_max is None:
4✔
2815
                domain_max = 0
4✔
2816

2817
            # Ensure that the domain is never empty.
2818
            if domain_min == domain_max:
4✔
2819
                domain_min -= 0.5
4✔
2820
                domain_max += 0.5
4✔
2821

2822
            # Allow users to override the domain.
2823
            if self.axis.domain.min is not None:
4✔
2824
                domain_min = self.axis.domain.min
4✔
2825
            if self.axis.domain.max is not None:
4✔
2826
                domain_max = self.axis.domain.max
4✔
2827

2828
            # Ensure that the domain is never empty.
2829
            if domain_min == domain_max:
4✔
2830
                domain_min -= 0.5
×
2831
                domain_max += 0.5
×
2832

2833
            # Calculate tick locations and labels.
2834
            tick_locations = []
4✔
2835
            tick_labels = []
4✔
2836
            tick_titles = []
4✔
2837
            if self.axis.show:
4✔
2838
                tick_locations, tick_labels, tick_titles = self.axis._locator().ticks(domain_min, domain_max)
4✔
2839

2840
            # Allow tick locations to grow (never shrink) the domain.
2841
            if len(tick_locations):
4✔
2842
                domain_min = numpy.amin((domain_min, tick_locations[0]))
4✔
2843
                domain_max = numpy.amax((domain_max, tick_locations[-1]))
4✔
2844

2845
            # Finalize the axis.
2846
            self.axis._finalize(
4✔
2847
                x1=self._x1,
2848
                x2=self._x2,
2849
                y1=self._y1,
2850
                y2=self._y2,
2851
                offset=self.padding,
2852
                domain_min=domain_min,
2853
                domain_max=domain_max,
2854
                tick_locations=tick_locations,
2855
                tick_labels=tick_labels,
2856
                tick_titles=tick_titles,
2857
                default_tick_location="below",
2858
                default_ticks_near=3,
2859
                default_ticks_far=3,
2860
                default_label_location="below",
2861
                #label_baseline_shift="-200%",
2862
                )
2863
            self._finalized = self
4✔
2864

2865
        return self._finalized
4✔
2866

2867

2868
##########################################################################
2869
# Table
2870

2871
class Table(object):
4✔
2872
    """Row and column-based table coordinate system.
2873

2874
    Do not create Table instances directly.  Use factory methods such
2875
    as :meth:`toyplot.canvas.Canvas.table` instead.
2876
    """
2877
    class Label(object):
4✔
2878
        """Controls the appearance and behavior of the table label."""
2879
        def __init__(self, text, style):
4✔
2880

2881
            self._style = {}
4✔
2882
            self._text = None
4✔
2883

2884
            self.text = text
4✔
2885

2886
            self.style = {
4✔
2887
                "font-weight": "bold",
2888
                }
2889
            self.style = style
4✔
2890

2891
        style = _create_text_style_property()
4✔
2892
        text = _create_text_property()
4✔
2893

2894

2895
    class CellMark(object):
4✔
2896
        """Abstract interface for objects that embed other Toyplot visualizations in table cells."""
2897
        def __init__(self, table, axes, series):
4✔
2898
            self._table = table
4✔
2899
            self._axes = axes
4✔
2900
            self._series = toyplot.require.value_in(series, ["columns", "rows"])
4✔
2901
            self._finalized = None
4✔
2902

2903
        def _finalize(self):
4✔
2904
            raise NotImplementedError() # pragma: no cover
2905

2906
    class CellBarMark(CellMark):
4✔
2907
        def __init__(
4✔
2908
                self,
2909
                table,
2910
                axes,
2911
                baseline,
2912
                color,
2913
                filename,
2914
                opacity,
2915
                padding,
2916
                series,
2917
                style,
2918
                title,
2919
                width,
2920
            ):
2921
            Table.CellMark.__init__(self, table, axes, series)
4✔
2922

2923
            self._baseline = baseline
4✔
2924
            self._color = color
4✔
2925
            self._filename = filename
4✔
2926
            self._opacity = opacity
4✔
2927
            self._padding = toyplot.units.convert(padding, "px", "px")
4✔
2928
            self._style = style
4✔
2929
            self._title = title
4✔
2930
            self._width = toyplot.require.scalar(width)
4✔
2931

2932
        def _finalize(self):
4✔
2933
            if self._finalized is None:
×
2934
                rows, columns = numpy.nonzero(self._table._cell_axes == self._axes)
×
2935
                row_min = rows.min()
×
2936
                row_max = rows.max()
×
2937
                column_min = columns.min()
×
2938
                column_max = columns.max()
×
2939

2940
                if self._series == "columns":
×
2941
                    shape = (row_max + 1 - row_min, column_max + 1 - column_min)
×
2942
                    cell_begin = self._table._cell_top
×
2943
                    cell_end = self._table._cell_bottom
×
2944
                    cell_indices = numpy.unique(rows)
×
2945
                    along = "y"
×
2946
                    along_axis = self._axes.y
×
2947
                    series = self._table._cell_data[self._table._cell_axes == self._axes].reshape(shape).astype("float64")
×
2948
                elif self._series == "rows":
×
2949
                    shape = (column_max + 1 - column_min, row_max + 1 - row_min)
×
2950
                    cell_begin = self._table._cell_left
×
2951
                    cell_end = self._table._cell_right
×
2952
                    cell_indices = numpy.unique(columns)
×
2953
                    along = "x"
×
2954
                    along_axis = self._axes.x
×
2955
                    series = self._table._cell_data[self._table._cell_axes == self._axes].reshape(shape).astype("float64")[:, ::-1]
×
2956

2957
                width = min(0.5 - numpy.finfo("float32").eps, 0.5 * self._width)
×
2958
                begin = numpy.arange(shape[0]) - width
×
2959
                end = numpy.arange(shape[0]) + width
×
2960

2961
                segments = []
×
2962
                for index, cell_index in enumerate(cell_indices):
×
2963
                    segments.append(toyplot.projection.Piecewise.Segment(
×
2964
                        "linear",
2965
                        index - 0.5,
2966
                        index - 0.5,
2967
                        index + 0.5,
2968
                        index + 0.5,
2969
                        cell_begin[cell_index] + self._padding,
2970
                        cell_begin[cell_index] + self._padding,
2971
                        cell_end[cell_index] - self._padding,
2972
                        cell_end[cell_index] - self._padding,
2973
                        ))
2974
                projection = toyplot.projection.Piecewise(segments)
×
2975
                along_axis._scale = projection
×
2976

2977
                color = self._color
×
2978
                if color == "datum":
×
2979
                    color = series
×
2980
                elif isinstance(color, tuple) and len(color) == 2 and color[0] == "datum":
×
2981
                    color = (series, color[1])
×
2982

2983
                self._finalized = self._axes.bars(
×
2984
                    begin,
2985
                    end,
2986
                    series,
2987
                    along=along,
2988
                    baseline=self._baseline,
2989
                    color=color,
2990
                    filename=self._filename,
2991
                    opacity=self._opacity,
2992
                    style=self._style,
2993
                    title=self._title,
2994
                    )
2995

2996
                self._axes._scenegraph.remove_edge(self._axes, "render", self._finalized)
×
2997

2998
            return self._finalized
×
2999

3000
    class CellPlotMark(CellMark):
4✔
3001
        def __init__(
4✔
3002
                self,
3003
                table,
3004
                axes,
3005
                area,
3006
                color,
3007
                filename,
3008
                marker,
3009
                mfill,
3010
                mlstyle,
3011
                mopacity,
3012
                mstyle,
3013
                mtitle,
3014
                opacity,
3015
                series,
3016
                size,
3017
                stroke_width,
3018
                style,
3019
                title,
3020
            ):
3021
            Table.CellMark.__init__(self, table, axes, series)
4✔
3022
            self._area = area
4✔
3023
            self._color = color
4✔
3024
            self._filename = filename
4✔
3025
            self._marker = marker
4✔
3026
            self._mfill = mfill
4✔
3027
            self._mlstyle = mlstyle
4✔
3028
            self._mopacity = mopacity
4✔
3029
            self._mstyle = mstyle
4✔
3030
            self._mtitle = mtitle
4✔
3031
            self._opacity = opacity
4✔
3032
            self._size = size
4✔
3033
            self._stroke_width = stroke_width
4✔
3034
            self._style = style
4✔
3035
            self._title = title
4✔
3036

3037
        def _finalize(self):
4✔
3038
            if self._finalized is None:
×
3039
                rows, columns = numpy.nonzero(self._table._cell_axes == self._axes)
×
3040
                row_min = rows.min()
×
3041
                row_max = rows.max()
×
3042
                column_min = columns.min()
×
3043
                column_max = columns.max()
×
3044

3045
                if self._series == "columns":
×
3046
                    shape = (row_max + 1 - row_min, column_max + 1 - column_min)
×
3047
                    cell_begin = self._table._cell_top
×
3048
                    cell_end = self._table._cell_bottom
×
3049
                    cell_indices = numpy.unique(rows)
×
3050
                    along = "y"
×
3051
                    along_axis = self._axes.y
×
3052
                    series = self._table._cell_data[self._table._cell_axes == self._axes].reshape(shape).astype("float64")
×
3053
                elif self._series == "rows":
×
3054
                    shape = (column_max + 1 - column_min, row_max + 1 - row_min)
×
3055
                    cell_begin = self._table._cell_left
×
3056
                    cell_end = self._table._cell_right
×
3057
                    cell_indices = numpy.unique(columns)
×
3058
                    along = "x"
×
3059
                    along_axis = self._axes.x
×
3060
                    series = self._table._cell_data[self._table._cell_axes == self._axes].reshape(shape).astype("float64")[:, ::-1]
×
3061

3062
                segments = []
×
3063
                for index, cell_index in enumerate(cell_indices):
×
3064
                    segments.append(toyplot.projection.Piecewise.Segment(
×
3065
                        "linear",
3066
                        index - 0.5,
3067
                        index - 0.5,
3068
                        index + 0.5,
3069
                        index + 0.5,
3070
                        cell_begin[cell_index],
3071
                        cell_begin[cell_index],
3072
                        cell_end[cell_index],
3073
                        cell_end[cell_index],
3074
                        ))
3075
                projection = toyplot.projection.Piecewise(segments)
×
3076
                along_axis._scale = projection
×
3077

3078
                color = self._color
×
3079
                if color == "datum":
×
3080
                    color = series
×
3081
                elif isinstance(color, tuple) and len(color) == 2 and color[0] == "datum":
×
3082
                    color = (series, color[1])
×
3083

3084
                mfill = self._mfill
×
3085
                if mfill == "datum":
×
3086
                    mfill = series
×
3087
                elif isinstance(mfill, tuple) and len(mfill) == 2 and mfill[0] == "datum":
×
3088
                    mfill = (series, mfill[1])
×
3089

3090
                self._finalized = self._axes.plot(
×
3091
                    series,
3092
                    along=along,
3093
                    area=self._area,
3094
                    color=color,
3095
                    filename=self._filename,
3096
                    marker=self._marker,
3097
                    mfill=mfill,
3098
                    mlstyle=self._mlstyle,
3099
                    mopacity=self._mopacity,
3100
                    mstyle=self._mstyle,
3101
                    mtitle=self._mtitle,
3102
                    opacity=self._opacity,
3103
                    size=self._size,
3104
                    stroke_width=self._stroke_width,
3105
                    style=self._style,
3106
                    title=self._title,
3107
                    )
3108

3109
                self._axes._scenegraph.remove_edge(self._axes, "render", self._finalized)
×
3110

3111
            return self._finalized
×
3112

3113
    class EmbeddedCartesian(Cartesian):
4✔
3114
        def __init__(self, table, *args, **kwargs):
4✔
3115
            toyplot.coordinates.Cartesian.__init__(self, *args, xmin_range=0, xmax_range=1, ymin_range=0, ymax_range=1, **kwargs)
4✔
3116
            self._table = table
4✔
3117

3118
        def cell_bars(
4✔
3119
                self,
3120
                baseline="stacked",
3121
                color=None,
3122
                filename=None,
3123
                opacity=1.0,
3124
                padding=0,
3125
                series="columns",
3126
                style=None,
3127
                title=None,
3128
                width=0.5,
3129
            ):
3130

3131
            return self.add_mark(
4✔
3132
                toyplot.coordinates.Table.CellBarMark(
3133
                    table=self._table,
3134
                    axes=self,
3135
                    baseline=baseline,
3136
                    color=color,
3137
                    filename=filename,
3138
                    opacity=opacity,
3139
                    padding=padding,
3140
                    series=series,
3141
                    style=style,
3142
                    title=title,
3143
                    width=width,
3144
                    ))
3145

3146

3147
        def cell_plot(
4✔
3148
                self,
3149
                area=None,
3150
                color=None,
3151
                filename=None,
3152
                marker=None,
3153
                mfill=None,
3154
                mlstyle=None,
3155
                mopacity=1.0,
3156
                mstyle=None,
3157
                mtitle=None,
3158
                opacity=1.0,
3159
                series="columns",
3160
                size=None,
3161
                stroke_width=2.0,
3162
                style=None,
3163
                title=None,
3164
            ):
3165

3166
            return self.add_mark(
4✔
3167
                toyplot.coordinates.Table.CellPlotMark(
3168
                    table=self._table,
3169
                    axes=self,
3170
                    area=area,
3171
                    color=color,
3172
                    filename=filename,
3173
                    marker=marker,
3174
                    mfill=mfill,
3175
                    mlstyle=mlstyle,
3176
                    mopacity=mopacity,
3177
                    mstyle=mstyle,
3178
                    mtitle=mtitle,
3179
                    opacity=opacity,
3180
                    series=series,
3181
                    size=size,
3182
                    stroke_width=stroke_width,
3183
                    style=style,
3184
                    title=title,
3185
                    ))
3186

3187

3188
    class CellReference(object):
4✔
3189
        def __init__(self, table, selection):
4✔
3190
            self._table = table
4✔
3191
            self._selection = selection
4✔
3192

3193
        def _set_align(self, value):
4✔
3194
            self._table._cell_align[self._selection] = value
4✔
3195
        align = property(fset=_set_align)
4✔
3196

3197
        def _set_angle(self, value):
4✔
3198
            self._table._cell_angle[self._selection] = value
4✔
3199
        angle = property(fset=_set_angle)
4✔
3200

3201
        def _set_data(self, value):
4✔
3202
            self._table._cell_data[self._selection] = numpy.array(value).flat
4✔
3203
        data = property(fset=_set_data)
4✔
3204

3205
        def _set_format(self, value):
4✔
3206
            self._table._cell_format[self._selection] = value
4✔
3207
        format = property(fset=_set_format)
4✔
3208

3209
        def _set_height(self, value):
4✔
3210
            row_indices, column_indices = self._table._selection_coordinates(self._selection)
4✔
3211
            self._table._row_heights[row_indices] = toyplot.units.convert(value, "px", "px")
4✔
3212
        height = property(fset=_set_height)
4✔
3213

3214
        def _set_lstyle(self, value):
4✔
3215
            value = toyplot.style.require(value, allowed=toyplot.style.allowed.text)
4✔
3216
            value = [toyplot.style.combine(style, value) for style in self._table._cell_lstyle[self._selection]]
4✔
3217
            self._table._cell_lstyle[self._selection] = value
4✔
3218
        lstyle = property(fset=_set_lstyle)
4✔
3219

3220
        def _set_show(self, value):
4✔
3221
            self._table._cell_show[self._selection] = True if value else False
×
3222
        show = property(fset=_set_show)
4✔
3223

3224
        def _set_style(self, value):
4✔
3225
            value = toyplot.style.require(value, allowed=toyplot.style.allowed.fill)
4✔
3226
            value = [toyplot.style.combine(style, value) for style in self._table._cell_style[self._selection]]
4✔
3227
            self._table._cell_style[self._selection] = value
4✔
3228
        style = property(fset=_set_style)
4✔
3229

3230
        def _set_title(self, value):
4✔
3231
            self._table._cell_title[self._selection] = str(value)
4✔
3232
        title = property(fset=_set_title)
4✔
3233

3234
        def _set_hyperlink(self, value):
4✔
3235
            self._table._cell_hyperlink[self._selection] = str(value)
×
3236
        hyperlink = property(fset=_set_hyperlink)
4✔
3237

3238
        def _set_width(self, value):
4✔
3239
            row_indices, column_indices = self._table._selection_coordinates(self._selection)
4✔
3240
            self._table._column_widths[column_indices] = toyplot.units.convert(value, "px", "px")
4✔
3241
        width = property(fset=_set_width)
4✔
3242

3243
        def cartesian(
4✔
3244
                self,
3245
                aspect=None,
3246
                hyperlink=None,
3247
                cell_padding=3,
3248
                label=None,
3249
                padding=3,
3250
                palette=None,
3251
                show=True,
3252
                xlabel=None,
3253
                xmax=None,
3254
                xmin=None,
3255
                xscale="linear",
3256
                xshow=False,
3257
                xticklocator=None,
3258
                ylabel=None,
3259
                ymax=None,
3260
                ymin=None,
3261
                yscale="linear",
3262
                yshow=False,
3263
                yticklocator=None,
3264
            ):
3265

3266
            axes = toyplot.coordinates.Table.EmbeddedCartesian(
4✔
3267
                aspect=aspect,
3268
                hyperlink=hyperlink,
3269
                label=label,
3270
                padding=padding,
3271
                palette=palette,
3272
                scenegraph=self._table._scenegraph,
3273
                show=show,
3274
                xaxis=None,
3275
                xlabel=xlabel,
3276
                xmax=xmax,
3277
                xmin=xmin,
3278
                xscale=xscale,
3279
                xshow=xshow,
3280
                xticklocator=xticklocator,
3281
                yaxis=None,
3282
                ylabel=ylabel,
3283
                ymax=ymax,
3284
                ymin=ymin,
3285
                yscale=yscale,
3286
                yshow=yshow,
3287
                yticklocator=yticklocator,
3288
                table=self._table,
3289
                )
3290

3291
            self._table._merge_cells(self._selection)
4✔
3292
            self._table._cell_format[self._selection] = toyplot.format.NullFormatter()
4✔
3293
            self._table._cell_axes[self._selection] = axes
4✔
3294
            self._table._axes.append(axes)
4✔
3295
            self._table._axes_padding.append(cell_padding)
4✔
3296

3297
            return axes
4✔
3298

3299
        def merge(self):
4✔
3300
            self._table._merge_cells(self._selection)
4✔
3301
            self._table._cell_data[self._selection] = self._table._cell_data[self._selection][0]
4✔
3302
            return self
4✔
3303

3304
    class ColumnCellReference(CellReference):
4✔
3305
        def __init__(self, table, selection):
4✔
3306
            Table.CellReference.__init__(self, table, selection)
4✔
3307

3308
        def delete(self):
4✔
3309
            row_indices, column_indices = self._table._selection_coordinates(self._selection)
4✔
3310
            self._table._delete_cells(column_indices, axis=1)
4✔
3311

3312
    class RowCellReference(CellReference):
4✔
3313
        def __init__(self, table, selection):
4✔
3314
            Table.CellReference.__init__(self, table, selection)
4✔
3315

3316
        def delete(self):
4✔
3317
            row_indices, column_indices = self._table._selection_coordinates(self._selection)
4✔
3318
            self._table._delete_cells(row_indices, axis=0)
4✔
3319

3320
    class DistanceArrayReference(object):
4✔
3321
        def __init__(self, array):
4✔
3322
            self._array = array
4✔
3323

3324
        def __getitem__(self, key):
4✔
3325
            return self._array[key]
×
3326

3327
        def __setitem__(self, key, value):
4✔
3328
            self._array[key] = toyplot.units.convert(value, "px", "px")
4✔
3329

3330
    class GapReference(object):
4✔
3331
        def __init__(self, row_gaps, column_gaps):
4✔
3332
            self._row_gaps = row_gaps
4✔
3333
            self._column_gaps = column_gaps
4✔
3334

3335
        @property
4✔
3336
        def rows(self):
4✔
3337
            return Table.DistanceArrayReference(self._row_gaps)
4✔
3338

3339
        @property
4✔
3340
        def columns(self):
4✔
3341
            return Table.DistanceArrayReference(self._column_gaps)
4✔
3342

3343
    class GridReference(object):
4✔
3344
        def __init__(self, table, hlines, vlines):
4✔
3345
            self._table = table
4✔
3346
            self._hlines = hlines
4✔
3347
            self._vlines = vlines
4✔
3348

3349
        @property
4✔
3350
        def hlines(self):
4✔
3351
            return self._hlines
4✔
3352

3353
        @property
4✔
3354
        def vlines(self):
4✔
3355
            return self._vlines
4✔
3356

3357
        @property
4✔
3358
        def separation(self):
4✔
3359
            return self._table._separation
×
3360

3361
        @separation.setter
4✔
3362
        def separation(self, value):
4✔
3363
            self._table._separation = value
4✔
3364

3365
        @property
4✔
3366
        def style(self):
4✔
3367
            return self._table._gstyle
×
3368

3369
        @style.setter
4✔
3370
        def style(self, value):
4✔
3371
            self._table._gstyle = toyplot.style.combine(
4✔
3372
                self._table._gstyle,
3373
                toyplot.style.require(value, toyplot.style.allowed.line),
3374
                )
3375

3376
    class Region(object):
4✔
3377
        class ColumnAccessor(object):
4✔
3378
            def __init__(self, region):
4✔
3379
                self._region = region
4✔
3380

3381
            def __getitem__(self, selection):
4✔
3382
                table, region = self._region._selection()
4✔
3383
                region[Ellipsis, selection] = True
4✔
3384
                return Table.ColumnCellReference(self._region._table, table)
4✔
3385

3386
            def insert(self, before=None, after=None):
4✔
3387
                if (before is None) + (after is None) != 1:
4✔
3388
                    raise ValueError("Specify either before or after.")
×
3389
                if before is not None:
4✔
3390
                    table, region = self._region._selection()
4✔
3391
                    region[Ellipsis, before] = True
4✔
3392
                    rows, columns = numpy.nonzero(table)
4✔
3393
                    before = numpy.unique(columns)
4✔
3394
                if after is not None:
4✔
3395
                    table, region = self._region._selection()
4✔
3396
                    region[Ellipsis, after] = True
4✔
3397
                    rows, columns = numpy.nonzero(table)
4✔
3398
                    after = numpy.unique(columns)
4✔
3399
                self._region._table._insert_cells(before=before, after=after, axis=1)
4✔
3400

3401
        class RowAccessor(object):
4✔
3402
            def __init__(self, region):
4✔
3403
                self._region = region
4✔
3404

3405
            def __getitem__(self, selection):
4✔
3406
                table, region = self._region._selection()
4✔
3407
                region[selection, Ellipsis] = True
4✔
3408
                return Table.RowCellReference(self._region._table, table)
4✔
3409

3410
            def insert(self, before=None, after=None):
4✔
3411
                if (before is None) + (after is None) != 1:
4✔
3412
                    raise ValueError("Specify either before or after.")
×
3413
                if before is not None:
4✔
3414
                    table, region = self._region._selection()
4✔
3415
                    region[before, Ellipsis] = True
4✔
3416
                    rows, columns = numpy.nonzero(table)
4✔
3417
                    before = numpy.unique(rows)
4✔
3418
                if after is not None:
4✔
3419
                    table, region = self._region._selection()
4✔
3420
                    region[after, Ellipsis] = True
4✔
3421
                    rows, columns = numpy.nonzero(table)
4✔
3422
                    after = numpy.unique(rows)
4✔
3423
                self._region._table._insert_cells(before=before, after=after, axis=0)
4✔
3424

3425
        class CellAccessor(object):
4✔
3426
            def __init__(self, region):
4✔
3427
                self._region = region
4✔
3428

3429
            def __getitem__(self, selection):
4✔
3430
                table, region = self._region._selection()
4✔
3431
                region[selection] = True
4✔
3432
                return Table.CellReference(self._region._table, table)
4✔
3433

3434
        def __init__(self, table, row_begin, row_end, column_begin, column_end):
4✔
3435
            self._table = table
4✔
3436
            self._row_begin = row_begin
4✔
3437
            self._row_end = row_end
4✔
3438
            self._column_begin = column_begin
4✔
3439
            self._column_end = column_end
4✔
3440

3441
        def _selection(self):
4✔
3442
            table = numpy.zeros(self._table._shape, dtype="bool")
4✔
3443
            region = table[self._row_begin:self._row_end, self._column_begin:self._column_end]
4✔
3444
            return table, region
4✔
3445

3446
        @property
4✔
3447
        def cell(self):
4✔
3448
            return Table.Region.CellAccessor(self)
4✔
3449

3450
        @property
4✔
3451
        def cells(self):
4✔
3452
            region_selection = numpy.zeros(self._table._shape, dtype="bool")
4✔
3453
            region_selection[self._row_begin:self._row_end, self._column_begin:self._column_end] = True
4✔
3454
            return Table.CellReference(self._table, region_selection)
4✔
3455

3456
        @property
4✔
3457
        def column(self):
4✔
3458
            return Table.Region.ColumnAccessor(self)
4✔
3459

3460
        @property
4✔
3461
        def gaps(self):
4✔
3462
            return Table.GapReference(self._table._row_gaps[self._row_begin:self._row_end-1], self._table._column_gaps[self._column_begin: self._column_end-1])
4✔
3463

3464
        @property
4✔
3465
        def grid(self):
4✔
3466
            return Table.GridReference(
4✔
3467
                self._table,
3468
                self._table._hlines[self._row_begin:self._row_end+1, self._column_begin:self._column_end],
3469
                self._table._vlines[self._row_begin:self._row_end, self._column_begin:self._column_end+1],
3470
                )
3471

3472
        @property
4✔
3473
        def row(self):
4✔
3474
            return Table.Region.RowAccessor(self)
4✔
3475

3476
        @property
4✔
3477
        def shape(self):
4✔
3478
            return (self._row_end - self._row_begin, self._column_end - self._column_begin)
×
3479

3480
    def __init__(
4✔
3481
            self,
3482
            xmin_range,
3483
            xmax_range,
3484
            ymin_range,
3485
            ymax_range,
3486
            rows,
3487
            columns,
3488
            trows,
3489
            brows,
3490
            lcolumns,
3491
            rcolumns,
3492
            label,
3493
            scenegraph,
3494
            annotation,
3495
            filename,
3496
        ):
3497
        self._finalized = None
4✔
3498

3499
        self._xmin_range = xmin_range
4✔
3500
        self._xmax_range = xmax_range
4✔
3501
        self._ymin_range = ymin_range
4✔
3502
        self._ymax_range = ymax_range
4✔
3503
        self._scenegraph = scenegraph
4✔
3504
        self._annotation = False
4✔
3505
        self.annotation = annotation
4✔
3506
        self._filename = toyplot.require.filename(filename)
4✔
3507

3508
        self._shape = (trows + rows + brows, lcolumns + columns + rcolumns)
4✔
3509

3510
        self._cell_align = numpy.empty(self._shape, dtype="object")
4✔
3511
        self._cell_angle = numpy.zeros(self._shape, dtype="float")
4✔
3512
        self._cell_axes = numpy.empty(self._shape, dtype="object")
4✔
3513
        self._cell_data = numpy.empty(self._shape, dtype="object")
4✔
3514
        self._cell_format = numpy.tile(toyplot.format.BasicFormatter(), self._shape)
4✔
3515
        self._cell_group = numpy.arange(self._shape[0] * self._shape[1]).reshape(self._shape)
4✔
3516
        self._cell_lstyle = numpy.empty(self._shape, dtype="object")
4✔
3517

3518
        self._cell_region = numpy.zeros(self._shape, dtype="int")
4✔
3519
        self._cell_region[:trows, :lcolumns] = 0
4✔
3520
        self._cell_region[:trows, lcolumns:lcolumns+columns] = 1
4✔
3521
        self._cell_region[:trows, lcolumns+columns:] = 2
4✔
3522
        self._cell_region[trows:trows+rows, :lcolumns] = 3
4✔
3523
        self._cell_region[trows:trows+rows, lcolumns:lcolumns+columns] = 4
4✔
3524
        self._cell_region[trows:trows+rows, lcolumns+columns:] = 5
4✔
3525
        self._cell_region[trows+rows:, :lcolumns] = 6
4✔
3526
        self._cell_region[trows+rows:, lcolumns:lcolumns+columns] = 7
4✔
3527
        self._cell_region[trows+rows:, lcolumns+columns:] = 8
4✔
3528

3529
        self._cell_show = numpy.ones(self._shape, dtype="bool")
4✔
3530
        self._cell_style = numpy.empty(self._shape, dtype="object")
4✔
3531
        self._cell_title = numpy.empty(self._shape, dtype="object")
4✔
3532
        self._cell_hyperlink = numpy.empty(self._shape, dtype="object")
4✔
3533

3534
        self._hlines = numpy.empty((self._shape[0] + 1, self._shape[1]), dtype="object")
4✔
3535
        self._hlines_show = numpy.ones((self._shape[0] + 1, self._shape[1]), dtype="bool")
4✔
3536
        self._vlines = numpy.empty((self._shape[0], self._shape[1] + 1), dtype="object")
4✔
3537
        self._vlines_show = numpy.ones((self._shape[0], self._shape[1] + 1), dtype="bool")
4✔
3538

3539
        self._row_heights = numpy.zeros(self._shape[0], dtype="float")
4✔
3540
        self._row_gaps = numpy.zeros(self._shape[0] - 1, dtype="float")
4✔
3541
        self._column_widths = numpy.zeros(self._shape[1], dtype="float")
4✔
3542
        self._column_gaps = numpy.zeros(self._shape[1] - 1, dtype="float")
4✔
3543

3544
        self._axes = []
4✔
3545
        self._axes_padding = []
4✔
3546

3547
        self._label = Table.Label(
4✔
3548
            label, style={"font-size": "14px", "baseline-shift": "100%"})
3549

3550
        self._separation = 2
4✔
3551
        self._gstyle = {"stroke": toyplot.color.black, "stroke-width": 0.5}
4✔
3552

3553
        lstyle = {
4✔
3554
            }
3555
        self._cell_lstyle[...] = lstyle
4✔
3556
        self._cell_align[...] = "center"
4✔
3557

3558
    @property
4✔
3559
    def annotation(self):
4✔
3560
        return self._annotation
4✔
3561

3562
    @annotation.setter
4✔
3563
    def annotation(self, value):
4✔
3564
        self._annotation = True if value else False
4✔
3565

3566
    def _region_bounds(self, region):
4✔
3567
        rows, columns = numpy.nonzero(self._cell_region == region)
4✔
3568
        if len(rows) and len(columns):
4✔
3569
            return (rows.min(), rows.max() + 1, columns.min(), columns.max() + 1)
4✔
3570
        return (0, 0, 0, 0)
4✔
3571

3572
    def _selection_coordinates(self, selection):
4✔
3573
        table_selection = numpy.zeros(self._shape, dtype="bool")
4✔
3574
        table_selection[selection] = True
4✔
3575
        return numpy.nonzero(table_selection)
4✔
3576

3577
    def _merge_cells(self, selection):
4✔
3578
        self._cell_group[selection] = numpy.unique(self._cell_group).max() + 1 # pylint: disable=no-member
4✔
3579

3580
        # TODO: Handle non-rectangular shapes here
3581
        row_indices, column_indices = self._selection_coordinates(selection)
4✔
3582
        if row_indices.max() - row_indices.min() > 0:
4✔
3583
            self._hlines_show[row_indices.min() + 1 : row_indices.max() + 1, column_indices.min() : column_indices.max() + 1] = False
4✔
3584
        if column_indices.max() - column_indices.min() > 0:
4✔
3585
            self._vlines_show[row_indices.min() : row_indices.max() + 1, column_indices.min() + 1 : column_indices.max() + 1] = False
4✔
3586

3587
    def _delete_cells(self, indices, axis):
4✔
3588
        indices = numpy.unique(indices)
4✔
3589

3590
        self._cell_align = numpy.delete(self._cell_align, indices, axis=axis)
4✔
3591
        self._cell_angle = numpy.delete(self._cell_angle, indices, axis=axis)
4✔
3592
        self._cell_axes = numpy.delete(self._cell_axes, indices, axis=axis)
4✔
3593
        self._cell_data = numpy.delete(self._cell_data, indices, axis=axis)
4✔
3594
        self._cell_format = numpy.delete(self._cell_format, indices, axis=axis)
4✔
3595
        self._cell_group = numpy.delete(self._cell_group, indices, axis=axis)
4✔
3596
        self._cell_lstyle = numpy.delete(self._cell_lstyle, indices, axis=axis)
4✔
3597
        self._cell_region = numpy.delete(self._cell_region, indices, axis=axis)
4✔
3598
        self._cell_show = numpy.delete(self._cell_show, indices, axis=axis)
4✔
3599
        self._cell_style = numpy.delete(self._cell_style, indices, axis=axis)
4✔
3600
        self._cell_title = numpy.delete(self._cell_title, indices, axis=axis)
4✔
3601
        self._cell_hyperlink = numpy.delete(self._cell_hyperlink, indices, axis=axis)
4✔
3602

3603
        self._hlines = numpy.delete(self._hlines, indices, axis=axis)
4✔
3604
        self._hlines_show = numpy.delete(self._hlines_show, indices, axis=axis)
4✔
3605

3606
        self._vlines = numpy.delete(self._vlines, indices, axis=axis)
4✔
3607
        self._vlines_show = numpy.delete(self._vlines_show, indices, axis=axis)
4✔
3608

3609
        if axis == 0:
4✔
3610
            self._row_heights = numpy.delete(self._row_heights, indices)
4✔
3611
        if axis == 1:
4✔
3612
            self._column_widths = numpy.delete(self._column_widths, indices)
4✔
3613

3614
        # TODO: handle this better
3615
        if axis == 0:
4✔
3616
            self._row_gaps = self._row_gaps[len(indices):]
4✔
3617
        if axis == 1:
4✔
3618
            self._column_gaps = self._column_gaps[len(indices):]
4✔
3619

3620
        self._shape = self._cell_align.shape
4✔
3621

3622
    def _insert_cells(self, before, after, axis):
4✔
3623
        # Create a selection for the source row / column.
3624
        source = numpy.zeros(self.shape[axis], dtype="bool")
4✔
3625
        if before is not None:
4✔
3626
            source[before] = True
4✔
3627
        if after is not None:
4✔
3628
            source[after] = True
4✔
3629
        source = numpy.flatnonzero(source)
4✔
3630
        if axis == 0:
4✔
3631
            source = (source, Ellipsis)
4✔
3632
        if axis == 1:
4✔
3633
            source = (Ellipsis, source)
4✔
3634

3635
        # Numpy always inserts *before* a given row / column, so convert everything into strictly "before" indices.
3636
        position = numpy.zeros(self.shape[axis] + 1, dtype="bool")
4✔
3637
        if before is not None:
4✔
3638
            position[before] = True
4✔
3639
        if after is not None:
4✔
3640
            position[1:][after] = True
4✔
3641
        position = numpy.flatnonzero(position)
4✔
3642

3643
        self._cell_align = numpy.insert(self._cell_align, position, None, axis=axis)
4✔
3644
        self._cell_angle = numpy.insert(self._cell_angle, position, 0, axis=axis)
4✔
3645
        self._cell_axes = numpy.insert(self._cell_axes, position, None, axis=axis)
4✔
3646
        self._cell_data = numpy.insert(self._cell_data, position, None, axis=axis)
4✔
3647
        self._cell_format = numpy.insert(self._cell_format, position, toyplot.format.BasicFormatter(), axis=axis)
4✔
3648
        self._cell_group = numpy.insert(self._cell_group, position, -1, axis=axis)
4✔
3649
        self._cell_group[self._cell_group == -1] = numpy.unique(self._cell_group).max() + 1 + numpy.arange(numpy.count_nonzero(self._cell_group == -1)) # pylint: disable=no-member
4✔
3650
        self._cell_lstyle = numpy.insert(self._cell_lstyle, position, None, axis=axis)
4✔
3651
        self._cell_region = numpy.insert(self._cell_region, position, self._cell_region[source], axis=axis)
4✔
3652
        self._cell_show = numpy.insert(self._cell_show, position, True, axis=axis)
4✔
3653
        self._cell_style = numpy.insert(self._cell_style, position, self._cell_style[source], axis=axis)
4✔
3654
        self._cell_title = numpy.insert(self._cell_title, position, None, axis=axis)
4✔
3655
        self._cell_hyperlink = numpy.insert(self._cell_hyperlink, position, None, axis=axis)
4✔
3656

3657
        self._hlines = numpy.insert(self._hlines, position, self._hlines[source], axis=axis)
4✔
3658
        self._hlines_show = numpy.insert(self._hlines_show, position, True, axis=axis)
4✔
3659

3660
        self._vlines = numpy.insert(self._vlines, position, self._vlines[source], axis=axis)
4✔
3661
        self._vlines_show = numpy.insert(self._vlines_show, position, True, axis=axis)
4✔
3662

3663
        if axis == 0:
4✔
3664
            self._row_heights = numpy.insert(self._row_heights, position, 0)
4✔
3665
        if axis == 1:
4✔
3666
            self._column_widths = numpy.insert(self._column_widths, position, 0)
4✔
3667

3668
        # TODO: handle this better
3669
        if axis == 0:
4✔
3670
            self._row_gaps = numpy.concatenate((self._row_gaps, numpy.zeros(len(position))))
4✔
3671
        if axis == 1:
4✔
3672
            self._column_gaps = numpy.concatenate((self._column_gaps, numpy.zeros(len(position))))
4✔
3673

3674
        self._shape = self._cell_align.shape
4✔
3675

3676
    @property
4✔
3677
    def body(self):
4✔
3678
        region = Table.Region(self, *self._region_bounds(4))
4✔
3679
        return region
4✔
3680

3681
    @property
4✔
3682
    def bottom(self):
4✔
3683
        region = Table.Region(self, *self._region_bounds(7))
4✔
3684
        region.left = Table.Region(self, *self._region_bounds(6))
4✔
3685
        region.right = Table.Region(self, *self._region_bounds(8))
4✔
3686
        return region
4✔
3687

3688
    @property
4✔
3689
    def cells(self):
4✔
3690
        return Table.Region(self, 0, self.shape[0], 0, self.shape[1])
4✔
3691

3692
    @property
4✔
3693
    def label(self):
4✔
3694
        return self._label
4✔
3695

3696
    @property
4✔
3697
    def left(self):
4✔
3698
        region = Table.Region(self, *self._region_bounds(3))
4✔
3699
        return region
4✔
3700

3701
    @property
4✔
3702
    def right(self):
4✔
3703
        region = Table.Region(self, *self._region_bounds(5))
4✔
3704
        return region
4✔
3705

3706
    @property
4✔
3707
    def shape(self):
4✔
3708
        return self._shape
4✔
3709

3710
    @property
4✔
3711
    def top(self):
4✔
3712
        region = Table.Region(self, *self._region_bounds(1))
4✔
3713
        region.left = Table.Region(self, *self._region_bounds(0))
4✔
3714
        region.right = Table.Region(self, *self._region_bounds(2))
4✔
3715
        return region
4✔
3716

3717
    def _finalize(self):
4✔
3718
        if self._finalized is None:
4✔
3719
            # Collect explicit row heights, column widths, and gaps.
3720
            row_heights = numpy.zeros(len(self._row_heights) + len(self._row_gaps))
4✔
3721
            row_heights[0::2] = self._row_heights
4✔
3722
            row_heights[1::2] = self._row_gaps
4✔
3723

3724
            column_widths = numpy.zeros(len(self._column_widths) + len(self._column_gaps))
4✔
3725
            column_widths[0::2] = self._column_widths
4✔
3726
            column_widths[1::2] = self._column_gaps
4✔
3727

3728
            # Compute implicit heights and widths for the remaining rows and columns.
3729
            table_height = self._ymax_range - self._ymin_range
4✔
3730
            available_height = table_height - numpy.sum(row_heights)
4✔
3731
            default_height = available_height / numpy.count_nonzero(row_heights[0::2] == 0)
4✔
3732
            row_heights[0::2][row_heights[0::2] == 0] = default_height
4✔
3733

3734
            table_width = self._xmax_range - self._xmin_range
4✔
3735
            available_width = table_width - numpy.sum(column_widths)
4✔
3736
            default_width = available_width / numpy.count_nonzero(column_widths[0::2] == 0)
4✔
3737
            column_widths[0::2][column_widths[0::2] == 0] = default_width
4✔
3738

3739
            row_boundaries = self._ymin_range + numpy.cumsum(numpy.concatenate(([0], row_heights)))
4✔
3740
            column_boundaries = self._xmin_range + numpy.cumsum(numpy.concatenate(([0], column_widths)))
4✔
3741

3742
            # Compute cell boundaries.
3743
            self._cell_top = row_boundaries[0::2]
4✔
3744
            self._cell_bottom = row_boundaries[1::2]
4✔
3745
            self._cell_left = column_boundaries[0::2]
4✔
3746
            self._cell_right = column_boundaries[1::2]
4✔
3747

3748
            # Compute grid boundaries.
3749
            self._row_boundaries = numpy.concatenate((
4✔
3750
                row_boundaries[0:1],
3751
                (row_boundaries[1:-1:2] + row_boundaries[2:-1:2]) / 2,
3752
                row_boundaries[-1:],
3753
                ))
3754
            self._column_boundaries = numpy.concatenate((
4✔
3755
                column_boundaries[0:1],
3756
                (column_boundaries[1:-1:2] + column_boundaries[2:-1:2]) / 2,
3757
                column_boundaries[-1:],
3758
                ))
3759

3760
            # Assign ranges and finalize embedded coordinate systems.
3761
            for axes, padding in zip(self._axes, self._axes_padding):
4✔
3762
                axes_rows, axes_columns = numpy.nonzero(self._cell_axes == axes)
4✔
3763
                row_min = axes_rows.min()
4✔
3764
                row_max = axes_rows.max()
4✔
3765
                column_min = axes_columns.min()
4✔
3766
                column_max = axes_columns.max()
4✔
3767

3768
                if isinstance(axes, toyplot.coordinates.Cartesian):
4✔
3769
                    axes.xmin_range = self._cell_left[column_min] + padding
4✔
3770
                    axes.xmax_range = self._cell_right[column_max] - padding
4✔
3771
                    axes.ymin_range = self._cell_top[row_min] + padding
4✔
3772
                    axes.ymax_range = self._cell_bottom[row_max] - padding
4✔
3773
                else:
3774
                    raise NotImplementedError("Unknown coordinate system: %s" % axes) # pragma: no cover
3775

3776
            self._finalized = self
4✔
3777
        return self._finalized
4✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc