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

sandialabs / toyplot / 9912662049

12 Jul 2024 06:45PM UTC coverage: 94.497% (-0.1%) from 94.629%
9912662049

Pull #214

github

web-flow
Merge cce091bb5 into 93ab5d60c
Pull Request #214: add as_float, change repr(x) -> str(x)

70 of 87 new or added lines in 10 files covered. (80.46%)

31 existing lines in 2 files now uncovered.

5409 of 5724 relevant lines covered (94.5%)

8.5 hits per line

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

92.2
/toyplot/html.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
"""Functions to render the canonical HTML representation of a Toyplot figure."""
6

7
# pylint: disable=function-redefined
8

9

10
import base64
9✔
11
import collections
9✔
12
import copy
9✔
13
import functools
9✔
14
import itertools
9✔
15
import json
9✔
16
import logging
9✔
17
import string
9✔
18
import uuid
9✔
19
import xml.etree.ElementTree as xml
9✔
20

21
from multipledispatch import dispatch
9✔
22
import numpy
9✔
23

24
import toyplot.bitmap
9✔
25
import toyplot.coordinates
9✔
26
import toyplot.canvas
9✔
27
import toyplot.color
9✔
28
import toyplot.font
9✔
29
import toyplot.mark
9✔
30
import toyplot.marker
9✔
31
import toyplot.text
9✔
32
from toyplot.require import as_float
9✔
33

34
log = logging.getLogger(__name__)
9✔
35

36

37
_namespace = dict()
9✔
38

39
#: Decorator for registering custom rendering code.
40
#:
41
#: This is only of use when creating your own custom Toyplot marks.  It is
42
#: not intended for end-users.
43
#:
44
#: Example
45
#: -------
46
#: To register your own rendering function::
47
#:
48
#:     @toyplot.html.dispatch(toyplot.coordinates.Cartesian, MyCustomMark, toyplot.html.RenderContext)
49
#:     def _render(axes, mark, context):
50
#:         # Rendering implementation here
51
dispatch = functools.partial(dispatch, namespace=_namespace)
9✔
52

53

54
class _CustomJSONEncoder(json.JSONEncoder):
9✔
55
    # pylint: disable=method-hidden
56
    def default(self, o): # pragma: no cover
57
        if isinstance(o, numpy.generic):
58
            return o.item()
59
        if isinstance(o, xml.Element):
60
            return xml.tostring(o, encoding="unicode", method="html")
61
        return json.JSONEncoder.default(self, o)
62

63

64
class RenderContext(object):
9✔
65
    """Stores context data during rendering.
66

67
    This is only of use for Toyplot developers and library developers who are
68
    implementing rendering code.  It is not intended for end-users.
69
    """
70
    def __init__(self, scenegraph, root):
9✔
71
        self._animation = {}
9✔
72
        self._id_cache = {}
9✔
73
        self._javascript_modules = {}
9✔
74
        self._javascript_calls = []
9✔
75
        self._parent = None
9✔
76
        self._rendered = set()
9✔
77
        self._root = root
9✔
78
        self._scenegraph = scenegraph
9✔
79

80
    def already_rendered(self, o):
9✔
81
        """Track whether an object has already been rendered.
82

83
        Used to prevent objects that can be shared, such as
84
        :class:`toyplot.coordinates.Axis`, from generating duplicate markup in
85
        the output HTML.
86

87
        Parameters
88
        ----------
89
        o: any Python object, required
90

91
        Returns
92
        -------
93
        rendered: bool
94
            If the given object hasn't already been rendered, records it as
95
            rendered and returns `False`.  Subsequent calls with the given
96
            object will always return `True`.
97

98
        Examples
99
        --------
100

101
        The following checks to see if `mark` has already been rendered::
102

103
                if not context.already_rendered(mark):
104
                    # Render the mark
105
        """
106
        if o in self._rendered:
9✔
107
            return True
9✔
108
        self._rendered.add(o)
9✔
109
        return False
9✔
110

111
    def get_id(self, o):
9✔
112
        """Return a globally unique identifier for an object.
113

114
        The generated identifier is cached, so multiple lookups on the same
115
        object will return consistent results.
116

117
        Parameters
118
        ----------
119
        o: any Python object, required.
120

121
        Returns
122
        -------
123
        id: str
124
            Globally unique identifier that can be used in HTML markup as an identifier
125
            that can be targeted from Javascript code.
126
        """
127
        python_id = id(o)
9✔
128
        if python_id not in self._id_cache:
9✔
129
            self._id_cache[python_id] = "t" + uuid.uuid4().hex
9✔
130
        return self._id_cache[python_id]
9✔
131

132
    def copy(self, parent):
9✔
133
        """Copy the current :class:`toyplot.html.RenderContext`.
134

135
        Creates a copy of the current render context that can be used to render
136
        children of the currently-rendered object.
137

138
        Parameters
139
        ----------
140
        parent: any Python object, required.
141

142
        Returns
143
        -------
144
        context: :class:`toyplot.html.RenderContext`
145
            The `parent` attribute will be set to the supplied parent object.
146
        """
147
        result = copy.copy(self)
9✔
148
        result._parent = parent
9✔
149
        return result
9✔
150

151
    def define(self, name, dependencies=None, factory=None, value=None):
9✔
152
        """Define a Javascript module that can be embedded in the output markup.
153

154
        The module will only be embedded in the output if it is listed as a
155
        dependency of another module, or code specified using :meth:`require`.
156

157
        You must specify either `factory` or `value`.
158

159
        Parameters
160
        ----------
161
        name: string, required
162
            Module name.  Any string is valid, but alphanumerics separated with
163
            slashes are recommended. Multiple calls to `define` with the same
164
            name argument will be silently ignored.
165
        dependencies: sequence of strings, optional
166
            Names of modules that are dependencies of this module.
167
        factory: string, optional
168
            Javascript code that will construct the module, which must be a
169
            function that takes the modules listed in `dependencies`, in-order,
170
            as arguments, and returns the initialized module.
171
        value: Python object, optional
172
            Arbitrary value for this module, which must be compatible with
173
            :func:`json.dumps`.
174
        """
175
        if name in self._javascript_modules:
9✔
176
            return
9✔
177

178
        if dependencies is None:
9✔
179
            dependencies = []
9✔
180

181
        if factory is None and value is None:
9✔
182
            raise ValueError("You must specify either factory or value.") #pragma: no cover
183
        if factory is not None and value is not None:
9✔
184
            raise ValueError("You must specify either factory or value.") #pragma: no cover
185
        if value is not None and dependencies:
9✔
186
            raise ValueError("Dependencies can only be specified when defining a factory, not a value.") #pragma: no cover
187

188
        self._javascript_modules[name] = (dependencies, factory, value)
9✔
189

190
    def require(self, dependencies=None, arguments=None, code=None):
9✔
191
        """Embed Javascript code and its dependencies into the output markup.
192

193
        The given code will be unconditionally embedded in the output markup,
194
        along with any modules listed as dependencies (plus their dependencies,
195
        and-so-on).
196

197
        Parameters
198
        ----------
199
        dependencies: sequence of strings, optional
200
            Names of modules that are required by this code.
201
        arguments: sequence of Python objects, optional
202
            Additional arguments to be passed to the Javascript code, which
203
            must be compatible with :func:`json.dumps`.
204
        code: string, required
205
            Javascript code to be embedded, which must be a function that
206
            accepts the modules listed in `requirements` in-order, followed by
207
            the values listed in `arguments` in-order, as arguments.
208
        """
209
        if dependencies is None:
9✔
210
            dependencies = []
×
211
        if arguments is None:
9✔
212
            arguments = []
×
213
        if code is None:
9✔
214
            raise ValueError("You must specify a Javascript function using the code argument.") #pragma: no cover
215
        self._javascript_calls.append((dependencies, arguments, code))
9✔
216

217
    @property
9✔
218
    def animation(self):
9✔
219
        return self._animation
9✔
220

221
    @property
9✔
222
    def parent(self):
9✔
223
        """Current DOM node.  Typical rendering code will append HTML content to this node."""
224
        return self._parent
9✔
225

226
    @property
9✔
227
    def root(self):
9✔
228
        """Top-level DOM node."""
229
        return self._root
×
230

231
    @property
9✔
232
    def scenegraph(self):
9✔
233
        return self._scenegraph
9✔
234

235

236
def apply_changes(html, changes):
9✔
237
    for change, states in changes.items():
×
238
        if change == "set-mark-style":
×
239
            for state in states:
×
240
                mark = html.find(".//*[@id='%s']" % state["mark"])
×
241
                style = toyplot.style.combine(dict([declaration.split(":") for declaration in mark.get("style").split(";") if declaration != ""]), state["style"])
×
242
                mark.set("style", _css_style(style))
×
243
        elif change == "set-datum-style":
×
244
            for state in states:
×
245
                mark_xml = html.find(".//*[@id='%s']" % state["mark"])
×
246
                series_xml = mark_xml.findall("*[@class='toyplot-Series']")[state["series"]]
×
247
                datum_xml = series_xml.findall("*[@class='toyplot-Datum']")[state["datum"]]
×
248
                style = toyplot.style.combine(dict([declaration.split(
×
249
                    ":") for declaration in datum_xml.get("style").split(";") if declaration != ""]), state["style"])
250
                datum_xml.set("style", _css_style(style))
×
251
        elif change == "set-datum-text":
×
252
            for state in states:
×
253
                mark_xml = html.find(".//*[@id='%s']" % state["mark"])
×
254
                series_xml = mark_xml.findall("*[@class='toyplot-Series']")[state["series"]]
×
255
                datum_xml = series_xml.findall("*[@class='toyplot-Datum']")[state["datum"]]
×
256

257
                # Remove old markup from the datum.
258
                while len(datum_xml):
×
259
                    del datum_xml[0]
×
260

261
                # Copy new markup to the datum.
262
                for child in state["layout"]:
×
263
                    datum_xml.append(child)
×
264

265

266
def render(canvas, fobj=None, animation=False, style=None):
9✔
267
    """Convert a canvas to its HTML DOM representation.
268

269
    Generates HTML markup with an embedded SVG representation of the canvas, plus
270
    JavaScript code for interactivity.  If the canvas contains animation, the
271
    markup will include an HTML user interface to control playback.
272

273
    Parameters
274
    ----------
275
    canvas: :class:`toyplot.canvas.Canvas`
276
      The canvas to be rendered.
277

278
    fobj: file-like object or string, optional
279
      The file to write.  Use a string filepath to write data directly to disk.
280
      If `None` (the default), the HTML tree will be returned to the caller
281
      instead.
282

283
    animation: boolean, optional
284
      If `True`, return a representation of the changes to be made to the HTML
285
      tree for animation.
286

287
    style: dict, optional
288
      Dictionary of CSS styles that will be applied to the top-level output <div>.
289

290
    Returns
291
    -------
292
    html: :class:`xml.etree.ElementTree.Element` or `None`
293
      HTML representation of `canvas`, as a DOM tree, or `None` if the caller
294
      specifies the `fobj` parameter.
295

296
    changes: JSON-compatible data structure, or `None`
297
      JSON-compatible representation of the animated changes to `canvas`.
298

299
    Notes
300
    -----
301
    The output HTML is the "canonical" representation of a Toyplot canvas - the
302
    other toyplot backends operate by converting the output from
303
    toyplot.html.render() to the desired end target.
304

305
    Note that the output HTML is a fragment wrapped in a <div>, suitable for
306
    embedding in a larger document.  It is the caller's responsibility to
307
    supply the <html>, <body> etc. if the result is intended as a standalone
308
    HTML document.
309
    """
310
    canvas = toyplot.require.instance(canvas, toyplot.canvas.Canvas)
9✔
311
    canvas.autorender(False)
9✔
312

313
    # Create the top-level HTML element.
314
    root_xml = xml.Element(
9✔
315
        "div",
316
        attrib={"class": "toyplot"},
317
        id="t" + uuid.uuid4().hex,
318
        )
319
    if style is not None:
9✔
320
        root_xml.set("style", toyplot.style.to_css(style))
9✔
321

322
    # Setup a render context
323
    context = RenderContext(scenegraph=canvas._scenegraph, root=root_xml)
9✔
324

325
    # Register a Javascript module to keep track of the root id
326
    context.define("toyplot/root/id", value=root_xml.get("id"))
9✔
327
    # Register a Javascript module to keep track of the root
328
    context.define("toyplot/root", ["toyplot/root/id"], factory="""function(root_id)
9✔
329
    {
330
        return document.querySelector("#" + root_id);
331
    }""")
332

333
    # Render the canvas.
334
    _render(canvas, context.copy(parent=root_xml)) # pylint: disable=no-value-for-parameter
9✔
335

336
    # Return / write the results.
337
    if isinstance(fobj, str):
9✔
338
        with open(fobj, "wb") as stream:
9✔
339
            stream.write(xml.tostring(root_xml, method="html"))
9✔
340
    elif fobj is not None:
9✔
341
        fobj.write(xml.tostring(root_xml, method="html"))
9✔
342
    else:
343
        if animation:
9✔
344
            return root_xml, context.animation
9✔
345
        return root_xml
9✔
346

347

348
def tostring(canvas, style=None):
9✔
349
    """Convert a canvas to its HTML string representation.
350

351
    Generates HTML markup with an embedded SVG representation of the canvas, plus
352
    JavaScript code for interactivity.  If the canvas contains animation, the
353
    markup will include an HTML user interface to control playback.
354

355
    Parameters
356
    ----------
357
    canvas: :class:`toyplot.canvas.Canvas`
358
      The canvas to be rendered.
359

360
    style: dict, optional
361
      Dictionary of CSS styles that will be applied to the top-level output <div>.
362

363
    Returns
364
    -------
365
    html: str
366
      HTML representation of `canvas` as a string.
367

368
    Notes
369
    -----
370
    The output HTML is a fragment wrapped in a <div>, suitable for embedding in
371
    a larger document.  It is the caller's responsibility to supply the <html>,
372
    <body> etc. if the result is intended as a standalone HTML document.
373
    """
374
    return xml.tostring(render(canvas=canvas, style=style), encoding="unicode", method="html")
9✔
375

376

377
def _color_fixup(styles):
9✔
378
    """It turns-out that many applications and libraries (Inkscape, Adobe Illustrator, Qt)
379
    don't handle CSS rgba() colors correctly.  So convert them to CSS rgb colors and use
380
    fill-opacity / stroke-opacity instead."""
381

382
    if "fill" in styles:
9✔
383
        color = toyplot.color.css(styles["fill"])
9✔
384
        if color is not None:
9✔
385
            opacity = as_float(styles.get("fill-opacity", 1.0))
9✔
386
            styles["fill"] = "rgb(%.3g%%,%.3g%%,%.3g%%)" % (
9✔
387
                color["r"] * 100, color["g"] * 100, color["b"] * 100)
388
            styles["fill-opacity"] = str(color["a"] * opacity)
9✔
389
    if "stroke" in styles:
9✔
390
        color = toyplot.color.css(styles["stroke"])
9✔
391
        if color is not None:
9✔
392
            opacity = as_float(styles.get("stroke-opacity", 1.0))
9✔
393
            styles["stroke"] = "rgb(%.3g%%,%.3g%%,%.3g%%)" % (
9✔
394
                color["r"] * 100, color["g"] * 100, color["b"] * 100)
395
            styles["stroke-opacity"] = str(color["a"] * opacity)
9✔
396

397
    return styles
9✔
398

399
def _css_style(*styles):
9✔
400
    style = _color_fixup(toyplot.style.combine(*styles))
9✔
401
    return ";".join(["%s:%s" % (key, value)
9✔
402
                     for key, value in sorted(style.items())])
403

404

405
def _css_attrib(*styles):
9✔
406
    style = _color_fixup(toyplot.style.combine(*styles))
9✔
407
    attrib = {}
9✔
408
    if style:
9✔
409
        attrib["style"] = ";".join(
9✔
410
            ["%s:%s" % (key, value) for key, value in sorted(style.items())])
411
    return attrib
9✔
412

413

414
def _flat_contiguous(a):
9✔
415
    i = 0
9✔
416
    result = []
9✔
417
    for (k, g) in itertools.groupby(a.ravel()):
9✔
418
        n = len(list(g))
9✔
419
        if k:
9✔
420
            result.append(slice(i, i + n))
9✔
421
        i += n
9✔
422
    return result
9✔
423

424

425
def _walk_tree(node):
9✔
426
    yield ("start", node.tag, node.attrib)
×
427
    if node.text:
×
428
        yield ("text", node.text)
×
429
    for child in node:
×
430
        for item in _walk_tree(child):
×
431
            yield item
×
432
    yield ("end", node.tag)
×
433
    if node.tail:
×
434
        yield ("text", node.tail)
×
435

436
def _draw_text(
9✔
437
        root,
438
        text,
439
        x=0,
440
        y=0,
441
        style=None,
442
        angle=None,
443
        title=None,
444
        attributes=None,
445
    ):
446

447
    if not text:
9✔
448
        return
9✔
449

450
    style = toyplot.style.combine({"font-family": "helvetica"}, style)
9✔
451

452
    if attributes is None:
9✔
453
        attributes = {}
9✔
454

455
    fonts = toyplot.font.ReportlabLibrary()
9✔
456
    layout = text if isinstance(text, toyplot.text.Layout) else toyplot.text.layout(text, style, fonts)
9✔
457

458
    transform = ""
9✔
459
    if x or y:
9✔
460
        transform += "translate(%s,%s)" % (x, y)
9✔
461
    if angle:
9✔
462
        transform += "rotate(%s)" % (-angle) # pylint: disable=invalid-unary-operand-type
9✔
463

464
    group = xml.SubElement(
9✔
465
        root,
466
        "g",
467
        attrib=attributes,
468
        )
469
    if transform:
9✔
470
        group.set("transform", transform)
9✔
471

472
    if title is not None:
9✔
473
        xml.SubElement(group, "title").text = str(title)
9✔
474

475
    layout_opacity = 0.5
9✔
476
    layout_stroke_width = 1
9✔
477

478
    if layout.style.get("-toyplot-text-layout-visibility", None) == "visible": # pragma: no cover
479
        xml.SubElement(
480
            group,
481
            "rect",
482
            x=str(layout.left),
483
            y=str(layout.top),
484
            width=str(layout.width),
485
            height=str(layout.height),
486
            stroke="red",
487
            fill="none",
488
            opacity=str(layout_opacity),
489
            attrib={"stroke-width": str(layout_stroke_width)},
490
            )
491
        xml.SubElement(
492
            group,
493
            "circle",
494
            x="0",
495
            y="0",
496
            r="1.5",
497
            stroke="red",
498
            fill="none",
499
            opacity=str(layout_opacity),
500
            attrib={"stroke-width": str(layout_stroke_width)},
501
            )
502

503
    hyperlink = []
9✔
504
    for line in layout.children:
9✔
505
        if line.style.get("-toyplot-text-layout-line-visibility", None) == "visible": # pragma: no cover
506
            xml.SubElement(
507
                group,
508
                "rect",
509
                x=str(line.left),
510
                y=str(line.top),
511
                width=str(line.width),
512
                height=str(line.height),
513
                stroke="green",
514
                fill="none",
515
                opacity=str(layout_opacity),
516
                attrib={"stroke-width": str(layout_stroke_width)},
517
                )
518
            xml.SubElement(
519
                group,
520
                "line",
521
                x1=str(line.left),
522
                y1=str(line.baseline),
523
                x2=str(line.right),
524
                y2=str(line.baseline),
525
                stroke="green",
526
                fill="none",
527
                opacity=str(layout_opacity),
528
                attrib={"stroke-width": str(layout_stroke_width)},
529
                )
530
        for box in line.children:
9✔
531
            if isinstance(box, toyplot.text.TextBox):
9✔
532
                xml.SubElement(
9✔
533
                    group,
534
                    "text",
535
                    x=str(box.left),
536
                    y=str(box.baseline),
537
                    style=toyplot.style.to_css(box.style),
538
                    ).text = box.text
539
                if box.style.get("-toyplot-text-layout-box-visibility", None) == "visible": # pragma: no cover
540
                    xml.SubElement(
541
                        group,
542
                        "rect",
543
                        x=str(box.left),
544
                        y=str(box.top),
545
                        width=str(box.width),
546
                        height=str(box.height),
547
                        stroke="blue",
548
                        fill="none",
549
                        opacity=str(layout_opacity),
550
                        attrib={"stroke-width": str(layout_stroke_width)},
551
                        )
552
                    xml.SubElement(
553
                        group,
554
                        "line",
555
                        x1=str(box.left),
556
                        y1=str(box.baseline),
557
                        x2=str(box.right),
558
                        y2=str(box.baseline),
559
                        stroke="blue",
560
                        fill="none",
561
                        opacity=str(layout_opacity),
562
                        attrib={"stroke-width": str(layout_stroke_width)},
563
                        )
564

565
            elif isinstance(box, toyplot.text.MarkerBox):
9✔
566
                if box.marker:
9✔
567
                    _draw_marker(
9✔
568
                        group,
569
                        cx=(box.left + box.right) * 0.5,
570
                        cy=(box.top + box.bottom) * 0.5,
571
                        marker=toyplot.marker.create(size=box.height) + box.marker,
572
                        )
573
                if box.style.get("-toyplot-text-layout-box-visibility", None) == "visible": # pragma: no cover
574
                    xml.SubElement(
575
                        group,
576
                        "rect",
577
                        x=str(box.left),
578
                        y=str(box.top),
579
                        width=str(box.width),
580
                        height=str(box.height),
581
                        stroke="blue",
582
                        fill="none",
583
                        opacity=str(layout_opacity),
584
                        attrib={"stroke-width": str(layout_stroke_width)},
585
                        )
586
                    xml.SubElement(
587
                        group,
588
                        "line",
589
                        x1=str(box.left),
590
                        y1=str(box.baseline),
591
                        x2=str(box.right),
592
                        y2=str(box.baseline),
593
                        stroke="blue",
594
                        fill="none",
595
                        opacity=str(layout_opacity),
596
                        attrib={"stroke-width": str(layout_stroke_width)},
597
                        )
598

599
            elif isinstance(box, toyplot.text.PushHyperlink):
9✔
600
                hyperlink.append(group)
9✔
601
                group = xml.SubElement(
9✔
602
                    group,
603
                    "a",
604
                    style=toyplot.style.to_css(box.style),
605
                    )
606
                group.set("xlink:href", box.href)
9✔
607
                if box.target is not None:
9✔
608
                    group.set("target", box.target)
9✔
609
            elif isinstance(box, toyplot.text.PopHyperlink):
9✔
610
                group = hyperlink.pop()
9✔
611

612

613
def _draw_bar(parent_xml, size, angle=0):
9✔
614
    markup = xml.SubElement(
9✔
615
        parent_xml,
616
        "line",
617
        y1=str(-size / 2),
618
        y2=str(size / 2),
619
        )
620
    if angle:
9✔
621
        markup.set("transform", "rotate(%s)" % (-angle,))
9✔
622

623

624
def _draw_rect(parent_xml, size, width=1, height=1, angle=0):
9✔
625
    markup = xml.SubElement(
9✔
626
        parent_xml,
627
        "rect",
628
        x=str(-size / 2 * width),
629
        y=str(-size / 2 * height),
630
        width=str(size * width),
631
        height=str(size * height),
632
        )
633
    if angle:
9✔
634
        markup.set("transform", "rotate(%s)" % (-angle,))
9✔
635

636

637
def _draw_triangle(parent_xml, size, angle=0):
9✔
638
    markup = xml.SubElement(
9✔
639
        parent_xml,
640
        "polygon",
641
        points=" ".join(["%s,%s" % (xp, yp) for xp, yp in [
642
           (-size / 2, size / 2),
643
           (0, -size / 2),
644
           (size / 2, size / 2),
645
           ]]),
646
        )
647
    if angle:
9✔
648
        markup.set("transform", "rotate(%s)" % (-angle,))
9✔
649

650

651
def _draw_circle(parent_xml, size):
9✔
652
    xml.SubElement(
9✔
653
        parent_xml,
654
        "circle",
655
        r=str(size / 2),
656
        )
657

658
def _draw_marker(
9✔
659
        root,
660
        marker,
661
        cx=None,
662
        cy=None,
663
        extra_class=None,
664
        title=None,
665
        transform=None,
666
        ):
667

668
    attrib = _css_attrib(marker.mstyle)
9✔
669
    if extra_class is not None:
9✔
670
        attrib["class"] = extra_class
9✔
671
    marker_xml = xml.SubElement(root, "g", attrib=attrib)
9✔
672
    if title is not None:
9✔
673
        xml.SubElement(marker_xml, "title").text = str(title)
9✔
674

675
    if transform is None:
9✔
676
        transform = "translate(%s, %s)" % (cx, cy)
9✔
677
        if marker.angle:
9✔
678
            transform += " rotate(%s)" % (-marker.angle,)
9✔
679
    marker_xml.set("transform", transform)
9✔
680

681
    if marker.shape == "|":
9✔
682
        _draw_bar(marker_xml, marker.size)
9✔
683
    elif marker.shape == "/":
9✔
684
        _draw_bar(marker_xml, marker.size, angle=-45)
9✔
685
    elif marker.shape == "-":
9✔
686
        _draw_bar(marker_xml, marker.size, angle=90)
9✔
687
    elif marker.shape == "\\":
9✔
688
        _draw_bar(marker_xml, marker.size, angle=45)
9✔
689
    elif marker.shape == "+":
9✔
690
        _draw_bar(marker_xml, marker.size)
9✔
691
        _draw_bar(marker_xml, marker.size, angle=90)
9✔
692
    elif marker.shape == "x":
9✔
693
        _draw_bar(marker_xml, marker.size, angle=-45)
9✔
694
        _draw_bar(marker_xml, marker.size, angle=45)
9✔
695
    elif marker.shape == "*":
9✔
696
        _draw_bar(marker_xml, marker.size)
9✔
697
        _draw_bar(marker_xml, marker.size, angle=-60)
9✔
698
        _draw_bar(marker_xml, marker.size, angle=60)
9✔
699
    elif marker.shape == "^":
9✔
700
        _draw_triangle(marker_xml, marker.size)
9✔
701
    elif marker.shape == ">":
9✔
702
        _draw_triangle(marker_xml, marker.size, angle=-90)
9✔
703
    elif marker.shape == "v":
9✔
704
        _draw_triangle(marker_xml, marker.size, angle=180)
9✔
705
    elif marker.shape == "<":
9✔
706
        _draw_triangle(marker_xml, marker.size, angle=90)
9✔
707
    elif marker.shape == "s":
9✔
708
        _draw_rect(marker_xml, marker.size)
9✔
709
    elif marker.shape == "d":
9✔
710
        _draw_rect(marker_xml, marker.size, angle=45)
9✔
711
    elif marker.shape and marker.shape[0] == "r":
9✔
712
        width, height = marker.shape[1:].split("x")
×
NEW
713
        _draw_rect(marker_xml, marker.size, width=as_float(width), height=as_float(height))
×
714
    elif marker.shape == "o":
9✔
715
        _draw_circle(marker_xml, marker.size)
9✔
716
    elif marker.shape == "oo":
9✔
717
        _draw_circle(marker_xml, marker.size)
9✔
718
        _draw_circle(marker_xml, marker.size / 2)
9✔
719
    elif marker.shape == "o|":
9✔
720
        _draw_circle(marker_xml, marker.size)
9✔
721
        _draw_bar(marker_xml, marker.size)
9✔
722
    elif marker.shape == "o/":
9✔
723
        _draw_circle(marker_xml, marker.size)
×
724
        _draw_bar(marker_xml, marker.size, -45)
×
725
    elif marker.shape == "o-":
9✔
726
        _draw_circle(marker_xml, marker.size)
9✔
727
        _draw_bar(marker_xml, marker.size, 90)
9✔
728
    elif marker.shape == "o\\":
9✔
729
        _draw_circle(marker_xml, marker.size)
×
730
        _draw_bar(marker_xml, marker.size, 45)
×
731
    elif marker.shape == "o+":
9✔
732
        _draw_circle(marker_xml, marker.size)
9✔
733
        _draw_bar(marker_xml, marker.size)
9✔
734
        _draw_bar(marker_xml, marker.size, 90)
9✔
735
    elif marker.shape == "ox":
9✔
736
        _draw_circle(marker_xml, marker.size)
9✔
737
        _draw_bar(marker_xml, marker.size, -45)
9✔
738
        _draw_bar(marker_xml, marker.size, 45)
9✔
739
    elif marker.shape == "o*":
9✔
740
        _draw_circle(marker_xml, marker.size)
9✔
741
        _draw_bar(marker_xml, marker.size)
9✔
742
        _draw_bar(marker_xml, marker.size, -60)
9✔
743
        _draw_bar(marker_xml, marker.size, 60)
9✔
744

745
    if marker.label: # Never compute a text layout unless we have to.
9✔
746
        _draw_text(
9✔
747
            root=marker_xml,
748
            text=marker.label,
749
            style=toyplot.style.combine(
750
                {
751
                    "-toyplot-vertical-align": "middle",
752
                    "fill": toyplot.color.black,
753
                    "font-size": "%spx" % (marker.size * 0.75),
754
                    "stroke": "none",
755
                    "text-anchor": "middle",
756
                },
757
                marker.lstyle),
758
            )
759
    return marker_xml
9✔
760

761

762
def _axis_transform(x1, y1, x2, y2, offset, return_length=False):
9✔
763
    p = numpy.row_stack(((x1, y1), (x2, y2)))
9✔
764
    basis = p[1] - p[0]
9✔
765
    length = numpy.linalg.norm(basis)
9✔
766
    theta = numpy.rad2deg(numpy.arctan2(basis[1], basis[0]))
9✔
767
    transform = str()
9✔
768
    if p[0][0] or p[0][1]:
9✔
769
        transform += "translate(%s,%s)" % (p[0][0], p[0][1])
9✔
770
    if theta:
9✔
771
        transform += "rotate(%s)" % theta
9✔
772
    if offset:
9✔
773
        transform += "translate(0,%s)" % offset
9✔
774
    if return_length:
9✔
775
        return transform, length
9✔
776
    return transform
×
777

778

779
@dispatch(toyplot.canvas.Canvas, RenderContext)
9✔
780
def _render(canvas, context):
9✔
781
    # Optionally apply a hyperlink to the entire canvas.
782
    parent_xml = context.parent
9✔
783
    if canvas._hyperlink:
9✔
784
        hyperlink_xml = xml.SubElement(parent_xml, "a", attrib={"href": canvas._hyperlink})
×
785
        parent_xml = hyperlink_xml
×
786

787
    # Create the root SVG element.
788
    svg_xml = xml.SubElement(
9✔
789
        parent_xml,
790
        "svg",
791
        xmlns="http://www.w3.org/2000/svg",
792
        attrib={
793
            "class": "toyplot-canvas-Canvas",
794
            "xmlns:toyplot": "http://www.sandia.gov/toyplot",
795
            "xmlns:xlink": "http://www.w3.org/1999/xlink",
796
            },
797
        width="%spx" % canvas.width,
798
        height="%spx" % canvas.height,
799
        viewBox="0 0 %s %s" % (canvas.width, canvas.height),
800
        preserveAspectRatio="xMidYMid meet",
801
        style=_css_style(canvas._style),
802
        id=context.get_id(canvas))
803

804
    # Render everything on the canvas.
805
    for node in context.scenegraph.targets(canvas, "render"):
9✔
806
        _render(node._finalize(), context.copy(parent=svg_xml))
9✔
807

808
    # Create a container for any Javascript code.
809
    javascript_xml = xml.SubElement(
9✔
810
        context.parent,
811
        "div",
812
        attrib={"class": "toyplot-behavior"},
813
        )
814

815
    # Register a Javascript module to keep track of the canvas id.
816
    context.define("toyplot/canvas/id", value=context.get_id(canvas))
9✔
817
    # Register a Javascript module to keep track of the canvas.
818
    context.define("toyplot/canvas", ["toyplot/canvas/id"], factory="""function(canvas_id)
9✔
819
    {
820
        return document.querySelector("#" + canvas_id);
821
    }""")
822

823
    # Register a Javascript module for storing table data.
824
    context.define("toyplot/tables", factory="""function()
9✔
825
    {
826
        var tables = [];
827

828
        var module = {};
829

830
        module.set = function(owner, key, names, columns)
831
        {
832
            tables.push({owner: owner, key: key, names: names, columns: columns});
833
        }
834

835
        module.get = function(owner, key)
836
        {
837
            for(var i = 0; i != tables.length; ++i)
838
            {
839
                var table = tables[i];
840
                if(table.owner != owner)
841
                    continue;
842
                if(table.key != key)
843
                    continue;
844
                return {names: table.names, columns: table.columns};
845
            }
846
        }
847

848
        module.get_csv = function(owner, key)
849
        {
850
            var table = module.get(owner, key);
851
            if(table != undefined)
852
            {
853
                var csv = "";
854
                csv += table.names.join(",") + "\\n";
855
                for(var i = 0; i != table.columns[0].length; ++i)
856
                {
857
                  for(var j = 0; j != table.columns.length; ++j)
858
                  {
859
                    if(j)
860
                      csv += ",";
861
                    csv += table.columns[j][i];
862
                  }
863
                  csv += "\\n";
864
                }
865
                return csv;
866
            }
867
        }
868

869
        return module;
870
    }""")
871

872
    # Register a Javascript module for saving data from the browser.
873
    context.define("toyplot/io", factory="""function()
9✔
874
    {
875
        var module = {};
876
        module.save_file = function(mime_type, charset, data, filename)
877
        {
878
            var uri = "data:" + mime_type + ";charset=" + charset + "," + data;
879
            uri = encodeURI(uri);
880

881
            var link = document.createElement("a");
882
            if(typeof link.download != "undefined")
883
            {
884
              link.href = uri;
885
              link.style = "visibility:hidden";
886
              link.download = filename;
887

888
              document.body.appendChild(link);
889
              link.click();
890
              document.body.removeChild(link);
891
            }
892
            else
893
            {
894
              window.open(uri);
895
            }
896
        };
897
        return module;
898
    }""")
899

900
    # Register a Javascript module to provide a popup context menu.
901
    context.define("toyplot/menus/context", ["toyplot/root", "toyplot/canvas"], factory="""function(root, canvas)
9✔
902
    {
903
        var wrapper = document.createElement("div");
904
        wrapper.innerHTML = "<ul class='toyplot-context-menu' style='background:#eee; border:1px solid #b8b8b8; border-radius:5px; box-shadow: 0px 0px 8px rgba(0%,0%,0%,0.25); margin:0; padding:3px 0; position:fixed; visibility:hidden;'></ul>"
905
        var menu = wrapper.firstChild;
906

907
        root.appendChild(menu);
908

909
        var items = [];
910

911
        var ignore_mouseup = null;
912
        function open_menu(e)
913
        {
914
            var show_menu = false;
915
            for(var index=0; index != items.length; ++index)
916
            {
917
                var item = items[index];
918
                if(item.show(e))
919
                {
920
                    item.item.style.display = "block";
921
                    show_menu = true;
922
                }
923
                else
924
                {
925
                    item.item.style.display = "none";
926
                }
927
            }
928

929
            if(show_menu)
930
            {
931
                ignore_mouseup = true;
932
                menu.style.left = (e.clientX + 1) + "px";
933
                menu.style.top = (e.clientY - 5) + "px";
934
                menu.style.visibility = "visible";
935
                e.stopPropagation();
936
                e.preventDefault();
937
            }
938
        }
939

940
        function close_menu()
941
        {
942
            menu.style.visibility = "hidden";
943
        }
944

945
        function contextmenu(e)
946
        {
947
            open_menu(e);
948
        }
949

950
        function mousemove(e)
951
        {
952
            ignore_mouseup = false;
953
        }
954

955
        function mouseup(e)
956
        {
957
            if(ignore_mouseup)
958
            {
959
                ignore_mouseup = false;
960
                return;
961
            }
962
            close_menu();
963
        }
964

965
        function keydown(e)
966
        {
967
            if(e.key == "Escape" || e.key == "Esc" || e.keyCode == 27)
968
            {
969
                close_menu();
970
            }
971
        }
972

973
        canvas.addEventListener("contextmenu", contextmenu);
974
        canvas.addEventListener("mousemove", mousemove);
975
        document.addEventListener("mouseup", mouseup);
976
        document.addEventListener("keydown", keydown);
977

978
        var module = {};
979
        module.add_item = function(label, show, activate)
980
        {
981
            var wrapper = document.createElement("div");
982
            wrapper.innerHTML = "<li class='toyplot-context-menu-item' style='background:#eee; color:#333; padding:2px 20px; list-style:none; margin:0; text-align:left;'>" + label + "</li>"
983
            var item = wrapper.firstChild;
984

985
            items.push({item: item, show: show});
986

987
            function mouseover()
988
            {
989
                this.style.background = "steelblue";
990
                this.style.color = "white";
991
            }
992

993
            function mouseout()
994
            {
995
                this.style.background = "#eee";
996
                this.style.color = "#333";
997
            }
998

999
            function choose_item(e)
1000
            {
1001
                close_menu();
1002
                activate();
1003

1004
                e.stopPropagation();
1005
                e.preventDefault();
1006
            }
1007

1008
            item.addEventListener("mouseover", mouseover);
1009
            item.addEventListener("mouseout", mouseout);
1010
            item.addEventListener("mouseup", choose_item);
1011
            item.addEventListener("contextmenu", choose_item);
1012

1013
            menu.appendChild(item);
1014
        };
1015
        return module;
1016
    }""")
1017

1018
    # Embed Javascript code and dependencies in the container.
1019
    _render_javascript(context.copy(parent=javascript_xml))
9✔
1020

1021

1022
def _render_javascript(context):
9✔
1023
    # Convert module dependencies into an adjacency list.
1024
    adjacency_list = collections.defaultdict(list)
9✔
1025
    for name, (requirements, factory, value) in context._javascript_modules.items():
9✔
1026
        for requirement in requirements:
9✔
1027
            adjacency_list[name].append(requirement)
9✔
1028

1029
    # Identify required modules and sort them into topological order.
1030
    modules = []
9✔
1031
    visited = {}
9✔
1032
    def search(name, visited, modules):
9✔
1033
        visited[name] = True
9✔
1034
        for neighbor in adjacency_list[name]:
9✔
1035
            if not visited.get(neighbor, False):
9✔
1036
                search(neighbor, visited, modules)
9✔
1037
        modules.append((name, context._javascript_modules[name]))
9✔
1038
    for requirements, arguments, code in context._javascript_calls:
9✔
1039
        for requirement in requirements:
9✔
1040
            if not visited.get(requirement, False):
9✔
1041
                search(requirement, visited, modules)
9✔
1042

1043
    # Generate the code.
1044
    script = """(function()
9✔
1045
{
1046
var modules={};
1047
"""
1048

1049
    # Initialize required modules.
1050
    for name, (requirements, factory, value) in modules:
9✔
1051
        script += """modules["%s"] = """ % name
9✔
1052

1053
        if factory is not None:
9✔
1054
            script += "("
9✔
1055
            script += factory
9✔
1056
            script += ")("
9✔
1057
            argument_list = ["""modules["%s"]""" % requirement for requirement in requirements]
9✔
1058
            script += ",".join(argument_list)
9✔
1059
            script += ");\n"
9✔
1060
        if value is not None:
9✔
1061
            script += json.dumps(value, cls=_CustomJSONEncoder, sort_keys=True)
9✔
1062
            script += ";\n"
9✔
1063

1064
    # Make all calls.
1065
    for requirements, arguments, code in context._javascript_calls:
9✔
1066
        script += """("""
9✔
1067
        script += code
9✔
1068
        script += """)("""
9✔
1069
        argument_list = ["""modules["%s"]""" % requirement for requirement in requirements]
9✔
1070
        argument_list += [json.dumps(argument, cls=_CustomJSONEncoder, sort_keys=True) for argument in arguments]
9✔
1071
        script += ",".join(argument_list)
9✔
1072
        script += """);\n"""
9✔
1073

1074
    script += """})();"""
9✔
1075

1076
    # Create the DOM elements.
1077
    xml.SubElement(context.parent, "script").text = script
9✔
1078

1079

1080
def _render_table(owner, key, label, table, filename, context):
9✔
1081
    if isinstance(owner, toyplot.mark.Mark) and owner.annotation:
9✔
1082
        return
9✔
1083
    if isinstance(owner, toyplot.coordinates.Table) and owner.annotation:
9✔
1084
        return
9✔
1085

1086
    names = []
9✔
1087
    columns = []
9✔
1088

1089
    if isinstance(table, toyplot.data.Table):
9✔
1090
        for name, column in table.items():
9✔
1091
            if "toyplot:exportable" in table.metadata(name) and table.metadata(name)["toyplot:exportable"]:
9✔
1092
                if column.dtype == toyplot.color.dtype:
9✔
1093
                    raise ValueError("Color column table export isn't supported.") # pragma: no cover
1094
                else:
1095
                    names.append(name)
9✔
1096
                    columns.append(column.tolist())
9✔
1097
    else: # Assume numpy matrix
1098
        for column in table.T:
9✔
1099
            names.append(column[0])
9✔
1100
            columns.append(column[1:].tolist())
9✔
1101

1102
    if not (names and columns):
9✔
1103
        return
9✔
1104

1105
    owner_id = context.get_id(owner)
9✔
1106
    if filename is None:
9✔
1107
        filename = "toyplot"
9✔
1108

1109
    context.require(
9✔
1110
        dependencies=["toyplot/tables", "toyplot/menus/context", "toyplot/io"],
1111
        arguments=[owner_id, key, label, names, columns, filename],
1112
        code="""function(tables, context_menu, io, owner_id, key, label, names, columns, filename)
1113
        {
1114
            tables.set(owner_id, key, names, columns);
1115

1116
            var owner = document.querySelector("#" + owner_id);
1117
            function show_item(e)
1118
            {
1119
                return owner.contains(e.target);
1120
            }
1121

1122
            function choose_item()
1123
            {
1124
                io.save_file("text/csv", "utf-8", tables.get_csv(owner_id, key), filename + ".csv");
1125
            }
1126

1127
            context_menu.add_item("Save " + label + " as CSV", show_item, choose_item);
1128
        }""",
1129
    )
1130

1131

1132
@dispatch(toyplot.coordinates.Axis, RenderContext)
9✔
1133
def _render(axis, context):
9✔
1134
    if context.already_rendered(axis):
9✔
1135
        return
9✔
1136

1137
    if not axis.show:
9✔
1138
        return
9✔
1139

1140
    transform, length = _axis_transform(axis._x1, axis._y1, axis._x2, axis._y2, offset=axis._offset, return_length=True)
9✔
1141

1142
    axis_xml = xml.SubElement(
9✔
1143
        context.parent,
1144
        "g",
1145
        id=context.get_id(axis),
1146
        transform=transform,
1147
        attrib={"class": "toyplot-coordinates-Axis"},
1148
        )
1149

1150
    if axis.spine.show:
9✔
1151
        x1 = 0
9✔
1152
        x2 = length
9✔
1153
        if axis.domain.show and axis._data_min is not None and axis._data_max is not None:
9✔
1154
            x1 = max(
9✔
1155
                x1, axis.projection(axis._data_min))
1156
            x2 = min(
9✔
1157
                x2, axis.projection(axis._data_max))
1158
        xml.SubElement(
9✔
1159
            axis_xml,
1160
            "line",
1161
            x1=str(x1),
1162
            y1=str(0),
1163
            x2=str(x2),
1164
            y2=str(0),
1165
            style=_css_style(
1166
                axis.spine._style))
1167

1168
        if axis.ticks.show:
9✔
1169
            y1 = axis._ticks_near if axis._tick_location == "below" else -axis._ticks_near
9✔
1170
            y2 = -axis._ticks_far if axis._tick_location == "below" else axis._ticks_far
9✔
1171

1172
            ticks_group = xml.SubElement(axis_xml, "g")
9✔
1173
            for location, tick_style in zip(
9✔
1174
                    axis._tick_locations,
1175
                    axis.ticks.tick.styles(axis._tick_locations),
1176
                ):
1177
                x = axis.projection(location)
9✔
1178
                xml.SubElement(
9✔
1179
                    ticks_group,
1180
                    "line",
1181
                    x1=str(x),
1182
                    y1=str(y1),
1183
                    x2=str(x),
1184
                    y2=str(y2),
1185
                    style=_css_style(
1186
                        axis.ticks._style,
1187
                        tick_style))
1188

1189
    if axis.ticks.labels.show:
9✔
1190
        location = axis._tick_labels_location
9✔
1191

1192
        if axis.ticks.labels.angle:
9✔
1193
            vertical_align = "middle"
9✔
1194

1195
            if location == "above":
9✔
1196
                text_anchor = "start" if axis.ticks.labels.angle > 0 else "end"
9✔
1197
            elif location == "below":
9✔
1198
                text_anchor = "end" if axis.ticks.labels.angle > 0 else "start"
9✔
1199
        else:
1200
            vertical_align = "last-baseline" if location == "above" else "top"
9✔
1201
            text_anchor = "middle"
9✔
1202

1203
        y = axis._tick_labels_offset if location == "below" else -axis._tick_labels_offset
9✔
1204

1205
        ticks_group = xml.SubElement(axis_xml, "g")
9✔
1206
        for location, label, title, label_style in zip(
9✔
1207
                axis._tick_locations,
1208
                axis._tick_labels,
1209
                axis._tick_titles,
1210
                axis.ticks.labels.label.styles(axis._tick_locations),
1211
            ):
1212
            x = axis.projection(location)
9✔
1213

1214
            style = toyplot.style.combine(
9✔
1215
                {
1216
                    "-toyplot-vertical-align": vertical_align,
1217
                    "text-anchor": text_anchor,
1218
                },
1219
                axis.ticks.labels.style,
1220
                label_style,
1221
            )
1222

1223
            _draw_text(
9✔
1224
                root=ticks_group,
1225
                text=label,
1226
                x=x,
1227
                y=y,
1228
                style=style,
1229
                angle=axis.ticks.labels.angle,
1230
                title=title,
1231
                )
1232

1233
    location = axis._label_location
9✔
1234
    vertical_align = "last-baseline" if location == "above" else "top"
9✔
1235
    text_anchor = "middle"
9✔
1236
    y = axis._label_offset if location == "below" else -axis._label_offset
9✔
1237

1238
    _draw_text(
9✔
1239
        root=axis_xml,
1240
        text=axis.label.text,
1241
        x=length * 0.5,
1242
        y=y,
1243
        style=toyplot.style.combine(
1244
            {
1245
                "-toyplot-vertical-align": vertical_align,
1246
                "text-anchor": text_anchor,
1247
            },
1248
            axis.label.style,
1249
        ),
1250
        )
1251

1252
    if axis.interactive.coordinates.show:
9✔
1253
        coordinates_xml = xml.SubElement(
9✔
1254
            axis_xml, "g",
1255
            attrib={"class": "toyplot-coordinates-Axis-coordinates"},
1256
            style=_css_style({"visibility": "hidden"}),
1257
            transform="",
1258
            )
1259

1260
        if axis.interactive.coordinates.tick.show:
9✔
1261
            y1 = axis._tick_labels_offset if axis._interactive_coordinates_location == "below" else -axis._tick_labels_offset
9✔
1262
            y1 *= 0.5
9✔
1263
            y2 = -axis._tick_labels_offset if axis._interactive_coordinates_location == "below" else axis._tick_labels_offset
9✔
1264
            y2 *= 0.75
9✔
1265
            xml.SubElement(
9✔
1266
                coordinates_xml, "line",
1267
                x1="0",
1268
                x2="0",
1269
                y1=str(y1),
1270
                y2=str(y2),
1271
                style=_css_style(axis.interactive.coordinates.tick.style),
1272
                )
1273

1274
        if axis.interactive.coordinates.label.show:
9✔
1275
            y = axis._tick_labels_offset if axis._interactive_coordinates_location == "below" else -axis._tick_labels_offset
9✔
1276
            alignment_baseline = "hanging" if axis._interactive_coordinates_location == "below" else "alphabetic"
9✔
1277
            xml.SubElement(
9✔
1278
                coordinates_xml, "text",
1279
                x="0",
1280
                y=str(y),
1281
                style=_css_style(toyplot.style.combine(
1282
                    {"alignment-baseline": alignment_baseline},
1283
                    axis.interactive.coordinates.label.style,
1284
                    )),
1285
                )
1286

1287
    context.define("toyplot.coordinates.Axis", ["toyplot/canvas"], """
9✔
1288
        function(canvas)
1289
        {
1290
            function sign(x)
1291
            {
1292
                return x < 0 ? -1 : x > 0 ? 1 : 0;
1293
            }
1294

1295
            function mix(a, b, amount)
1296
            {
1297
                return ((1.0 - amount) * a) + (amount * b);
1298
            }
1299

1300
            function log(x, base)
1301
            {
1302
                return Math.log(Math.abs(x)) / Math.log(base);
1303
            }
1304

1305
            function in_range(a, x, b)
1306
            {
1307
                var left = Math.min(a, b);
1308
                var right = Math.max(a, b);
1309
                return left <= x && x <= right;
1310
            }
1311

1312
            function inside(range, projection)
1313
            {
1314
                for(var i = 0; i != projection.length; ++i)
1315
                {
1316
                    var segment = projection[i];
1317
                    if(in_range(segment.range.min, range, segment.range.max))
1318
                        return true;
1319
                }
1320
                return false;
1321
            }
1322

1323
            function to_domain(range, projection)
1324
            {
1325
                for(var i = 0; i != projection.length; ++i)
1326
                {
1327
                    var segment = projection[i];
1328
                    if(in_range(segment.range.bounds.min, range, segment.range.bounds.max))
1329
                    {
1330
                        if(segment.scale == "linear")
1331
                        {
1332
                            var amount = (range - segment.range.min) / (segment.range.max - segment.range.min);
1333
                            return mix(segment.domain.min, segment.domain.max, amount)
1334
                        }
1335
                        else if(segment.scale[0] == "log")
1336
                        {
1337
                            var amount = (range - segment.range.min) / (segment.range.max - segment.range.min);
1338
                            var base = segment.scale[1];
1339
                            return sign(segment.domain.min) * Math.pow(base, mix(log(segment.domain.min, base), log(segment.domain.max, base), amount));
1340
                        }
1341
                    }
1342
                }
1343
            }
1344

1345
            var axes = {};
1346

1347
            function display_coordinates(e)
1348
            {
1349
                var current = canvas.createSVGPoint();
1350
                current.x = e.clientX;
1351
                current.y = e.clientY;
1352

1353
                for(var axis_id in axes)
1354
                {
1355
                    var axis = document.querySelector("#" + axis_id);
1356
                    var coordinates = axis.querySelector(".toyplot-coordinates-Axis-coordinates");
1357
                    if(coordinates)
1358
                    {
1359
                        var projection = axes[axis_id];
1360
                        var local = current.matrixTransform(axis.getScreenCTM().inverse());
1361
                        if(inside(local.x, projection))
1362
                        {
1363
                            var domain = to_domain(local.x, projection);
1364
                            coordinates.style.visibility = "visible";
1365
                            coordinates.setAttribute("transform", "translate(" + local.x + ")");
1366
                            var text = coordinates.querySelector("text");
1367
                            text.textContent = domain.toFixed(2);
1368
                        }
1369
                        else
1370
                        {
1371
                            coordinates.style.visibility= "hidden";
1372
                        }
1373
                    }
1374
                }
1375
            }
1376

1377
            canvas.addEventListener("click", display_coordinates);
1378

1379
            var module = {};
1380
            module.show_coordinates = function(axis_id, projection)
1381
            {
1382
                axes[axis_id] = projection;
1383
            }
1384

1385
            return module;
1386
        }""",
1387
        )
1388

1389
    projection = []
9✔
1390
    for segment in axis.projection._segments:
9✔
1391
        projection.append({
9✔
1392
            "scale": segment.scale,
1393
            "domain":
1394
            {
1395
                "min": segment.domain.min,
1396
                "max": segment.domain.max,
1397
                "bounds":
1398
                {
1399
                    "min": segment.domain.bounds.min,
1400
                    "max": segment.domain.bounds.max,
1401
                },
1402
            },
1403
            "range":
1404
            {
1405
                "min": segment.range.min,
1406
                "max": segment.range.max,
1407
                "bounds":
1408
                {
1409
                    "min": segment.range.bounds.min,
1410
                    "max": segment.range.bounds.max,
1411
                },
1412
            },
1413
        })
1414

1415
    context.require(
9✔
1416
        dependencies=["toyplot.coordinates.Axis"],
1417
        arguments=[context.get_id(axis), projection],
1418
        code="""function(axis, axis_id, projection)
1419
        {
1420
            axis.show_coordinates(axis_id, projection);
1421
        }""",
1422
    )
1423

1424

1425
@dispatch(toyplot.coordinates.Numberline, RenderContext)
9✔
1426
def _render(numberline, context):
9✔
1427
    numberline_xml = xml.SubElement(context.parent, "g", id=context.get_id(
9✔
1428
        numberline), attrib={"class": "toyplot-coordinates-Numberline"})
1429

1430
    clip_xml = xml.SubElement(
9✔
1431
        numberline_xml,
1432
        "clipPath",
1433
        id="t" + uuid.uuid4().hex,
1434
        )
1435

1436
    transform, length = _axis_transform(numberline._x1, numberline._y1, numberline._x2, numberline._y2, offset=0, return_length=True)
9✔
1437

1438
    height = numberline.axis._offset
9✔
1439
    if numberline._child_offset:
9✔
1440
        height += numpy.amax(list(numberline._child_offset.values()))
9✔
1441

1442
    xml.SubElement(
9✔
1443
        clip_xml,
1444
        "rect",
1445
        x=str(0),
1446
        y=str(-height),
1447
        width=str(length),
1448
        height=str(height + numberline.axis._offset),
1449
        )
1450

1451
    children_xml = xml.SubElement(
9✔
1452
        numberline_xml,
1453
        "g",
1454
        attrib={"clip-path": "url(#%s)" % clip_xml.get("id")},
1455
        transform=transform,
1456
        )
1457

1458
    for child in numberline._scenegraph.targets(numberline, "render"):
9✔
1459
        _render(numberline, child._finalize(), context.copy(parent=children_xml))
9✔
1460

1461
    _render(numberline.axis, context.copy(parent=numberline_xml))
9✔
1462

1463

1464
@dispatch(toyplot.coordinates.Numberline, toyplot.color.CategoricalMap, RenderContext)
9✔
1465
def _render(numberline, colormap, context):
9✔
1466
    offset = numberline._child_offset[colormap]
9✔
1467
    width = numberline._child_width[colormap]
9✔
1468
    style = numberline._child_style[colormap]
9✔
1469

1470
    mark_xml = xml.SubElement(
9✔
1471
        context.parent,
1472
        "g", id=context.get_id(colormap),
1473
        attrib={"class": "toyplot-color-CategoricalMap"},
1474
        )
1475
    if offset:
9✔
UNCOV
1476
        mark_xml.set("transform", "translate(0,%s)" % -offset)
×
1477

1478
    samples = numpy.linspace(colormap.domain.min, colormap.domain.max, len(colormap._palette), endpoint=True)
9✔
1479
    projected = numberline.axis.projection(samples)
9✔
1480
    colormap_range_min, colormap_range_max = numberline.axis.projection([colormap.domain.min, colormap.domain.max])
9✔
1481

1482
    for index, (x1, x2), in enumerate(zip(projected[:-1], projected[1:])):
9✔
1483
        color = colormap._palette[index]
9✔
1484
        xml.SubElement(
9✔
1485
            mark_xml,
1486
            "rect",
1487
            x=str(x1),
1488
            y=str(-width * 0.5),
1489
            width=str(x2 - x1),
1490
            height=str(width),
1491
            style=_css_style({"stroke": "none", "fill": toyplot.color.to_css(color)}),
1492
            )
1493

1494
    style = toyplot.style.combine(
9✔
1495
        {"stroke": "none", "stroke-width":1.0, "fill": "none"},
1496
        style,
1497
        )
1498

1499
    xml.SubElement(
9✔
1500
        mark_xml,
1501
        "rect",
1502
        x=str(colormap_range_min),
1503
        y=str(-width * 0.5),
1504
        width=str(colormap_range_max - colormap_range_min),
1505
        height=str(width),
1506
        style=_css_style(style),
1507
        )
1508

1509
@dispatch(toyplot.coordinates.Numberline, toyplot.color.Map, RenderContext)
9✔
1510
def _render(numberline, colormap, context):
9✔
1511
    offset = numberline._child_offset[colormap]
9✔
1512
    width = numberline._child_width[colormap]
9✔
1513
    style = numberline._child_style[colormap]
9✔
1514

1515
    colormap_range_min, colormap_range_max = numberline.axis.projection([colormap.domain.min, colormap.domain.max])
9✔
1516

1517
    mark_xml = xml.SubElement(
9✔
1518
        context.parent,
1519
        "g", id=context.get_id(colormap),
1520
        attrib={"class": "toyplot-color-Map"},
1521
        )
1522
    if offset:
9✔
UNCOV
1523
        mark_xml.set("transform", "translate(0, %s)" % -offset)
×
1524

1525
    defs_xml = xml.SubElement(
9✔
1526
        mark_xml,
1527
        "defs",
1528
        )
1529

1530
    gradient_xml = xml.SubElement(
9✔
1531
        defs_xml,
1532
        "linearGradient",
1533
        id="t" + uuid.uuid4().hex,
1534
        x1=str(colormap_range_min),
1535
        x2=str(colormap_range_max),
1536
        y1=str(0),
1537
        y2=str(0),
1538
        gradientUnits="userSpaceOnUse",
1539
        )
1540

1541
    samples = numpy.linspace(colormap.domain.min, colormap.domain.max, 64, endpoint=True)
9✔
1542
    for sample in samples:
9✔
1543
        color = colormap.colors(sample)
9✔
1544
        psample = numberline.axis.projection(sample)
9✔
1545
        offset = (psample - colormap_range_min) / (colormap_range_max - colormap_range_min)
9✔
1546
        xml.SubElement(
9✔
1547
            gradient_xml,
1548
            "stop",
1549
            offset="%s" % offset,
1550
            attrib={
1551
                "stop-color": "rgb(%.3g%%,%.3g%%,%.3g%%)" % (color["r"] * 100, color["g"] * 100, color["b"] * 100),
1552
                "stop-opacity": str(color["a"]),
1553
                },
1554
            )
1555

1556
    style = toyplot.style.combine(
9✔
1557
        {"stroke": "none", "stroke-width":1.0, "fill": "url(#%s)" % gradient_xml.get("id")},
1558
        style,
1559
        )
1560

1561
    xml.SubElement(
9✔
1562
        mark_xml,
1563
        "rect",
1564
        x=str(colormap_range_min),
1565
        y=str(-width * 0.5),
1566
        width=str(colormap_range_max - colormap_range_min),
1567
        height=str(width),
1568
        style=_css_style(style),
1569
        )
1570

1571

1572
@dispatch(toyplot.coordinates.Numberline, toyplot.mark.Point, RenderContext)
9✔
1573
def _render(numberline, mark, context):
9✔
1574
    offset = numberline._child_offset[mark]
9✔
1575

1576
    mark_xml = xml.SubElement(
9✔
1577
        context.parent,
1578
        "g",
1579
        id=context.get_id(mark),
1580
        attrib={"class": "toyplot-mark-Point"},
1581
        )
1582
    if offset:
9✔
UNCOV
1583
        mark_xml.set("transform", "translate(0,%s)" % -offset)
×
1584

1585
    _render_table(owner=mark, key="data", label="point data", table=mark._table, filename=mark._filename, context=context)
9✔
1586

1587
    dimension1 = numpy.ma.column_stack([mark._table[key] for key in mark._coordinates])
9✔
1588
    X = numberline.axis.projection(dimension1)
9✔
1589
    for x, marker, msize, mfill, mstroke, mopacity, mtitle, mhyperlink in zip(
9✔
1590
            X.T,
1591
            [mark._table[key] for key in mark._marker],
1592
            [mark._table[key] for key in mark._msize],
1593
            [mark._table[key] for key in mark._mfill],
1594
            [mark._table[key] for key in mark._mstroke],
1595
            [mark._table[key] for key in mark._mopacity],
1596
            [mark._table[key] for key in mark._mtitle],
1597
            [mark._table[key] for key in mark._mhyperlink],
1598
        ):
1599
        not_null = numpy.invert(numpy.ma.getmaskarray(x))
9✔
1600

1601
        series_xml = xml.SubElement(mark_xml, "g", attrib={"class": "toyplot-Series"})
9✔
1602
        for dx, dmarker, dsize, dfill, dstroke, dopacity, dtitle, dhyperlink in zip(
9✔
1603
                x[not_null],
1604
                marker[not_null],
1605
                msize[not_null],
1606
                mfill[not_null],
1607
                mstroke[not_null],
1608
                mopacity[not_null],
1609
                mtitle[not_null],
1610
                mhyperlink[not_null],
1611
            ):
1612
            if dmarker:
9✔
1613
                if dhyperlink:
9✔
UNCOV
1614
                    datum_xml = xml.SubElement(series_xml, "a", attrib={"xlink:href": dhyperlink})
×
1615
                else:
1616
                    datum_xml = series_xml
9✔
1617

1618
                dstyle = toyplot.style.combine(
9✔
1619
                    {
1620
                        "fill": toyplot.color.to_css(dfill),
1621
                        "stroke": toyplot.color.to_css(dstroke),
1622
                        "opacity": dopacity,
1623
                    },
1624
                    mark._mstyle)
1625
                _draw_marker(
9✔
1626
                    datum_xml,
1627
                    cx=dx,
1628
                    cy=0,
1629
                    marker=toyplot.marker.create(size=dsize, mstyle=dstyle, lstyle=mark._mlstyle) + toyplot.marker.convert(dmarker),
1630
                    extra_class="toyplot-Datum",
1631
                    title=dtitle,
1632
                    )
1633

1634

1635
@dispatch(toyplot.coordinates.Numberline, toyplot.mark.Range, RenderContext)
9✔
1636
def _render(numberline, mark, context):
9✔
UNCOV
1637
    offset = numberline._child_offset[mark]
×
UNCOV
1638
    width = numberline._child_width[mark]
×
1639

UNCOV
1640
    mark_xml = xml.SubElement(context.parent, "g", id=context.get_id(mark), attrib={"class": "toyplot-mark-Range"})
×
UNCOV
1641
    if offset:
×
UNCOV
1642
        mark_xml.set("transform", "translate(0,%s)" % -offset)
×
1643

UNCOV
1644
    _render_table(owner=mark, key="data", label="range data", table=mark._table, filename=mark._filename, context=context)
×
1645

UNCOV
1646
    x1 = numberline.axis.projection(mark._table[mark._coordinates[0]])
×
UNCOV
1647
    x2 = numberline.axis.projection(mark._table[mark._coordinates[1]])
×
1648

UNCOV
1649
    series_xml = xml.SubElement(mark_xml, "g", attrib={"class": "toyplot-Series"})
×
1650

UNCOV
1651
    for dx1, dx2, dfill, dopacity, dtitle in zip(
×
1652
        x1,
1653
        x2,
1654
        mark._table[mark._fill[0]],
1655
        mark._table[mark._opacity[0]],
1656
        mark._table[mark._title[0]],
1657
        ):
UNCOV
1658
        dstyle = toyplot.style.combine({"fill": toyplot.color.to_css(dfill), "opacity": dopacity}, mark._style)
×
UNCOV
1659
        datum_xml = xml.SubElement(
×
1660
            series_xml,
1661
            "rect",
1662
            attrib={"class": "toyplot-Datum"},
1663
            x=str(min(dx1, dx2)),
1664
            y=str(-width * 0.5),
1665
            width=str(numpy.abs(dx1 - dx2)),
1666
            height=str(width),
1667
            style=_css_style(dstyle),
1668
            )
UNCOV
1669
        if dtitle is not None:
×
UNCOV
1670
            xml.SubElement(datum_xml, "title").text = str(dtitle)
×
1671

1672

1673
@dispatch(toyplot.coordinates.Cartesian, RenderContext)
9✔
1674
def _render(axes, context):
9✔
1675
    cartesian_xml = xml.SubElement(context.parent, "g", id=context.get_id(
9✔
1676
        axes), attrib={"class": "toyplot-coordinates-Cartesian"})
1677

1678
    clip_xml = xml.SubElement(cartesian_xml, "clipPath", id="t" + uuid.uuid4().hex)
9✔
1679
    xml.SubElement(
9✔
1680
        clip_xml,
1681
        "rect",
1682
        x=str(axes._xmin_range - axes.padding),
1683
        y=str(axes._ymin_range - axes.padding),
1684
        width=str(axes._xmax_range - axes._xmin_range + axes.padding * 2),
1685
        height=str(axes._ymax_range - axes._ymin_range + axes.padding * 2),
1686
        )
1687

1688
    if axes._hyperlink:
9✔
UNCOV
1689
        hyperlink_xml = xml.SubElement(cartesian_xml, "a", attrib={"xlink:href": axes._hyperlink})
×
UNCOV
1690
        xml.SubElement(
×
1691
            hyperlink_xml,
1692
            "rect",
1693
            x=str(axes._xmin_range),
1694
            y=str(axes._ymin_range),
1695
            width=str(axes._xmax_range - axes._xmin_range),
1696
            height=str(axes._ymax_range - axes._ymin_range),
1697
            attrib={"fill": "none", "stroke": "none", "pointer-events": "fill"},
1698
            )
1699

1700
    children_xml = xml.SubElement(
9✔
1701
        cartesian_xml,
1702
        "g",
1703
        attrib={"clip-path" : "url(#%s)" % clip_xml.get("id")},
1704
        )
1705

1706
    for child in context.scenegraph.targets(axes, "render"):
9✔
1707
        _render(axes, child._finalize(), context.copy(parent=children_xml))
9✔
1708

1709
    if axes._show:
9✔
1710
        _render(axes.x, context.copy(parent=cartesian_xml))
9✔
1711
        _render(axes.y, context.copy(parent=cartesian_xml))
9✔
1712
        _draw_text(
9✔
1713
            root=cartesian_xml,
1714
            text=axes.label._text,
1715
            x=(axes._xmin_range + axes._xmax_range) * 0.5,
1716
            y=axes._ymin_range - axes.label._offset,
1717
            style=axes.label._style,
1718
            )
1719

1720

1721
@dispatch(toyplot.coordinates.Table, RenderContext)
9✔
1722
def _render(axes, context):
9✔
1723
    axes_xml = xml.SubElement(context.parent, "g", id=context.get_id(
9✔
1724
        axes), attrib={"class": "toyplot-coordinates-Table"})
1725

1726
    _render_table(owner=axes, key="data", label="table data", table=axes._cell_data, filename=axes._filename, context=context)
9✔
1727

1728
    # Render title
1729
    _draw_text(
9✔
1730
        root=axes_xml,
1731
        text=axes._label._text,
1732
        x=(axes._xmin_range + axes._xmax_range) * 0.5,
1733
        y=axes._ymin_range,
1734
        style=axes._label._style,
1735
        )
1736

1737
    # For each unique group of cells.
1738
    for cell_group in numpy.unique(axes._cell_group):
9✔
1739
        cell_selection = (axes._cell_group == cell_group)
9✔
1740

1741
        # Skip hidden groups.
1742
        cell_show = axes._cell_show[cell_selection][0]
9✔
1743
        if not cell_show:
9✔
UNCOV
1744
            continue
×
1745

1746
        # Identify the closed range of rows and columns that contain the cell.
1747
        cell_rows, cell_columns = numpy.nonzero(cell_selection)
9✔
1748
        row_min = cell_rows.min()
9✔
1749
        row_max = cell_rows.max()
9✔
1750
        column_min = cell_columns.min()
9✔
1751
        column_max = cell_columns.max()
9✔
1752

1753
        # Optionally render the cell background.
1754
        cell_style = axes._cell_style[cell_selection][0]
9✔
1755
        cell_hyperlink = axes._cell_hyperlink[cell_selection][0]
9✔
1756
        cell_title = axes._cell_title[cell_selection][0]
9✔
1757
        if cell_style is not None or cell_hyperlink is not None or cell_title is not None:
9✔
1758
            # Compute the cell boundaries.
1759
            cell_top = axes._cell_top[row_min]
9✔
1760
            cell_bottom = axes._cell_bottom[row_max]
9✔
1761
            cell_left = axes._cell_left[column_min]
9✔
1762
            cell_right = axes._cell_right[column_max]
9✔
1763

1764
            cell_parent_xml = axes_xml
9✔
1765
            if cell_hyperlink is not None:
9✔
UNCOV
1766
                cell_parent_xml = xml.SubElement(
×
1767
                    cell_parent_xml,
1768
                    "a",
1769
                    attrib={"xlink:href": cell_hyperlink},
1770
                    )
1771

1772
            cell_xml = xml.SubElement(
9✔
1773
                cell_parent_xml,
1774
                "rect",
1775
                x=str(cell_left),
1776
                y=str(cell_top),
1777
                width=str(cell_right - cell_left),
1778
                height=str(cell_bottom - cell_top),
1779
                style=_css_style({"fill":"transparent", "stroke":"none"}, cell_style),
1780
                )
1781

1782
            if cell_title is not None:
9✔
1783
                xml.SubElement(cell_xml, "title").text = str(cell_title)
9✔
1784

1785
        # Render the cell data.
1786
        cell_data = axes._cell_data[cell_selection][0]
9✔
1787
        if cell_data is not None:
9✔
1788
            # Compute the cell boundaries.
1789
            padding = 5
9✔
1790
            cell_top = axes._cell_top[row_min]
9✔
1791
            cell_bottom = axes._cell_bottom[row_max]
9✔
1792
            cell_left = axes._cell_left[column_min] + padding
9✔
1793
            cell_right = axes._cell_right[column_max] - padding
9✔
1794

1795
            # Compute the text placement within the cell boundaries.
1796
            cell_align = axes._cell_align[cell_selection][0]
9✔
1797
            if cell_align is None:
9✔
UNCOV
1798
                cell_align = "left"
×
1799
            cell_angle = axes._cell_angle[cell_selection][0]
9✔
1800
            y = (cell_top + cell_bottom) / 2
9✔
1801

1802
            # Format the cell data.
1803
            cell_format = axes._cell_format[cell_selection][0]
9✔
1804
            prefix, separator, suffix = cell_format.format(cell_data)
9✔
1805

1806
            # Get the cell style.
1807
            cell_lstyle = axes._cell_lstyle[cell_selection][0]
9✔
1808

1809
            # Render the cell data.
1810
            if cell_align == "left":
9✔
1811
                x = cell_left
9✔
1812
                _draw_text(
9✔
1813
                    root=axes_xml,
1814
                    x=x,
1815
                    y=y,
1816
                    angle=cell_angle,
1817
                    style=toyplot.style.combine(cell_lstyle, {"text-anchor": "start"}),
1818
                    text=prefix + separator + suffix,
1819
                    )
1820
            elif cell_align == "center":
9✔
1821
                x = (cell_left + cell_right) / 2
9✔
1822
                _draw_text(
9✔
1823
                    root=axes_xml,
1824
                    x=x,
1825
                    y=y,
1826
                    angle=cell_angle,
1827
                    style=toyplot.style.combine(cell_lstyle, {"text-anchor": "middle"}),
1828
                    text=prefix + separator + suffix,
1829
                    )
1830
            elif cell_align == "right":
9✔
1831
                x = cell_right
9✔
1832
                _draw_text(
9✔
1833
                    root=axes_xml,
1834
                    x=x,
1835
                    y=y,
1836
                    angle=cell_angle,
1837
                    style=toyplot.style.combine(cell_lstyle, {"text-anchor": "end"}),
1838
                    text=prefix + separator + suffix,
1839
                    )
1840
            elif cell_align == "separator":
9✔
1841
                x = (cell_left + cell_right) / 2
9✔
1842
                _draw_text(
9✔
1843
                    root=axes_xml,
1844
                    x=x - 2,
1845
                    y=y,
1846
                    style=toyplot.style.combine(cell_lstyle, {"text-anchor": "end"}),
1847
                    text=prefix,
1848
                    )
1849
                _draw_text(
9✔
1850
                    root=axes_xml,
1851
                    x=x,
1852
                    y=y,
1853
                    style=toyplot.style.combine(cell_lstyle, {"text-anchor": "middle"}),
1854
                    text=separator,
1855
                    )
1856
                _draw_text(
9✔
1857
                    root=axes_xml,
1858
                    x=x + 2,
1859
                    y=y,
1860
                    style=toyplot.style.combine(cell_lstyle, {"text-anchor": "start"}),
1861
                    text=suffix,
1862
                    )
1863

1864
    # Render children.
1865
    for child in axes._axes:
9✔
1866
        _render(child._finalize(), context.copy(parent=axes_xml))
9✔
1867

1868
    # Render grid lines.
1869
    row_boundaries = axes._row_boundaries
9✔
1870
    column_boundaries = axes._column_boundaries
9✔
1871

1872
    separation = axes._separation / 2
9✔
1873

1874
    def contiguous(a):
9✔
1875
        i = 0
9✔
1876
        result = []
9✔
1877
        for (k, g) in itertools.groupby(a.ravel()):
9✔
1878
            n = len(list(g))
9✔
1879
            if k:
9✔
1880
                result.append((i, i + n, k))
9✔
1881
            i += n
9✔
1882
        return result
9✔
1883

1884
    hlines = numpy.copy(axes._hlines)
9✔
1885
    hlines[numpy.logical_not(axes._hlines_show)] = False
9✔
1886
    for row_index, row in enumerate(hlines):
9✔
1887
        y = row_boundaries[row_index]
9✔
1888
        for start, end, line_type in contiguous(row):
9✔
1889
            if line_type == "single":
9✔
1890
                xml.SubElement(
9✔
1891
                    axes_xml,
1892
                    "line",
1893
                    x1=str(column_boundaries[start]),
1894
                    y1=str(y),
1895
                    x2=str(column_boundaries[end]),
1896
                    y2=str(y),
1897
                    style=_css_style(axes._gstyle),
1898
                    )
1899
            elif line_type == "double":
9✔
1900
                xml.SubElement(
9✔
1901
                    axes_xml,
1902
                    "line",
1903
                    x1=str(
1904
                        column_boundaries[start]),
1905
                    y1=str(
1906
                        y - separation),
1907
                    x2=str(
1908
                        column_boundaries[end]),
1909
                    y2=str(
1910
                        y - separation),
1911
                    style=_css_style(
1912
                        axes._gstyle))
1913
                xml.SubElement(
9✔
1914
                    axes_xml,
1915
                    "line",
1916
                    x1=str(
1917
                        column_boundaries[start]),
1918
                    y1=str(
1919
                        y + separation),
1920
                    x2=str(
1921
                        column_boundaries[end]),
1922
                    y2=str(
1923
                        y + separation),
1924
                    style=_css_style(
1925
                        axes._gstyle))
1926

1927
    vlines = numpy.copy(axes._vlines)
9✔
1928
    vlines[numpy.logical_not(axes._vlines_show)] = False
9✔
1929
    for column_index, column in enumerate(vlines.T):
9✔
1930
        x = column_boundaries[column_index]
9✔
1931
        for start, end, line_type in contiguous(column):
9✔
1932
            if line_type == "single":
9✔
1933
                xml.SubElement(
9✔
1934
                    axes_xml,
1935
                    "line",
1936
                    x1=str(x),
1937
                    y1=str(row_boundaries[start]),
1938
                    x2=str(x),
1939
                    y2=str(row_boundaries[end]),
1940
                    style=_css_style(axes._gstyle),
1941
                    )
1942
            elif line_type == "double":
9✔
1943
                xml.SubElement(
9✔
1944
                    axes_xml,
1945
                    "line",
1946
                    x1=str(x - separation),
1947
                    y1=str(row_boundaries[start]),
1948
                    x2=str(x - separation),
1949
                    y2=str(row_boundaries[end]),
1950
                    style=_css_style(axes._gstyle),
1951
                    )
1952
                xml.SubElement(
9✔
1953
                    axes_xml,
1954
                    "line",
1955
                    x1=str(x + separation),
1956
                    y1=str(row_boundaries[start]),
1957
                    x2=str(x + separation),
1958
                    y2=str(row_boundaries[end]),
1959
                    style=_css_style(axes._gstyle),
1960
                    )
1961

1962

1963
@dispatch(toyplot.coordinates.Cartesian, type(None), RenderContext)
9✔
1964
def _render(axes, mark, context):
9✔
UNCOV
1965
    pass
×
1966

1967

1968
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.BarBoundaries, RenderContext)
9✔
1969
def _render(axes, mark, context):
9✔
1970
    left = mark._table[mark._left[0]]
9✔
1971
    right = mark._table[mark._right[0]]
9✔
1972
    boundaries = numpy.ma.column_stack(
9✔
1973
        [mark._table[key] for key in mark._boundaries])
1974

1975
    if mark._coordinate_axes.tolist() == ["x", "y"]:
9✔
1976
        axis1 = "x"
9✔
1977
        axis2 = "y"
9✔
1978
        distance1 = "width"
9✔
1979
        distance2 = "height"
9✔
1980
        left = axes.project("x", left)
9✔
1981
        right = axes.project("x", right)
9✔
1982
        boundaries = axes.project("y", boundaries)
9✔
1983
    elif mark._coordinate_axes.tolist() == ["y", "x"]:
9✔
1984
        axis1 = "y"
9✔
1985
        axis2 = "x"
9✔
1986
        distance1 = "height"
9✔
1987
        distance2 = "width"
9✔
1988
        left = axes.project("y", left)
9✔
1989
        right = axes.project("y", right)
9✔
1990
        boundaries = axes.project("x", boundaries)
9✔
1991

1992
    mark_xml = xml.SubElement(
9✔
1993
        context.parent,
1994
        "g",
1995
        style=_css_style(
1996
            mark._style),
1997
        id=context.get_id(mark),
1998
        attrib={
1999
            "class": "toyplot-mark-BarBoundaries"})
2000

2001
    _render_table(owner=mark, key="data", label="bar data", table=mark._table, filename=mark._filename, context=context)
9✔
2002

2003
    for boundary1, boundary2, fill, opacity, title, hyperlink in zip(
9✔
2004
            boundaries.T[:-1],
2005
            boundaries.T[1:],
2006
            [mark._table[key] for key in mark._fill],
2007
            [mark._table[key] for key in mark._opacity],
2008
            [mark._table[key] for key in mark._title],
2009
            [mark._table[key] for key in mark._hyperlink],
2010
        ):
2011
        not_null = numpy.invert(
9✔
2012
            numpy.logical_or(
2013
                numpy.ma.getmaskarray(boundary1),
2014
                numpy.ma.getmaskarray(boundary2)))
2015

2016
        series_xml = xml.SubElement(
9✔
2017
            mark_xml, "g", attrib={"class": "toyplot-Series"})
2018
        for dleft, dright, dboundary1, dboundary2, dfill, dopacity, dtitle, dhyperlink in zip(
9✔
2019
                left[not_null],
2020
                right[not_null],
2021
                boundary1[not_null],
2022
                boundary2[not_null],
2023
                fill[not_null],
2024
                opacity[not_null],
2025
                title[not_null],
2026
                hyperlink[not_null],
2027
            ):
2028
            dstyle = toyplot.style.combine({
9✔
2029
                "fill": toyplot.color.to_css(dfill),
2030
                "opacity": dopacity,
2031
                }, mark._style)
2032

2033
            if dhyperlink:
9✔
UNCOV
2034
                parent_xml = xml.SubElement(series_xml, "a", attrib={"xlink:href": dhyperlink})
×
2035
            else:
2036
                parent_xml = series_xml
9✔
2037

2038
            datum_xml = xml.SubElement(
9✔
2039
                parent_xml,
2040
                "rect",
2041
                attrib={
2042
                    "class": "toyplot-Datum",
2043
                    axis1: str(min(dleft, dright)),
2044
                    axis2: str(min(dboundary1, dboundary2)),
2045
                    distance1: str(numpy.abs(dleft - dright)),
2046
                    distance2: str(numpy.abs(dboundary1 - dboundary2)),
2047
                    },
2048
                style=_css_style(dstyle),
2049
                )
2050
            if dtitle is not None:
9✔
2051
                xml.SubElement(datum_xml, "title").text = str(dtitle)
9✔
2052

2053

2054
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.BarMagnitudes, RenderContext)
9✔
2055
def _render(axes, mark, context):
9✔
2056
    left = mark._table[mark._left[0]]
9✔
2057
    right = mark._table[mark._right[0]]
9✔
2058
    boundaries = numpy.ma.cumsum(numpy.ma.column_stack(
9✔
2059
        [mark._table[mark._baseline[0]]] + [mark._table[key] for key in mark._magnitudes]), axis=1)
2060
    not_null = numpy.invert(
9✔
2061
        numpy.ma.any(numpy.ma.getmaskarray(boundaries), axis=1))
2062

2063
    if mark._coordinate_axes.tolist() == ["x", "y"]:
9✔
2064
        axis1 = "x"
9✔
2065
        axis2 = "y"
9✔
2066
        distance1 = "width"
9✔
2067
        distance2 = "height"
9✔
2068
        left = axes.project("x", left)
9✔
2069
        right = axes.project("x", right)
9✔
2070
        boundaries = axes.project("y", boundaries)
9✔
2071
    elif mark._coordinate_axes.tolist() == ["y", "x"]:
9✔
2072
        axis1 = "y"
9✔
2073
        axis2 = "x"
9✔
2074
        distance1 = "height"
9✔
2075
        distance2 = "width"
9✔
2076
        left = axes.project("y", left)
9✔
2077
        right = axes.project("y", right)
9✔
2078
        boundaries = axes.project("x", boundaries)
9✔
2079

2080
    mark_xml = xml.SubElement(
9✔
2081
        context.parent,
2082
        "g",
2083
        style=_css_style(
2084
            mark._style),
2085
        id=context.get_id(mark),
2086
        attrib={
2087
            "class": "toyplot-mark-BarMagnitudes"})
2088

2089
    _render_table(owner=mark, key="data", label="bar data", table=mark._table, filename=mark._filename, context=context)
9✔
2090

2091
    for boundary1, boundary2, fill, opacity, title, hyperlink in zip(
9✔
2092
            boundaries.T[:-1],
2093
            boundaries.T[1:],
2094
            [mark._table[key] for key in mark._fill],
2095
            [mark._table[key] for key in mark._opacity],
2096
            [mark._table[key] for key in mark._title],
2097
            [mark._table[key] for key in mark._hyperlink],
2098
        ):
2099
        series_xml = xml.SubElement(
9✔
2100
            mark_xml, "g", attrib={"class": "toyplot-Series"})
2101
        for dleft, dright, dboundary1, dboundary2, dfill, dopacity, dtitle, dhyperlink in zip(
9✔
2102
                left[not_null],
2103
                right[not_null],
2104
                boundary1[not_null],
2105
                boundary2[not_null],
2106
                fill[not_null],
2107
                opacity[not_null],
2108
                title[not_null],
2109
                hyperlink[not_null],
2110
            ):
2111
            dstyle = toyplot.style.combine(
9✔
2112
                {"fill": toyplot.color.to_css(dfill), "opacity": dopacity}, mark._style)
2113

2114
            if dhyperlink:
9✔
UNCOV
2115
                parent_xml = xml.SubElement(series_xml, "a", attrib={"xlink:href": dhyperlink})
×
2116
            else:
2117
                parent_xml = series_xml
9✔
2118

2119
            datum_xml = xml.SubElement(
9✔
2120
                parent_xml,
2121
                "rect",
2122
                attrib={
2123
                    "class": "toyplot-Datum",
2124
                    axis1: str(min(dleft, dright)),
2125
                    axis2: str(min(dboundary1, dboundary2)),
2126
                    distance1: str(numpy.abs(dleft - dright)),
2127
                    distance2: str(numpy.abs(dboundary1 - dboundary2)),
2128
                    },
2129
                style=_css_style(dstyle),
2130
                )
2131
            if dtitle is not None:
9✔
2132
                xml.SubElement(datum_xml, "title").text = str(dtitle)
9✔
2133

2134

2135
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.FillBoundaries, RenderContext)
9✔
2136
def _render(axes, mark, context):
9✔
2137
    boundaries = numpy.ma.column_stack(
9✔
2138
        [mark._table[key] for key in mark._boundaries])
2139

2140
    if mark._coordinate_axes.tolist() == ["x", "y"]:
9✔
2141
        position = axes.project("x", mark._table[mark._position[0]])
9✔
2142
        boundaries = axes.project("y", boundaries)
9✔
2143
    elif mark._coordinate_axes.tolist() == ["y", "x"]:
9✔
2144
        position = axes.project("y", mark._table[mark._position[0]])
9✔
2145
        boundaries = axes.project("x", boundaries)
9✔
2146

2147
    mark_xml = xml.SubElement(
9✔
2148
        context.parent,
2149
        "g",
2150
        style=_css_style(
2151
            mark._style),
2152
        id=context.get_id(mark),
2153
        attrib={
2154
            "class": "toyplot-mark-FillBoundaries"})
2155

2156
    _render_table(owner=mark, key="data", label="fill data", table=mark._table, filename=mark._filename, context=context)
9✔
2157

2158
    for boundary1, boundary2, fill, opacity, title in zip(
9✔
2159
            boundaries.T[:-1], boundaries.T[1:], mark._fill, mark._opacity, mark._title):
2160
        not_null = numpy.invert(
9✔
2161
            numpy.logical_or(
2162
                numpy.ma.getmaskarray(boundary1),
2163
                numpy.ma.getmaskarray(boundary2)))
2164
        segments = _flat_contiguous(not_null)
9✔
2165

2166
        series_style = toyplot.style.combine(
9✔
2167
            {"fill": toyplot.color.to_css(fill), "opacity": opacity}, mark._style)
2168

2169
        for segment in segments:
9✔
2170
            if mark._coordinate_axes[0] == "x":
9✔
2171
                coordinates = zip(
9✔
2172
                    numpy.concatenate((position[segment], position[segment][::-1])),
2173
                    numpy.concatenate((boundary1[segment], boundary2[segment][::-1])))
2174
            elif mark._coordinate_axes[0] == "y":
9✔
2175
                coordinates = zip(
9✔
2176
                    numpy.concatenate((boundary1[segment], boundary2[segment][::-1])),
2177
                    numpy.concatenate((position[segment], position[segment][::-1])))
2178
            series_xml = xml.SubElement(mark_xml, "polygon", points=" ".join(
9✔
2179
                ["%s,%s" % (xi, yi) for xi, yi in coordinates]), style=_css_style(series_style))
2180
            if title is not None:
9✔
2181
                xml.SubElement(series_xml, "title").text = str(title)
9✔
2182

2183

2184
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.FillMagnitudes, RenderContext)
9✔
2185
def _render(axes, mark, context):
9✔
2186
    magnitudes = numpy.ma.column_stack(
9✔
2187
        [mark._table[mark._baseline[0]]] + [mark._table[key] for key in mark._magnitudes])
2188
    boundaries = numpy.ma.cumsum(magnitudes, axis=1)
9✔
2189
    not_null = numpy.invert(
9✔
2190
        numpy.ma.any(numpy.ma.getmaskarray(boundaries), axis=1))
2191
    segments = _flat_contiguous(not_null)
9✔
2192

2193
    if mark._coordinate_axes.tolist() == ["x", "y"]:
9✔
2194
        position = axes.project("x", mark._table[mark._position[0]])
9✔
2195
        boundaries = axes.project("y", boundaries)
9✔
2196
    elif mark._coordinate_axes.tolist() == ["y", "x"]:
9✔
2197
        position = axes.project("y", mark._table[mark._position[0]])
9✔
2198
        boundaries = axes.project("x", boundaries)
9✔
2199

2200
    mark_xml = xml.SubElement(
9✔
2201
        context.parent,
2202
        "g",
2203
        style=_css_style(
2204
            mark._style),
2205
        id=context.get_id(mark),
2206
        attrib={
2207
            "class": "toyplot-mark-FillMagnitudes"})
2208

2209
    _render_table(owner=mark, key="data", label="fill data", table=mark._table, filename=mark._filename, context=context)
9✔
2210

2211
    for boundary1, boundary2, fill, opacity, title in zip(
9✔
2212
            boundaries.T[:-1], boundaries.T[1:], mark._fill, mark._opacity, mark._title):
2213
        series_style = toyplot.style.combine(
9✔
2214
            {"fill": toyplot.color.to_css(fill), "opacity": opacity}, mark._style)
2215
        for segment in segments:
9✔
2216
            if mark._coordinate_axes[0] == "x":
9✔
2217
                coordinates = zip(
9✔
2218
                    numpy.concatenate((position[segment], position[segment][::-1])),
2219
                    numpy.concatenate((boundary1[segment], boundary2[segment][::-1])))
2220
            elif mark._coordinate_axes[0] == "y":
9✔
2221
                coordinates = zip(
9✔
2222
                    numpy.concatenate((boundary1[segment], boundary2[segment][::-1])),
2223
                    numpy.concatenate((position[segment], position[segment][::-1])))
2224
            series_xml = xml.SubElement(mark_xml, "polygon", points=" ".join(
9✔
2225
                ["%s,%s" % (xi, yi) for xi, yi in coordinates]), style=_css_style(series_style))
2226
            if title is not None:
9✔
2227
                xml.SubElement(series_xml, "title").text = str(title)
9✔
2228

2229

2230
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.AxisLines, RenderContext)
9✔
2231
def _render(axes, mark, context):
9✔
2232
    if mark._coordinate_axes[0] == "x":
9✔
2233
        p1 = "x1"
9✔
2234
        p2 = "x2"
9✔
2235
        b1 = "y1"
9✔
2236
        b2 = "y2"
9✔
2237
        position = axes.project("x", mark._table[mark._coordinates[0]])
9✔
2238
        boundary1 = axes._ymin_range
9✔
2239
        boundary2 = axes._ymax_range
9✔
2240
    elif mark._coordinate_axes[0] == "y":
9✔
2241
        p1 = "y1"
9✔
2242
        p2 = "y2"
9✔
2243
        b1 = "x1"
9✔
2244
        b2 = "x2"
9✔
2245
        position = axes.project("y", mark._table[mark._coordinates[0]])
9✔
2246
        boundary1 = axes._xmin_range
9✔
2247
        boundary2 = axes._xmax_range
9✔
2248
    mark_xml = xml.SubElement(
9✔
2249
        context.parent,
2250
        "g",
2251
        style=_css_style(
2252
            mark._style),
2253
        id=context.get_id(mark),
2254
        attrib={
2255
            "class": "toyplot-mark-AxisLines"})
2256
    series_xml = xml.SubElement(
9✔
2257
        mark_xml, "g", attrib={"class": "toyplot-Series"})
2258
    for dposition, dstroke, dopacity, dtitle in zip(
9✔
2259
            position,
2260
            mark._table[mark._stroke[0]],
2261
            mark._table[mark._opacity[0]],
2262
            mark._table[mark._title[0]],
2263
        ):
2264
        dstyle = toyplot.style.combine(
9✔
2265
            {"stroke": toyplot.color.to_css(dstroke), "opacity": dopacity}, mark._style)
2266
        datum_xml = xml.SubElement(
9✔
2267
            series_xml,
2268
            "line",
2269
            attrib={
2270
                "class": "toyplot-Datum",
2271
                p1: str(dposition),
2272
                p2: str(dposition),
2273
                b1: str(boundary1),
2274
                b2: str(boundary2),
2275
            },
2276
            style=_css_style(dstyle),
2277
        )
2278
        if dtitle is not None:
9✔
2279
            xml.SubElement(datum_xml, "title").text = str(dtitle)
9✔
2280

2281

2282
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.Ellipse, RenderContext)
9✔
2283
def _render(axes, mark, context):
9✔
2284
    assert(mark._coordinate_axes.tolist() == ["x", "y"])
9✔
2285

2286
    mark_xml = xml.SubElement(
9✔
2287
        context.parent,
2288
        "g",
2289
        style=_css_style(
2290
            mark._style),
2291
        id=context.get_id(mark),
2292
        attrib={
2293
            "class": "toyplot-mark-Ellipse"})
2294

2295
    _render_table(owner=mark, key="data", label="ellipse data", table=mark._table, filename=mark._filename, context=context)
9✔
2296

2297
    series_xml = xml.SubElement(
9✔
2298
        mark_xml, "g", attrib={"class": "toyplot-Series"})
2299
    for dx, dy, drx, dry, dangle, dfill, dopacity, dtitle in zip(
9✔
2300
            mark._table[mark._x[0]],
2301
            mark._table[mark._y[0]],
2302
            mark._table[mark._rx[0]],
2303
            mark._table[mark._ry[0]],
2304
            mark._table[mark._angle[0]],
2305
            mark._table[mark._fill[0]],
2306
            mark._table[mark._opacity[0]],
2307
            mark._table[mark._title[0]],
2308
        ):
2309
        dstyle = toyplot.style.combine(
9✔
2310
            {"fill": toyplot.color.to_css(dfill), "opacity": dopacity}, mark._style)
2311

2312
        theta = numpy.radians(dangle)
9✔
2313
        u = numpy.array((numpy.cos(theta), numpy.sin(theta))) * drx
9✔
2314
        v = numpy.array((numpy.cos(theta + numpy.pi / 2), numpy.sin(theta + numpy.pi / 2))) * dry
9✔
2315

2316
        mix = numpy.linspace(0, numpy.pi * 2, 100)
9✔
2317
        p = numpy.cos(mix)[:,None] * u + numpy.sin(mix)[:,None] * v
9✔
2318

2319
        x = axes.project("x", dx + p[:,0])
9✔
2320
        y = axes.project("y", dy + p[:,1])
9✔
2321

2322
        points = ["%s,%s" % point for point in zip(x, y)]
9✔
2323

2324
        datum_xml = xml.SubElement(
9✔
2325
            series_xml,
2326
            "polygon",
2327
            attrib={"class": "toyplot-Datum"},
2328
            points=" ".join(points),
2329
            style=_css_style(dstyle),
2330
            )
2331

2332
        if dtitle is not None:
9✔
2333
            xml.SubElement(datum_xml, "title").text = str(dtitle)
9✔
2334

2335

2336
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.Graph, RenderContext)
2337
def _render(axes, mark, context): # pragma: no cover
2338
    # Project edge coordinates.
2339
    for i in range(2):
2340
        if mark._coordinate_axes[i] == "x":
2341
            edge_x = axes.project("x", mark._ecoordinates.T[i])
2342
        elif mark._coordinate_axes[i] == "y":
2343
            edge_y = axes.project("y", mark._ecoordinates.T[i])
2344
    edge_coordinates = numpy.column_stack((edge_x, edge_y))
2345

2346
    # Project vertex coordinates.
2347
    for i in range(2):
2348
        if mark._coordinate_axes[i] == "x":
2349
            vertex_x = axes.project("x", mark._vtable[mark._vcoordinates[i]])
2350
        elif mark._coordinate_axes[i] == "y":
2351
            vertex_y = axes.project("y", mark._vtable[mark._vcoordinates[i]])
2352

2353
    # Create final vertex markers.
2354
    vertex_markers = []
2355
    for vmarker, vsize, vcolor, vopacity in zip(
2356
            mark._vtable[mark._vmarker[0]],
2357
            mark._vtable[mark._vsize[0]],
2358
            mark._vtable[mark._vcolor[0]],
2359
            mark._vtable[mark._vopacity[0]],
2360
        ):
2361
        if vmarker:
2362
            vstyle = toyplot.style.combine(
2363
                {
2364
                    "fill": toyplot.color.to_css(vcolor),
2365
                    "stroke": toyplot.color.to_css(vcolor),
2366
                    "opacity": vopacity,
2367
                },
2368
                mark._vstyle)
2369
            vertex_marker = toyplot.marker.create(size=vsize, mstyle=vstyle, lstyle=mark._vlstyle) + toyplot.marker.convert(vmarker)
2370
            vertex_markers.append(vertex_marker)
2371
        else:
2372
            vertex_markers.append(None)
2373

2374
    # Create final edge styles.
2375
    edge_styles = []
2376
    for ecolor, ewidth, eopacity in zip(
2377
            mark._etable[mark._ecolor[0]],
2378
            mark._etable[mark._ewidth[0]],
2379
            mark._etable[mark._eopacity[0]],
2380
        ):
2381
        edge_styles.append(
2382
            toyplot.style.combine(
2383
                {
2384
                    "fill": "none",
2385
                    "stroke": toyplot.color.to_css(ecolor),
2386
                    "stroke-width": ewidth,
2387
                    "stroke-opacity": eopacity,
2388
                },
2389
                mark._estyle,
2390
            )
2391
        )
2392

2393
    edge_marker_styles = []
2394
    for ecolor, estyle in zip(
2395
            mark._etable[mark._ecolor[0]],
2396
            edge_styles,
2397
            ):
2398
        edge_marker_styles.append(toyplot.style.combine(estyle, {"fill": toyplot.color.to_css(ecolor)}))
2399

2400
    # Identify ranges of edge coordinates for each edge.
2401
    index = 0
2402
    edge_start = []
2403
    edge_end = []
2404
    for eshape in mark._etable[mark._eshape[0]]:
2405
        edge_start.append(index)
2406
        for segment in eshape:
2407
            if segment == "M":
2408
                count = 1
2409
            elif segment == "L":
2410
                count = 1
2411
            elif segment == "Q":
2412
                count = 2
2413
            elif segment == "C":
2414
                count = 3
2415
            index += count
2416
        edge_end.append(index)
2417

2418
    # Adjust edge coordinates so edges don't overlap vertex markers.
2419
    for esource, etarget, start, end in zip(
2420
            mark._etable[mark._esource[0]],
2421
            mark._etable[mark._etarget[0]],
2422
            edge_start,
2423
            edge_end,
2424
        ):
2425

2426
        # Skip loop edges.
2427
        if esource == etarget:
2428
            continue
2429

2430
        source_vertex_marker = vertex_markers[esource]
2431
        target_vertex_marker = vertex_markers[etarget]
2432

2433
        if source_vertex_marker:
2434
            dp = source_vertex_marker.intersect(edge_coordinates[start + 1] - edge_coordinates[start])
2435
            edge_coordinates[start] += dp
2436

2437
        if target_vertex_marker:
2438
            dp = target_vertex_marker.intersect(edge_coordinates[end - 2] - edge_coordinates[end - 1])
2439
            edge_coordinates[end - 1] += dp
2440

2441
    # Render the graph.
2442
    mark_xml = xml.SubElement(context.parent, "g", id=context.get_id(mark), attrib={"class": "toyplot-mark-Graph"})
2443
    _render_table(owner=mark, key="vertex_data", label="graph vertex data", table=mark._vtable, filename=mark._vfilename, context=context)
2444
    _render_table(owner=mark, key="edge_data", label="graph edge data", table=mark._etable, filename=mark._efilename, context=context)
2445

2446
    # Render edges.
2447
    edge_xml = xml.SubElement(mark_xml, "g", attrib={"class": "toyplot-Edges"})
2448
    for esource, etarget, eshape, estyle, hmarker, mmarker, mposition, tmarker, start, end in zip(
2449
            mark._etable[mark._esource[0]],
2450
            mark._etable[mark._etarget[0]],
2451
            mark._etable[mark._eshape[0]],
2452
            edge_styles,
2453
            mark._etable[mark._hmarker[0]],
2454
            mark._etable[mark._mmarker[0]],
2455
            mark._etable[mark._mposition[0]],
2456
            mark._etable[mark._tmarker[0]],
2457
            edge_start,
2458
            edge_end,
2459
        ):
2460

2461
        path = []
2462
        index = 0
2463
        for segment in eshape:
2464
            if segment == "M":
2465
                count = 1
2466
            elif segment == "L":
2467
                count = 1
2468
            elif segment == "Q":
2469
                count = 2
2470
            elif segment == "C":
2471
                count = 3
2472
            path.append(segment)
2473
            for _ in range(count):
2474
                path.append(str(edge_coordinates[start + index][0]))
2475
                path.append(str(edge_coordinates[start + index][1]))
2476
                index += 1
2477

2478
        xml.SubElement(
2479
            edge_xml,
2480
            "path",
2481
            d=" ".join(path),
2482
            style=_css_style(estyle),
2483
            )
2484

2485
    # Render edge head markers.
2486
    marker_xml = xml.SubElement(edge_xml, "g", attrib={"class": "toyplot-HeadMarkers"})
2487
    for marker, mstyle, estart, eend in zip(
2488
            mark._etable[mark._hmarker[0]],
2489
            edge_marker_styles,
2490
            edge_start,
2491
            edge_end,
2492
        ):
2493
        if marker:
2494
            # Create the marker with defaults.
2495
            marker = toyplot.marker.create(size=10, mstyle=mstyle) + toyplot.marker.convert(marker)
2496

2497
            # Compute the marker angle using the first edge segment.
2498
            edge_angle = -numpy.rad2deg(numpy.arctan2(
2499
                edge_coordinates[estart+1][1] - edge_coordinates[estart][1],
2500
                edge_coordinates[estart+1][0] - edge_coordinates[estart][0],
2501
                ))
2502

2503
            transform = "translate(%s, %s)" % (edge_coordinates[estart][0], edge_coordinates[estart][1])
2504
            if edge_angle:
2505
                transform += " rotate(%s)" % (-edge_angle,)
2506
            transform += " translate(%s, 0)" % (marker.size / 2,)
2507
            if marker.angle is not None:
2508
                if isinstance(marker.angle, str) and marker.angle[0:1] == "r":
2509
                    angle = as_float(marker.angle[1:])
2510
                else:
2511
                    angle = -edge_angle + as_float(marker.angle)
2512
                transform += " rotate(%s)" % (-angle,)
2513

2514

2515
            _draw_marker(
2516
                marker_xml,
2517
                marker=marker,
2518
                transform=transform,
2519
                )
2520

2521
    # Render edge middle markers.
2522
    marker_xml = xml.SubElement(edge_xml, "g", attrib={"class": "toyplot-MiddleMarkers"})
2523
    for mstyle, marker, mposition, start, end in zip(
2524
            edge_marker_styles,
2525
            mark._etable[mark._mmarker[0]],
2526
            mark._etable[mark._mposition[0]],
2527
            edge_start,
2528
            edge_end,
2529
        ):
2530
        if marker:
2531
            # Create the marker with defaults.
2532
            marker = toyplot.marker.create(size=10, mstyle=mstyle) + toyplot.marker.convert(marker)
2533

2534
            # Place the marker within the first edge segment.
2535
            x, y = edge_coordinates[start] * (1 - mposition) + edge_coordinates[start+1] * mposition
2536

2537
            # Compute the marker angle using the first edge segment.
2538
            angle = -numpy.rad2deg(numpy.arctan2(
2539
                edge_coordinates[start+1][1] - edge_coordinates[start][1],
2540
                edge_coordinates[start+1][0] - edge_coordinates[start][0],
2541
                ))
2542
            if marker.angle is not None:
2543
                if isinstance(marker.angle, str) and marker.angle[0:1] == "r":
2544
                    angle += as_float(marker.angle[1:])
2545
                else:
2546
                    angle = as_float(marker.angle)
2547

2548
            marker = marker + toyplot.marker.create(angle=angle)
2549

2550
            _draw_marker(
2551
                marker_xml,
2552
                cx=x,
2553
                cy=y,
2554
                marker=marker,
2555
                )
2556

2557
    # Render edge tail markers.
2558
    marker_xml = xml.SubElement(edge_xml, "g", attrib={"class": "toyplot-TailMarkers"})
2559
    for mstyle, marker, start, end in zip(
2560
            edge_marker_styles,
2561
            mark._etable[mark._tmarker[0]],
2562
            edge_start,
2563
            edge_end,
2564
        ):
2565
        if marker:
2566
            # Create the marker with defaults.
2567
            marker = toyplot.marker.create(size=10, mstyle=mstyle, lstyle={}) + toyplot.marker.convert(marker)
2568

2569
            # Compute the marker angle using the last edge segment.
2570
            edge_angle = -numpy.rad2deg(numpy.arctan2(
2571
                edge_coordinates[end-1][1] - edge_coordinates[end-2][1],
2572
                edge_coordinates[end-1][0] - edge_coordinates[end-2][0],
2573
                ))
2574

2575
            transform = "translate(%s, %s)" % (edge_coordinates[end-1][0], edge_coordinates[end-1][1])
2576
            if edge_angle:
2577
                transform += " rotate(%s)" % (-edge_angle,)
2578
            transform += " translate(%s, 0)" % (-marker.size / 2,)
2579
            if marker.angle is not None:
2580
                if isinstance(marker.angle, str) and marker.angle[0:1] == "r":
2581
                    angle = as_float(marker.angle[1:])
2582
                else:
2583
                    angle = -edge_angle + as_float(marker.angle)
2584
                transform += " rotate(%s)" % (-angle,)
2585

2586

2587
            _draw_marker(
2588
                marker_xml,
2589
                marker=marker,
2590
                transform=transform,
2591
                )
2592

2593
    # Render vertex markers
2594
    vertex_xml = xml.SubElement(mark_xml, "g", attrib={"class": "toyplot-Vertices"})
2595
    for vx, vy, vmarker, vtitle in zip(
2596
            vertex_x,
2597
            vertex_y,
2598
            vertex_markers,
2599
            mark._vtable[mark._vtitle[0]],
2600
        ):
2601
        if vmarker:
2602
            _draw_marker(
2603
                vertex_xml,
2604
                cx=vx,
2605
                cy=vy,
2606
                marker=vmarker,
2607
                extra_class="toyplot-Datum",
2608
                title=vtitle,
2609
                )
2610

2611
    # Render vertex labels
2612
    if mark._vlshow:
2613
        vlabel_xml = xml.SubElement(mark_xml, "g", attrib={"class": "toyplot-Labels"})
2614
        for dx, dy, dtext in zip(vertex_x, vertex_y, mark._vtable[mark._vlabel[0]]):
2615
            _draw_text(
2616
                root=vlabel_xml,
2617
                text=str(dtext),
2618
                x=dx,
2619
                y=dy,
2620
                style=mark._vlstyle,
2621
                attributes={"class": "toyplot-Datum"},
2622
                )
2623

2624

2625
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.Plot, RenderContext)
9✔
2626
def _render(axes, mark, context):
9✔
2627
    position = mark._table[mark._coordinates[0]]
9✔
2628
    series = numpy.ma.column_stack([mark._table[key] for key in mark._series])
9✔
2629

2630
    if mark._coordinate_axes[0] == "x":
9✔
2631
        position = axes.project("x", position)
9✔
2632
        series = axes.project("y", series)
9✔
2633
    elif mark._coordinate_axes[0] == "y":
9✔
2634
        position = axes.project("y", position)
9✔
2635
        series = axes.project("x", series)
9✔
2636

2637
    mark_xml = xml.SubElement(
9✔
2638
        context.parent,
2639
        "g",
2640
        style=_css_style(toyplot.style.combine({"fill":"none"}, mark._style)),
2641
        id=context.get_id(mark),
2642
        attrib={
2643
            "class": "toyplot-mark-Plot"})
2644

2645
    _render_table(owner=mark, key="data", label="plot data", table=mark._table, filename=mark._filename, context=context)
9✔
2646

2647
    for series, stroke, stroke_width, stroke_opacity, stroke_title, marker, msize, mfill, mstroke, mopacity, mtitle in zip(
9✔
2648
            series.T,
2649
            mark._stroke.T,
2650
            mark._stroke_width.T,
2651
            mark._stroke_opacity.T,
2652
            mark._stroke_title.T,
2653
            [mark._table[key] for key in mark._marker],
2654
            [mark._table[key] for key in mark._msize],
2655
            [mark._table[key] for key in mark._mfill],
2656
            [mark._table[key] for key in mark._mstroke],
2657
            [mark._table[key] for key in mark._mopacity],
2658
            [mark._table[key] for key in mark._mtitle],
2659
        ):
2660
        not_null = numpy.invert(numpy.logical_or(
9✔
2661
            numpy.ma.getmaskarray(position), numpy.ma.getmaskarray(series)))
2662
        segments = _flat_contiguous(not_null)
9✔
2663

2664
        stroke_style = toyplot.style.combine(
9✔
2665
            {
2666
                "stroke": toyplot.color.to_css(stroke),
2667
                "stroke-width": stroke_width,
2668
                "stroke-opacity": stroke_opacity},
2669
            mark._style)
2670
        if mark._coordinate_axes[0] == "x":
9✔
2671
            x = position
9✔
2672
            y = series
9✔
2673
        elif mark._coordinate_axes[0] == "y":
9✔
2674
            x = series
9✔
2675
            y = position
9✔
2676
        series_xml = xml.SubElement(
9✔
2677
            mark_xml, "g", attrib={"class": "toyplot-Series"})
2678
        if stroke_title is not None:
9✔
2679
            xml.SubElement(series_xml, "title").text = str(stroke_title)
9✔
2680

2681
        d = []
9✔
2682
        for segment in segments:
9✔
2683
            start, stop, step = segment.indices(len(not_null))
9✔
2684
            for i in range(start, start + 1):
9✔
2685
                d.append("M %s %s" % (x[i], y[i]))
9✔
2686
            for i in range(start + 1, stop):
9✔
2687
                d.append("L %s %s" % (x[i], y[i]))
9✔
2688
        xml.SubElement(
9✔
2689
            series_xml,
2690
            "path",
2691
            d=" ".join(d),
2692
            style=_css_style(stroke_style))
2693
        for dx, dy, dmarker, dsize, dfill, dstroke, dopacity, dtitle in zip(
9✔
2694
                x[not_null],
2695
                y[not_null],
2696
                marker[not_null],
2697
                msize[not_null],
2698
                mfill[not_null],
2699
                mstroke[not_null],
2700
                mopacity[not_null],
2701
                mtitle[not_null],
2702
            ):
2703
            if dmarker:
9✔
2704
                dstyle = toyplot.style.combine(
9✔
2705
                    {
2706
                        "fill": toyplot.color.to_css(dfill),
2707
                        "stroke": toyplot.color.to_css(dstroke),
2708
                        "opacity": dopacity},
2709
                    mark._mstyle)
2710
                _draw_marker(
9✔
2711
                    series_xml,
2712
                    cx=dx,
2713
                    cy=dy,
2714
                    marker=toyplot.marker.create(size=dsize, mstyle=dstyle, lstyle=mark._mlstyle) + toyplot.marker.convert(dmarker),
2715
                    extra_class="toyplot-Datum",
2716
                    title=dtitle,
2717
                    )
2718

2719

2720
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.Range, RenderContext)
9✔
2721
def _render(axes, mark, context):
9✔
2722
    if mark._coordinate_axes[0] == "x":
9✔
2723
        x1 = axes.project("x", mark._table[mark._coordinates[0]])
9✔
2724
        x2 = axes.project("x", mark._table[mark._coordinates[1]])
9✔
2725
        y1 = axes.project("y", mark._table[mark._coordinates[2]])
9✔
2726
        y2 = axes.project("y", mark._table[mark._coordinates[3]])
9✔
2727
    elif mark._coordinate_axes[0] == "y":
9✔
2728
        x1 = axes.project("x", mark._table[mark._coordinates[2]])
9✔
2729
        x2 = axes.project("x", mark._table[mark._coordinates[3]])
9✔
2730
        y1 = axes.project("y", mark._table[mark._coordinates[0]])
9✔
2731
        y2 = axes.project("y", mark._table[mark._coordinates[1]])
9✔
2732
    mark_xml = xml.SubElement(context.parent, "g", style=_css_style(mark._style), id=context.get_id(mark), attrib={"class": "toyplot-mark-Range"})
9✔
2733

2734
    _render_table(owner=mark, key="data", label="rect data", table=mark._table, filename=mark._filename, context=context)
9✔
2735

2736
    series_xml = xml.SubElement(
9✔
2737
        mark_xml, "g", attrib={"class": "toyplot-Series"})
2738
    for dx1, dx2, dy1, dy2, dfill, dopacity, dtitle in zip(
9✔
2739
            x1,
2740
            x2,
2741
            y1,
2742
            y2,
2743
            mark._table[mark._fill[0]],
2744
            mark._table[mark._opacity[0]],
2745
            mark._table[mark._title[0]],
2746
        ):
2747
        dstyle = toyplot.style.combine({"fill": toyplot.color.to_css(dfill), "opacity": dopacity}, mark._style)
9✔
2748
        datum_xml = xml.SubElement(
9✔
2749
            series_xml,
2750
            "rect",
2751
            attrib={"class": "toyplot-Datum"},
2752
            x=str(min(dx1, dx2)),
2753
            y=str(min(dy1, dy2)),
2754
            width=str(numpy.abs(dx1 - dx2)),
2755
            height=str(numpy.abs(dy1 - dy2)),
2756
            style=_css_style(dstyle),
2757
            )
2758
        if dtitle is not None:
9✔
2759
            xml.SubElement(datum_xml, "title").text = str(dtitle)
9✔
2760

2761

2762
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.Point, RenderContext)
9✔
2763
def _render(axes, mark, context):
9✔
2764
    dimension1 = numpy.ma.column_stack([mark._table[key] for key in mark._coordinates[0::2]])
9✔
2765
    dimension2 = numpy.ma.column_stack([mark._table[key] for key in mark._coordinates[1::2]])
9✔
2766

2767
    if mark._coordinate_axes[0] == "x":
9✔
2768
        X = axes.project("x", dimension1)
9✔
2769
        Y = axes.project("y", dimension2)
9✔
2770
    elif mark._coordinate_axes[0] == "y":
9✔
2771
        X = axes.project("x", dimension2)
9✔
2772
        Y = axes.project("y", dimension1)
9✔
2773

2774
    mark_xml = xml.SubElement(
9✔
2775
        context.parent,
2776
        "g",
2777
        id=context.get_id(mark),
2778
        attrib={"class": "toyplot-mark-Point"},
2779
        )
2780

2781
    _render_table(owner=mark, key="data", label="point", table=mark._table, filename=mark._filename, context=context)
9✔
2782

2783
    for x, y, marker, msize, mfill, mstroke, mopacity, mtitle, mhyperlink in zip(
9✔
2784
            X.T,
2785
            Y.T,
2786
            [mark._table[key] for key in mark._marker],
2787
            [mark._table[key] for key in mark._msize],
2788
            [mark._table[key] for key in mark._mfill],
2789
            [mark._table[key] for key in mark._mstroke],
2790
            [mark._table[key] for key in mark._mopacity],
2791
            [mark._table[key] for key in mark._mtitle],
2792
            [mark._table[key] for key in mark._mhyperlink],
2793
        ):
2794
        not_null = numpy.invert(numpy.logical_or(
9✔
2795
            numpy.ma.getmaskarray(x), numpy.ma.getmaskarray(y)))
2796

2797
        series_xml = xml.SubElement(
9✔
2798
            mark_xml, "g", attrib={"class": "toyplot-Series"})
2799
        for dx, dy, dmarker, dsize, dfill, dstroke, dopacity, dtitle, dhyperlink in zip(
9✔
2800
                x[not_null],
2801
                y[not_null],
2802
                marker[not_null],
2803
                msize[not_null],
2804
                mfill[not_null],
2805
                mstroke[not_null],
2806
                mopacity[not_null],
2807
                mtitle[not_null],
2808
                mhyperlink[not_null],
2809
            ):
2810
            if dmarker:
9✔
2811
                if dhyperlink:
9✔
UNCOV
2812
                    datum_xml = xml.SubElement(series_xml, "a", attrib={"xlink:href": dhyperlink})
×
2813
                else:
2814
                    datum_xml = series_xml
9✔
2815

2816
                dstyle = toyplot.style.combine(
9✔
2817
                    {
2818
                        "fill": toyplot.color.to_css(dfill),
2819
                        "stroke": toyplot.color.to_css(dstroke),
2820
                        "opacity": dopacity,
2821
                    },
2822
                    mark._mstyle)
2823
                _draw_marker(
9✔
2824
                    datum_xml,
2825
                    cx=dx,
2826
                    cy=dy,
2827
                    marker=toyplot.marker.create(size=dsize, mstyle=dstyle, lstyle=mark._mlstyle) + toyplot.marker.convert(dmarker),
2828
                    extra_class="toyplot-Datum",
2829
                    title=dtitle,
2830
                    )
2831

2832

2833
@dispatch(toyplot.mark.Text, RenderContext)
9✔
2834
def _render(mark, context):
9✔
2835
    x = mark._table[mark._coordinates[numpy.where(mark._coordinate_axes == "x")[0][0]]]
9✔
2836
    y = mark._table[mark._coordinates[numpy.where(mark._coordinate_axes == "y")[0][0]]]
9✔
2837

2838
#    if isinstance(parent, toyplot.coordinates.Cartesian):
2839
#        x = parent.project("x", x)
2840
#        y = parent.project("y", y)
2841

2842
    mark_xml = xml.SubElement(
9✔
2843
        context.parent,
2844
        "g",
2845
        id=context.get_id(mark),
2846
        attrib={"class": "toyplot-mark-Text"},
2847
        )
2848

2849
    _render_table(owner=mark, key="data", label="text data", table=mark._table, filename=mark._filename, context=context)
9✔
2850

2851
    series_xml = xml.SubElement(
9✔
2852
        mark_xml, "g", attrib={"class": "toyplot-Series"})
2853
    for dx, dy, dtext, dangle, dfill, dopacity, dtitle in zip(
9✔
2854
            x,
2855
            y,
2856
            mark._table[mark._text[0]],
2857
            mark._table[mark._angle[0]],
2858
            mark._table[mark._fill[0]],
2859
            mark._table[mark._opacity[0]],
2860
            mark._table[mark._title[0]],
2861
        ):
2862

2863
        _draw_text(
9✔
2864
            root=series_xml,
2865
            text=str(dtext),
2866
            x=dx,
2867
            y=dy,
2868
            angle=dangle,
2869
            attributes={"class": "toyplot-Datum"},
2870
            style=toyplot.style.combine({"fill": toyplot.color.to_css(dfill), "opacity": dopacity}, mark._style),
2871
            title=dtitle,
2872
            )
2873

2874

2875

2876
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.Text, RenderContext)
9✔
2877
def _render(axes, mark, context):
9✔
2878
    x = mark._table[mark._coordinates[numpy.where(mark._coordinate_axes == "x")[0][0]]]
9✔
2879
    y = mark._table[mark._coordinates[numpy.where(mark._coordinate_axes == "y")[0][0]]]
9✔
2880

2881
    x = axes.project("x", x)
9✔
2882
    y = axes.project("y", y)
9✔
2883

2884
    mark_xml = xml.SubElement(
9✔
2885
        context.parent,
2886
        "g",
2887
        id=context.get_id(mark),
2888
        attrib={"class": "toyplot-mark-Text"},
2889
        )
2890

2891
    _render_table(owner=mark, key="data", label="text data", table=mark._table, filename=mark._filename, context=context)
9✔
2892

2893
    series_xml = xml.SubElement(
9✔
2894
        mark_xml, "g", attrib={"class": "toyplot-Series"})
2895
    for dx, dy, dtext, dangle, dfill, dopacity, dtitle in zip(
9✔
2896
            x,
2897
            y,
2898
            mark._table[mark._text[0]],
2899
            mark._table[mark._angle[0]],
2900
            mark._table[mark._fill[0]],
2901
            mark._table[mark._opacity[0]],
2902
            mark._table[mark._title[0]],
2903
        ):
2904

2905
        _draw_text(
9✔
2906
            root=series_xml,
2907
            text=str(dtext),
2908
            x=dx,
2909
            y=dy,
2910
            angle=dangle,
2911
            attributes={"class": "toyplot-Datum"},
2912
            style=toyplot.style.combine({"fill": toyplot.color.to_css(dfill), "opacity": dopacity}, mark._style),
2913
            title=dtitle,
2914
            )
2915

2916

2917
@dispatch(toyplot.mark.Image, RenderContext)
9✔
2918
def _render(mark, context):
9✔
2919
    mark_xml = xml.SubElement(
9✔
2920
        context.parent,
2921
        "g",
2922
        id=context.get_id(mark),
2923
        attrib={"class": "toyplot-mark-Image"},
2924
        )
2925

2926
    xml.SubElement(
9✔
2927
        mark_xml,
2928
        "image",
2929
        x=str(mark._xmin_range),
2930
        y=str(mark._ymin_range),
2931
        width=str(mark._xmax_range - mark._xmin_range),
2932
        height=str(mark._ymax_range - mark._ymin_range),
2933
        attrib={"xlink:href": toyplot.bitmap.to_png_data_uri(mark._data)},
2934
        )
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

© 2025 Coveralls, Inc