# Copyright 2014, Sandia Corporation. Under the terms of Contract
# DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains certain
# rights in this software.
from __future__ import division, absolute_import
from multipledispatch import dispatch
import base64
import collections
import copy
import io
import itertools
import json
import numbers
import numpy
import string
import toyplot.coordinates
import toyplot.canvas
import toyplot.color
import toyplot.compatibility
import toyplot.mark
import uuid
import xml.etree.ElementTree as xml
try:
import HTMLParser
except: # pragma: no cover
import html.parser as HTMLParser
class _NumpyJSONEncoder(json.JSONEncoder):
def default(self, obj): # pragma: no cover
if isinstance(obj, numpy.generic):
return numpy.asscalar(obj)
return json.JSONEncoder.default(self, obj)
class _RenderContext(object):
def __init__(self, root):
self._animation = collections.defaultdict(lambda: collections.defaultdict(list))
self._coordinate_systems = set()
self._data = list()
self._id_cache = dict()
self._parent = None
self._rendered = set()
self._root = root
self._visible_axes = set()
def add_coordinate_system(self, coordinate_system):
self._coordinate_systems.add(coordinate_system)
def add_data(self, item, content, title, filename):
if isinstance(item, toyplot.mark.Mark) and item.annotation:
return
self._data.append({
"id": self.get_id(item),
"title": title,
"content": content,
"filename": filename,
})
def add_visible_axis(self, axis):
self._visible_axes.add(axis)
def already_rendered(self, renderable):
if renderable in self._rendered:
return True
self._rendered.add(renderable)
return False
def get_id(self, obj):
python_id = id(obj)
if python_id not in self._id_cache:
self._id_cache[python_id] = "t" + uuid.uuid4().hex
return self._id_cache[python_id]
def copy(self, parent):
result = copy.copy(self)
result._parent = parent
return result
@property
def animation(self):
return self._animation
@property
def coordinate_systems(self):
return self._coordinate_systems
@property
def parent(self):
return self._parent
@property
def root(self):
return self._root
@property
def visible_axes(self):
return self._visible_axes
[docs]def apply_changes(html, changes):
for change_type, instructions in changes.items():
if change_type == "set-mark-style":
for mark_id, style in instructions:
mark = html.find(".//*[@id='%s']" % mark_id)
style = toyplot.style.combine(dict([declaration.split(
":") for declaration in mark.get("style").split(";") if declaration != ""]), style)
mark.set("style", _css_style(style))
elif change_type == "set-datum-style":
for mark_id, series, datum, style in instructions:
mark_xml = html.find(".//*[@id='%s']" % mark_id)
series_xml = mark_xml.findall(
"*[@class='toyplot-Series']")[series]
datum_xml = series_xml.findall(
"*[@class='toyplot-Datum']")[datum]
style = toyplot.style.combine(dict([declaration.split(
":") for declaration in datum_xml.get("style").split(";") if declaration != ""]), style)
datum_xml.set("style", _css_style(style))
elif change_type == "set-datum-text":
for mark_id, series, datum, text in instructions:
mark_xml = html.find(".//*[@id='%s']" % mark_id)
series_xml = mark_xml.findall(
"*[@class='toyplot-Series']")[series]
datum_xml = series_xml.findall(
"*[@class='toyplot-Datum']")[datum]
datum_xml.text = text
[docs]def render(canvas, fobj=None, animation=False):
"""Render the HTML representation of a canvas.
Generates HTML markup with an embedded SVG representation of the canvas, plus
JavaScript code for interactivity. If the canvas contains animation, the
markup will include an HTML user interface to control playback.
Parameters
----------
canvas: :class:`toyplot.canvas.Canvas`
The canvas to be rendered.
fobj: file-like object or string, optional
The file to write. Use a string filepath to write data directly to disk.
If `None` (the default), the HTML tree will be returned to the caller
instead.
animation: boolean, optional
If `True`, return a representation of the changes to be made to the HTML
tree for animation.
Returns
-------
html: xml.etree.ElementTree.Element or `None`
HTML representation of `canvas`, as a DOM tree, or `None` if the caller
specifies the `fobj` parameter.
changes: JSON-compatible data structure, or `None`
JSON-compatible representation of the animated changes to `canvas`.
Notes
-----
The output HTML is the "canonical" representation of a Toyplot canvas - the
other toyplot backends operate by converting the output from
toyplot.html.render() to the desired end target.
Note that the output HTML is a fragment wrapped in a <div>, suitable for
embedding in a larger document. It is the caller's responsibility to
supply the <html>, <body> etc. if the result is intended as a standalone
HTML document.
"""
canvas = toyplot.require.instance(canvas, toyplot.canvas.Canvas)
canvas.autorender(False)
# Create the top-level HTML element.
root_xml = xml.Element(
"div",
align="center",
attrib={"class": "toyplot"},
id="t" + uuid.uuid4().hex,
)
# Render the canvas.
context = _RenderContext(root=root_xml)
_render(canvas, context.copy(parent=root_xml))
# Return / write the results.
if isinstance(fobj, toyplot.compatibility.string_type):
with open(fobj, "wb") as file:
file.write(xml.tostring(root_xml, method="html"))
elif fobj is not None:
fobj.write(xml.tostring(root_xml, method="html"))
else:
if animation:
return root_xml, context.animation
return root_xml
def _color_fixup(styles):
"""It turns-out that many applications and libraries (Inkscape, Adobe Illustrator, Qt)
don't handle CSS rgba() colors correctly. So convert them to CSS rgb colors and use
fill-opacity / stroke-opacity instead."""
if "fill" in styles:
color = toyplot.color.css(styles["fill"])
if color is not None:
opacity = float(styles.get("fill-opacity", 1.0))
styles["fill"] = "rgb(%.3g%%,%.3g%%,%.3g%%)" % (
color["r"] * 100, color["g"] * 100, color["b"] * 100)
styles["fill-opacity"] = str(color["a"] * opacity)
if "stroke" in styles:
color = toyplot.color.css(styles["stroke"])
if color is not None:
opacity = float(styles.get("stroke-opacity", 1.0))
styles["stroke"] = "rgb(%.3g%%,%.3g%%,%.3g%%)" % (
color["r"] * 100, color["g"] * 100, color["b"] * 100)
styles["stroke-opacity"] = str(color["a"] * opacity)
return styles
def _css_style(*styles):
style = _color_fixup(toyplot.style.combine(*styles))
return ";".join(["%s:%s" % (key, value)
for key, value in sorted(style.items())])
def _css_attrib(*styles):
style = _color_fixup(toyplot.style.combine(*styles))
attrib = {}
if len(style):
attrib["style"] = ";".join(
["%s:%s" % (key, value) for key, value in sorted(style.items())])
return attrib
def _flat_contiguous(a):
i = 0
result = []
for (k, g) in itertools.groupby(a.ravel()):
n = len(list(g))
if k:
result.append(slice(i, i + n))
i += n
return result
class _HTMLParser(HTMLParser.HTMLParser):
def __init__(self, element, font_size):
HTMLParser.HTMLParser.__init__(self)
self._element = element
self._font_size = font_size
self._root_node = xml.Element("root")
self._current_node = self._root_node
self._parent_map = {self._root_node: self._root_node}
def push_node(self, tag, **kwargs):
node = xml.SubElement(self._current_node, tag, **kwargs)
self._parent_map[node] = self._current_node
self._current_node = node
def pop_node(self):
while self._current_node.tag == "br":
self._current_node = self._parent_map[self._current_node]
while True:
self._current_node = self._parent_map[self._current_node]
if self._current_node.tag != "br":
break
def walk_tree(self, node, attributes, style, stack_state, global_state):
if node.tag == "text":
attributes = copy.copy(attributes)
style = copy.copy(style)
style["dominant-baseline"] = "inherit"
x = global_state.pop("x", None)
if x is not None:
attributes["x"] = x
new_y = global_state["line-y"] + stack_state["dy"]
if new_y != global_state["current-y"]:
attributes["dy"] = new_y - global_state["current-y"]
global_state["current-y"] = new_y
tspan = xml.SubElement(self._element, "tspan")
for key, value in attributes.items():
tspan.set(key, str(value))
tspan.set("style", _css_style(style))
tspan.text = node.text
else:
attributes = copy.copy(attributes)
style = copy.copy(style)
stack_state = copy.copy(stack_state)
if node.tag in ["b", "strong"]:
style["font-weight"] = "bold"
elif node.tag == "br":
font_size = stack_state["font-size"]
global_state["x"] = 0
global_state["line-y"] += font_size * 1.2
elif node.tag == "code":
style["font-family"] = "monospace"
elif node.tag in ["em", "i"]:
style["font-style"] = "italic"
elif node.tag == "small":
font_size = stack_state["font-size"]
stack_state["font-size"] = font_size * 0.8
style["font-size"] = "%spx" % (font_size * 0.8)
elif node.tag == "sub":
font_size = stack_state["font-size"]
stack_state["font-size"] = font_size * 0.7
style["font-size"] = "%spx" % (font_size * 0.7)
stack_state["dy"] += font_size * 0.2
elif node.tag == "sup":
font_size = stack_state["font-size"]
stack_state["font-size"] = font_size * 0.7
style["font-size"] = "%spx" % (font_size * 0.7)
stack_state["dy"] -= font_size * 0.3
for child in node:
self.walk_tree(child, attributes, style, stack_state, global_state)
def handle_starttag(self, tag, attrs):
if tag == "br":
self.push_node("br")
elif tag in ["b", "code", "em", "i", "small", "strong", "sub", "sup"]:
self.push_node(tag)
else:
toyplot.log.warning("Ignoring unknown <%s> tag." % tag) # pragma: no cover
def handle_endtag(self, tag):
if tag in ["br"]:
toyplot.log.warning("%s must not have an end tag." % tag) # pragma: no cover
elif tag in ["b", "code", "em", "i", "small", "strong", "sub", "sup"]:
self.pop_node()
else:
toyplot.log.warning("Ignoring unknown </%s> tag." % tag) # pragma: no cover
def handle_data(self, text):
xml.SubElement(self._current_node, "text").text = text
def close(self):
HTMLParser.HTMLParser.close(self)
self.walk_tree(self._root_node, attributes={}, style={}, stack_state={"dy":0,"font-size":self._font_size}, global_state={"line-y":0, "current-y":0})
def _draw_text(
root,
text,
x=0,
y=0,
style=None,
angle=None,
title=None,
attributes={},
):
if not text:
return
style = copy.copy(style)
font_size = toyplot.units.convert(style["font-size"], target="px", default="px")
style["dominant-baseline"] = style.pop("alignment-baseline", "middle")
baseline_shift = -toyplot.units.convert(style.pop("baseline-shift", 0), target="px", default="px", reference=font_size)
anchor_shift = toyplot.units.convert(style.pop("-toyplot-anchor-shift", 0), target="px", default="px", reference=font_size)
transform = "translate(%r,%r)" % (x, y)
if angle:
transform += "rotate(%r)" % (-angle)
if baseline_shift or anchor_shift:
transform += "translate(%r,%r)" % (anchor_shift, baseline_shift)
text_xml = xml.SubElement(
root,
"text",
transform=transform,
style=_css_style(style),
attrib=attributes,
)
if title is not None:
xml.SubElement(text_xml, "title").text = str(title)
parser = _HTMLParser(text_xml, font_size)
parser.feed(text)
parser.close()
def _draw_marker(
root,
cx,
cy,
size,
marker,
marker_style=None,
label_style=None,
extra_class=None,
title=None):
if marker is None:
return
if isinstance(marker, toyplot.compatibility.string_type):
marker = {"shape": marker}
shape = marker.get("shape", None)
shape_angle = marker.get("angle", 0)
shape_style = marker.get("mstyle", None)
shape_label = marker.get("label", None)
shape_label_style = marker.get("lstyle", None)
if shape in _draw_marker.variations:
variation = _draw_marker.variations[shape]
shape = variation[0]
shape_angle += variation[1]
attrib = _css_attrib(marker_style, shape_style)
if extra_class is not None:
attrib["class"] = extra_class
marker_xml = xml.SubElement(root, "g", attrib=attrib)
if title is not None:
xml.SubElement(marker_xml, "title").text = str(title)
if shape == "|":
xml.SubElement(marker_xml,
"line",
transform="rotate(%r, %r, %r)" % (-shape_angle,
cx,
cy),
x1=repr(cx),
x2=repr(cx),
y1=repr(cy - (size / 2)),
y2=repr(cy + (size / 2)))
elif shape == "+":
xml.SubElement(marker_xml,
"line",
transform="rotate(%r, %r, %r)" % (-shape_angle,
cx,
cy),
x1=repr(cx),
x2=repr(cx),
y1=repr(cy - (size / 2)),
y2=repr(cy + (size / 2)))
xml.SubElement(marker_xml,
"line",
transform="rotate(%r, %r, %r)" % (-shape_angle,
cx,
cy),
x1=repr(cx - (size / 2)),
x2=repr(cx + (size / 2)),
y1=repr(cy),
y2=repr(cy))
elif shape == "*":
xml.SubElement(marker_xml,
"line",
transform="rotate(%r, %r, %r)" % (-shape_angle,
cx,
cy),
x1=repr(cx),
x2=repr(cx),
y1=repr(cy - (size / 2)),
y2=repr(cy + (size / 2)))
xml.SubElement(marker_xml,
"line",
transform="rotate(%r, %r, %r)" % (-shape_angle + 60,
cx,
cy),
x1=repr(cx),
x2=repr(cx),
y1=repr(cy - (size / 2)),
y2=repr(cy + (size / 2)))
xml.SubElement(marker_xml,
"line",
transform="rotate(%r, %r, %r)" % (-shape_angle - 60,
cx,
cy),
x1=repr(cx),
x2=repr(cx),
y1=repr(cy - (size / 2)),
y2=repr(cy + (size / 2)))
elif shape == "^":
xml.SubElement(marker_xml,
"polygon",
transform="rotate(%r, %r, %r)" % (-shape_angle,
cx,
cy),
points=" ".join(["%r,%r" % (xp,
yp) for xp,
yp in [(cx - (size / 2),
cy + (size / 2)),
(cx,
cy - (size / 2)),
(cx + (size / 2),
cy + (size / 2))]]))
elif shape == "s":
xml.SubElement(marker_xml,
"rect",
transform="rotate(%r, %r, %r)" % (-shape_angle,
cx,
cy),
x=repr(cx - (size / 2)),
y=repr(cy - (size / 2)),
width=repr(size),
height=repr(size))
elif shape == "o":
xml.SubElement(
marker_xml, "circle", cx=repr(cx), cy=repr(cy), r=repr(size / 2))
elif shape == "oo":
xml.SubElement(
marker_xml, "circle", cx=repr(cx), cy=repr(cy), r=repr(size / 2))
xml.SubElement(
marker_xml, "circle", cx=repr(cx), cy=repr(cy), r=repr(size / 4))
elif shape == "o|":
xml.SubElement(
marker_xml, "circle", cx=repr(cx), cy=repr(cy), r=repr(size / 2))
xml.SubElement(marker_xml,
"line",
transform="rotate(%r, %r, %r)" % (-shape_angle,
cx,
cy),
x1=repr(cx),
x2=repr(cx),
y1=repr(cy - (size / 2)),
y2=repr(cy + (size / 2)))
elif shape == "o+":
xml.SubElement(
marker_xml, "circle", cx=repr(cx), cy=repr(cy), r=repr(size / 2))
xml.SubElement(marker_xml,
"line",
transform="rotate(%r, %r, %r)" % (-shape_angle,
cx,
cy),
x1=repr(cx),
x2=repr(cx),
y1=repr(cy - (size / 2)),
y2=repr(cy + (size / 2)))
xml.SubElement(marker_xml,
"line",
transform="rotate(%r, %r, %r)" % (-shape_angle,
cx,
cy),
x1=repr(cx - (size / 2)),
x2=repr(cx + (size / 2)),
y1=repr(cy),
y2=repr(cy))
elif shape == "o*":
xml.SubElement(
marker_xml, "circle", cx=repr(cx), cy=repr(cy), r=repr(size / 2))
xml.SubElement(marker_xml,
"line",
transform="rotate(%r, %r, %r)" % (-shape_angle,
cx,
cy),
x1=repr(cx),
x2=repr(cx),
y1=repr(cy - (size / 2)),
y2=repr(cy + (size / 2)))
xml.SubElement(marker_xml,
"line",
transform="rotate(%r, %r, %r)" % (-shape_angle + 60,
cx,
cy),
x1=repr(cx),
x2=repr(cx),
y1=repr(cy - (size / 2)),
y2=repr(cy + (size / 2)))
xml.SubElement(marker_xml,
"line",
transform="rotate(%r, %r, %r)" % (-shape_angle - 60,
cx,
cy),
x1=repr(cx),
x2=repr(cx),
y1=repr(cy - (size / 2)),
y2=repr(cy + (size / 2)))
if shape_label: # Not technically necessary, but we should avoid computing the style for every marker if we don't have to.
_draw_text(
root=marker_xml,
text=shape_label,
x=cx,
y=cy,
style = toyplot.style.combine(
{
"stroke": "none",
"fill": toyplot.color.near_black,
"text-anchor": "middle",
"alignment-baseline": "middle",
"font-size": "%rpx" % (size * 0.75),
},
label_style,
shape_label_style),
)
return marker_xml
_draw_marker.variations = {"-": ("|", 90), "/": ("|", -45), "\\": ("|", 45), "x": ("+", 45), "v": ("^", 180), "<": (
"^", -90), ">": ("^", 90), "d": ("s", 45), "o-": ("o|", 90), "ox": ("o+", 45)}
def _axis_transform(x1, y1, x2, y2, offset, return_length=False):
p = numpy.row_stack(((x1, y1), (x2, y2)))
basis = p[1] - p[0]
length = numpy.linalg.norm(basis)
theta = numpy.rad2deg(numpy.arctan2(basis[1], basis[0]))
transform=str()
if p[0][0] or p[0][1]:
transform += "translate(%s,%s)" % (p[0][0], p[0][1])
if theta:
transform += "rotate(%s)" % theta
if offset:
transform += "translate(0,%s)" % offset
if return_length:
return transform, length
return transform
@dispatch(toyplot.canvas.Canvas, _RenderContext)
def _render(canvas, context):
svg_xml = xml.SubElement(
context.parent,
"svg",
xmlns="http://www.w3.org/2000/svg",
attrib={
"class": "toyplot-canvas-Canvas",
"xmlns:toyplot": "http://www.sandia.gov/toyplot",
"xmlns:xlink": "http://www.w3.org/1999/xlink",
},
width="%rpx" % canvas.width,
height="%rpx" % canvas.height,
viewBox="0 0 %r %r" % (canvas.width, canvas.height),
preserveAspectRatio="xMidYMid meet",
style=_css_style(canvas._style),
id=context.get_id(canvas))
for child in canvas._children:
_render(canvas, child._finalize(), context.copy(parent=svg_xml))
interactive_xml = xml.SubElement(
context.parent,
"div",
attrib={"class": "toyplot-interactive"},
)
_render_data_table_export(context.copy(parent=interactive_xml))
_render_data_cursors(context.copy(parent=interactive_xml))
_render_interactive_mouse_coordinates(context.copy(parent=interactive_xml))
_render(canvas._animation, context.copy(parent=interactive_xml))
def _render_data_table_export(context):
if not context._data:
return
context.parent.append(xml.XML(
"""<ul class="toyplot-mark-popup" onmouseleave="this.style.visibility='hidden'" style="background:rgba(0%,0%,0%,0.75);border:0;border-radius:6px;color:white;cursor:default;list-style:none;margin:0;padding:5px;position:fixed;visibility:hidden">
<li class="toyplot-mark-popup-title" style="color:lightgray;cursor:default;padding:5px;list-style:none;margin:0"/>
<li class="toyplot-mark-popup-save-csv" style="border-radius:3px;padding:5px;list-style:none;margin:0" onmouseover="this.style.color='steelblue';this.style.background='white'" onmouseout="this.style.color='white';this.style.background='steelblue'">
Save as .csv
</li>
</ul>"""))
data_tables = list()
for data in context._data:
content = data["content"]
names = []
columns = []
if isinstance(content, toyplot.data.Table):
for name, column in content.items():
if "toyplot:exportable" in content.metadata(name) and content.metadata(name)["toyplot:exportable"]:
if column.dtype == toyplot.color.dtype:
raise ValueError("Color column table export isn't supported.") # pragma: no cover
else:
names.append(name)
columns.append(column.tolist())
else: # Assume numpy matrix
for column in content.T:
names.append(column[0])
columns.append(column[1:].tolist())
if names and columns:
data_tables.append({
"id": data["id"],
"filename": data["filename"] if data["filename"] else "toyplot",
"title": data["title"],
"names": names,
"columns": columns,
})
xml.SubElement(context.parent, "script").text = string.Template("""
(function()
{
var data_tables = $data_tables;
function save_csv(data_table)
{
var uri = "data:text/csv;charset=utf-8,";
uri += data_table.names.join(",") + "\\n";
for(var i = 0; i != data_table.columns[0].length; ++i)
{
for(var j = 0; j != data_table.columns.length; ++j)
{
if(j)
uri += ",";
uri += data_table.columns[j][i];
}
uri += "\\n";
}
uri = encodeURI(uri);
var link = document.createElement("a");
if(typeof link.download != "undefined")
{
link.href = uri;
link.style = "visibility:hidden";
link.download = data_table.filename + ".csv";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
else
{
window.open(uri);
}
}
function open_popup(data_table)
{
return function(e)
{
var popup = document.querySelector("#$root_id .toyplot-mark-popup");
popup.querySelector(".toyplot-mark-popup-title").innerHTML = data_table.title;
popup.querySelector(".toyplot-mark-popup-save-csv").onclick = function() { popup.style.visibility = "hidden"; save_csv(data_table); }
popup.style.left = (e.clientX - 50) + "px";
popup.style.top = (e.clientY - 20) + "px";
popup.style.visibility = "visible";
e.stopPropagation();
e.preventDefault();
}
}
for(var i = 0; i != data_tables.length; ++i)
{
var data_table = data_tables[i];
var event_target = document.querySelector("#" + data_table.id);
event_target.oncontextmenu = open_popup(data_table);
}
})();
""").substitute(
root_id=context.root.get("id"),
data_tables=json.dumps(data_tables),
)
def _render_data_cursors(context):
if not context.coordinate_systems:
return
def _render_interactive_mouse_coordinates(context):
if not context.visible_axes:
return
visible_axes = dict()
for axis in context.visible_axes:
key = context.get_id(axis)
visible_axes[key] = list()
for segment in axis.projection._segments:
visible_axes[key].append(
{
"scale": segment.scale,
"domain":
{
"min": segment.domain.min,
"max": segment.domain.max,
"bounds":
{
"min": segment.domain.bounds.min,
"max": segment.domain.bounds.max,
},
},
"range":
{
"min": segment.range.min,
"max": segment.range.max,
"bounds":
{
"min": segment.range.bounds.min,
"max": segment.range.bounds.max,
},
},
})
xml.SubElement(context.parent, "script").text = string.Template("""
(function()
{
function _sign(x)
{
return x < 0 ? -1 : x > 0 ? 1 : 0;
}
function _mix(a, b, amount)
{
return ((1.0 - amount) * a) + (amount * b);
}
function _log(x, base)
{
return Math.log(Math.abs(x)) / Math.log(base);
}
function _in_range(a, x, b)
{
var left = Math.min(a, b);
var right = Math.max(a, b);
return left <= x && x <= right;
}
function inside(range, projection)
{
for(var i = 0; i != projection.length; ++i)
{
var segment = projection[i];
if(_in_range(segment.range.min, range, segment.range.max))
return true;
}
return false;
}
function to_domain(range, projection)
{
for(var i = 0; i != projection.length; ++i)
{
var segment = projection[i];
if(_in_range(segment.range.bounds.min, range, segment.range.bounds.max))
{
if(segment.scale == "linear")
{
var amount = (range - segment.range.min) / (segment.range.max - segment.range.min);
return _mix(segment.domain.min, segment.domain.max, amount)
}
else if(segment.scale[0] == "log")
{
var amount = (range - segment.range.min) / (segment.range.max - segment.range.min);
var base = segment.scale[1];
return _sign(segment.domain.min) * Math.pow(base, _mix(_log(segment.domain.min, base), _log(segment.domain.max, base), amount));
}
}
}
}
function display_coordinates(e)
{
var current = svg.createSVGPoint();
current.x = e.clientX;
current.y = e.clientY;
for(var axis_id in axes)
{
var axis = document.querySelector("#" + axis_id);
var coordinates = axis.querySelector(".toyplot-coordinates-Axis-coordinates");
if(coordinates)
{
var projection = axes[axis_id];
var local = current.matrixTransform(axis.getScreenCTM().inverse());
if(inside(local.x, projection))
{
var domain = to_domain(local.x, projection);
coordinates.style.visibility = "visible";
coordinates.setAttribute("transform", "translate(" + local.x + ")");
var text = coordinates.querySelector("text");
text.textContent = domain.toFixed(2);
}
else
{
coordinates.style.visibility= "hidden";
}
}
}
}
var root_id = "$root_id";
var axes = $visible_axes;
var svg = document.querySelector("#" + root_id + " svg");
svg.addEventListener("click", display_coordinates);
})();
""").substitute(
root_id=context.root.get("id"),
visible_axes=json.dumps(visible_axes, cls=_NumpyJSONEncoder, sort_keys=True),
)
@dispatch(toyplot.canvas.Canvas._AnimationFrames, _RenderContext)
def _render(frames, context):
# Collect animation data.
for time, time_changes in list(frames.items())[:-1]:
# Ensure we have an entry for every time, even if there aren't any
# changes.
context.animation[time]
for type, type_changes in time_changes.items():
for change in type_changes:
context.animation[time][type].append(
[context.get_id(change[0])] + list(change[1:]))
if len(context.animation) < 2:
return
times = numpy.array(sorted(context.animation.keys()))
durations = times[1:] - times[:-1]
changes = [change for time, change in sorted(context.animation.items())]
context.parent.append(xml.XML(
"""<div class="toyplot-vcr-controls">
<input class="toyplot-current-frame" title="Frame" type="range" min="0" max="{frames}" step="1" value="0"/>
<button class="toyplot-rewind" title="Rewind" style="width:40px;height:24px">
<svg width="20" height="20">
<polygon points="10,5 0,10 10,15" stroke="none" fill="{near_black}"/>
<polygon points="20,5 10,10 20,15" stroke="none" fill="{near_black}"/>
</svg>
</button>
<button class="toyplot-reverse-play" title="Reverse Play" style="width:40px;height:24px">
<svg width="20" height="20">
<polygon points="15,5 5,10 15,15" stroke="none" fill="{near_black}"/>
</svg>
</button>
<button class="toyplot-frame-rewind" title="Frame Rewind" style="width:40px;height:24px">
<svg width="20" height="20">
<polygon points="15,5 5,10 15,15" stroke="none" fill="{near_black}"/>
<rect x="17" y="5" width="2" height="10" stroke="none" fill="{near_black}"/>
</svg>
</button>
<button class="toyplot-stop" title="Stop" style="width:40px;height:24px">
<svg width="20" height="20">
<rect x="5" y="5" width="10" height="10" stroke="none" fill="{near_black}"/>
</svg>
</button>
<button class="toyplot-frame-advance" title="Frame Advance" style="width:40px;height:24px">
<svg width="20" height="20">
<polygon points="5,5 15,10 5,15" stroke="none" fill="{near_black}"/>
<rect x="1" y="5" width="2" height="10" stroke="none" fill="{near_black}"/>
</svg>
</button>
<button class="toyplot-forward-play" title="Play" style="width:40px;height:24px">
<svg width="20" height="20">
<polygon points="5,5 15,10 5,15" stroke="none" fill="{near_black}"/>
</svg>
</button>
<button class="toyplot-fast-forward" title="Fast Forward" style="width:40px;height:24px">
<svg width="20" height="20">
<polygon points="0,5 10,10 0,15" stroke="none" fill="{near_black}"/>
<polygon points="10,5 20,10 10,15" stroke="none" fill="{near_black}"/>
</svg>
</button>
</div>""".format(
frames = len(times) - 2,
near_black=toyplot.color.near_black,
)))
xml.SubElement(context.parent, "script").text = string.Template("""
(function()
{
var root_id = "$root_id";
var frame_durations = $frame_durations;
var state_changes = $state_changes;
var current_frame = null;
var timeout = null;
function set_timeout(value)
{
if(timeout !== null)
window.clearTimeout(timeout);
timeout = value;
}
function set_current_frame(frame)
{
current_frame = frame;
document.querySelector("#" + root_id + " .toyplot-current-frame").value = frame;
}
function play_reverse()
{
set_current_frame((current_frame - 1 + frame_durations.length) % frame_durations.length);
render_changes(0, current_frame+1)
set_timeout(window.setTimeout(play_reverse, frame_durations[(current_frame - 1 + frame_durations.length) % frame_durations.length] * 1000));
}
function play_forward()
{
set_current_frame((current_frame + 1) % frame_durations.length);
render_changes(current_frame, current_frame+1);
set_timeout(window.setTimeout(play_forward, frame_durations[current_frame] * 1000));
}
var item_cache = {};
function get_item(id)
{
if(!(id in item_cache))
item_cache[id] = document.getElementById(id);
return item_cache[id];
}
function render_changes(begin, end)
{
for(var frame = begin; frame != end; ++frame)
{
var changes = state_changes[frame];
for(var type in changes)
{
var type_changes = changes[type]
if(type == "set-mark-style")
{
for(var i = 0; i != type_changes.length; ++i)
{
var mark_style = type_changes[i];
var mark = get_item(mark_style[0]);
for(var key in mark_style[1])
mark.style.setProperty(key, mark_style[1][key]);
}
}
else if(type == "set-datum-style")
{
for(var i = 0; i != type_changes.length; ++i)
{
var datum_style = type_changes[i];
var datum = get_item(datum_style[0]).querySelectorAll(".toyplot-Series")[datum_style[1]].querySelectorAll(".toyplot-Datum")[datum_style[2]];
for(var key in datum_style[3])
datum.style.setProperty(key, datum_style[3][key]);
}
}
}
}
}
function on_set_frame()
{
set_timeout(null);
set_current_frame(document.querySelector("#" + root_id + " .toyplot-current-frame").valueAsNumber);
render_changes(0, current_frame+1);
}
function on_frame_rewind()
{
set_timeout(null);
set_current_frame((current_frame - 1 + frame_durations.length) % frame_durations.length);
render_changes(0, current_frame+1);
}
function on_rewind()
{
set_current_frame(0);
render_changes(0, current_frame+1);
}
function on_play_reverse()
{
set_timeout(window.setTimeout(play_reverse, frame_durations[(current_frame - 1 + frame_durations.length) % frame_durations.length] * 1000));
}
function on_stop()
{
set_timeout(null);
}
function on_play_forward()
{
set_timeout(window.setTimeout(play_forward, frame_durations[current_frame] * 1000));
}
function on_fast_forward()
{
set_timeout(null);
set_current_frame(frame_durations.length - 1);
render_changes(0, current_frame + 1)
}
function on_frame_advance()
{
set_timeout(null);
set_current_frame((current_frame + 1) % frame_durations.length);
render_changes(current_frame, current_frame+1);
}
set_current_frame(0);
render_changes(0, current_frame+1);
document.querySelector("#" + root_id + " .toyplot-current-frame").oninput = on_set_frame;
document.querySelector("#" + root_id + " .toyplot-rewind").onclick = on_rewind;
document.querySelector("#" + root_id + " .toyplot-reverse-play").onclick = on_play_reverse;
document.querySelector("#" + root_id + " .toyplot-frame-rewind").onclick = on_frame_rewind;
document.querySelector("#" + root_id + " .toyplot-stop").onclick = on_stop;
document.querySelector("#" + root_id + " .toyplot-frame-advance").onclick = on_frame_advance;
document.querySelector("#" + root_id + " .toyplot-forward-play").onclick = on_play_forward;
document.querySelector("#" + root_id + " .toyplot-fast-forward").onclick = on_fast_forward;
})();
""").substitute(
root_id=context.root.get("id"),
frame_durations=json.dumps(durations.tolist()),
state_changes=json.dumps(changes, cls=_NumpyJSONEncoder),
)
@dispatch(toyplot.canvas.Canvas, toyplot.coordinates.Axis, _RenderContext)
def _render(canvas, axis, context):
if context.already_rendered(axis):
return
if axis.show:
context.add_visible_axis(axis)
transform, length = _axis_transform(axis._x1, axis._y1, axis._x2, axis._y2, offset=axis._offset, return_length=True)
axis_xml = xml.SubElement(
context.parent,
"g",
id=context.get_id(axis),
transform=transform,
attrib={"class": "toyplot-coordinates-Axis"},
)
if axis.spine.show:
x1 = 0
x2 = length
if axis.domain.show and axis._data_min is not None and axis._data_max is not None:
x1 = max(
x1, axis.projection(axis._data_min))
x2 = min(
x2, axis.projection(axis._data_max))
xml.SubElement(
axis_xml,
"line",
x1=repr(x1),
y1=repr(0),
x2=repr(x2),
y2=repr(0),
style=_css_style(
axis.spine._style))
if axis.ticks.show:
y1 = axis._ticks_near if axis._tick_location == "below" else -axis._ticks_near
y2 = -axis._ticks_far if axis._tick_location == "below" else axis._ticks_far
ticks_group = xml.SubElement(axis_xml, "g")
for location, tick_style in zip(
axis._tick_locations,
axis.ticks.tick.styles(axis._tick_locations),
):
x = axis.projection(location)
xml.SubElement(
ticks_group,
"line",
x1=repr(x),
y1=repr(y1),
x2=repr(x),
y2=repr(y2),
style=_css_style(
axis.ticks._style,
tick_style))
if axis.ticks.labels.show:
location = axis._tick_labels_location
if axis.ticks.labels.angle:
alignment_baseline = "central"
if location == "above":
text_anchor = "start" if axis.ticks.labels.angle > 0 else "end"
elif location == "below":
text_anchor = "end" if axis.ticks.labels.angle > 0 else "start"
else:
alignment_baseline = "alphabetic" if location == "above" else "hanging"
text_anchor = "middle"
y = axis._tick_labels_offset if location == "below" else -axis._tick_labels_offset
ticks_group = xml.SubElement(axis_xml, "g")
for location, label, title, label_style in zip(
axis._tick_locations,
axis._tick_labels,
axis._tick_titles,
axis.ticks.labels.label.styles(axis._tick_locations),
):
x = axis.projection(location)
style=toyplot.style.combine(
{
"alignment-baseline": alignment_baseline,
"text-anchor": text_anchor,
},
axis.ticks.labels.style,
label_style,
)
_draw_text(
root=ticks_group,
text=label,
x=x,
y=y,
style=style,
angle=axis.ticks.labels.angle,
title=title,
)
location = axis._label_location
alignment_baseline = "alphabetic" if location == "above" else "hanging"
text_anchor = "middle"
y = axis._label_offset if location == "below" else -axis._label_offset
_draw_text(
root=axis_xml,
text=axis.label.text,
x=length * 0.5,
y=y,
style=toyplot.style.combine(
{
"alignment-baseline": alignment_baseline,
"text-anchor": text_anchor,
},
axis.label.style,
),
)
if axis.interactive.coordinates.show:
coordinates_xml = xml.SubElement(
axis_xml, "g",
attrib={"class": "toyplot-coordinates-Axis-coordinates"},
style=_css_style({"visibility": "hidden"}),
transform="",
)
if axis.interactive.coordinates.tick.show:
y1 = axis._tick_labels_offset if axis._interactive_coordinates_location == "below" else -axis._tick_labels_offset
y1 *= 0.5
y2 = -axis._tick_labels_offset if axis._interactive_coordinates_location == "below" else axis._tick_labels_offset
y2 *= 0.75
marker_xml = xml.SubElement(
coordinates_xml, "line",
x1="0",
x2="0",
y1=repr(y1),
y2=repr(y2),
style=_css_style(axis.interactive.coordinates.tick.style),
)
if axis.interactive.coordinates.label.show:
y = axis._tick_labels_offset if axis._interactive_coordinates_location == "below" else -axis._tick_labels_offset
alignment_baseline = "hanging" if axis._interactive_coordinates_location == "below" else "alphabetic"
text_xml = xml.SubElement(
coordinates_xml, "text",
x="0",
y=repr(y),
style=_css_style(toyplot.style.combine(
{"alignment-baseline": alignment_baseline},
axis.interactive.coordinates.label.style,
)),
)
@dispatch(toyplot.canvas.Canvas, toyplot.coordinates.Numberline, _RenderContext)
def _render(canvas, numberline, context):
context.add_coordinate_system(numberline)
numberline_xml = xml.SubElement(context.parent, "g", id=context.get_id(
numberline), attrib={"class": "toyplot-coordinates-Numberline"})
clip_xml = xml.SubElement(
numberline_xml,
"clipPath",
id="t" + uuid.uuid4().hex,
)
transform, length = _axis_transform(numberline._x1, numberline._y1, numberline._x2, numberline._y2, offset=0, return_length=True)
height = numberline.axis._offset
if numberline._child_offset:
height += numpy.amax(list(numberline._child_offset.values()))
xml.SubElement(
clip_xml,
"rect",
x=repr(0),
y=repr(-height),
width=repr(length),
height=repr(height + numberline.axis._offset),
)
children_xml = xml.SubElement(
numberline_xml,
"g",
attrib={"clip-path": "url(#%s)" % clip_xml.get("id")},
transform=transform,
)
for child in numberline._children:
_render(numberline, child._finalize(), context.copy(parent=children_xml))
_render(canvas, numberline.axis, context.copy(parent=numberline_xml))
@dispatch(toyplot.coordinates.Numberline, toyplot.color.CategoricalMap, _RenderContext)
def _render(numberline, colormap, context):
offset = numberline._child_offset[colormap]
width = numberline._child_width[colormap]
style = numberline._child_style[colormap]
mark_xml = xml.SubElement(
context.parent,
"g", id=context.get_id(colormap),
attrib={"class": "toyplot-color-CategoricalMap"},
)
if offset:
mark_xml.set("transform", "translate(0,%s)" % -offset)
samples = numpy.linspace(colormap.domain.min, colormap.domain.max, len(colormap._palette), endpoint=True)
projected = numberline.axis.projection(samples)
colormap_range_min, colormap_range_max = numberline.axis.projection([colormap.domain.min, colormap.domain.max])
for index, (x1, x2), in enumerate(zip(projected[:-1], projected[1:])):
color = colormap._palette[index]
xml.SubElement(
mark_xml,
"rect",
x=repr(x1),
y=repr(-width * 0.5),
width=repr(x2 - x1),
height=repr(width),
style=_css_style({"stroke": "none", "fill": toyplot.color.to_css(color)}),
)
style = toyplot.style.combine(
{"stroke": "none", "stroke-width":1.0, "fill": "none"},
style,
)
xml.SubElement(
mark_xml,
"rect",
x=repr(colormap_range_min),
y=repr(-width * 0.5),
width=repr(colormap_range_max - colormap_range_min),
height=repr(width),
style=_css_style(style),
)
@dispatch(toyplot.coordinates.Numberline, toyplot.color.Map, _RenderContext)
def _render(numberline, colormap, context):
offset = numberline._child_offset[colormap]
width = numberline._child_width[colormap]
style = numberline._child_style[colormap]
colormap_range_min, colormap_range_max = numberline.axis.projection([colormap.domain.min, colormap.domain.max])
mark_xml = xml.SubElement(
context.parent,
"g", id=context.get_id(colormap),
attrib={"class": "toyplot-color-Map"},
)
if offset:
mark_xml.set("transform", "translate(0, %s)" % -offset)
defs_xml = xml.SubElement(
mark_xml,
"defs",
)
gradient_xml = xml.SubElement(
defs_xml,
"linearGradient",
id="t" + uuid.uuid4().hex,
x1=repr(colormap_range_min),
x2=repr(colormap_range_max),
y1=repr(0),
y2=repr(0),
gradientUnits="userSpaceOnUse",
)
samples = numpy.linspace(colormap.domain.min, colormap.domain.max, 64, endpoint=True)
for sample in samples:
color = colormap.colors(sample)
psample = numberline.axis.projection(sample)
offset = (psample - colormap_range_min) / (colormap_range_max - colormap_range_min)
xml.SubElement(
gradient_xml,
"stop",
offset="%s" % offset,
attrib={
"stop-color": "rgb(%.3g%%,%.3g%%,%.3g%%)" % (color["r"] * 100, color["g"] * 100, color["b"] * 100),
"stop-opacity": str(color["a"]),
},
)
style = toyplot.style.combine(
{"stroke": "none", "stroke-width":1.0, "fill": "url(#%s)" % gradient_xml.get("id")},
style,
)
xml.SubElement(
mark_xml,
"rect",
x=repr(colormap_range_min),
y=repr(-width * 0.5),
width=repr(colormap_range_max - colormap_range_min),
height=repr(width),
style=_css_style(style),
)
@dispatch(toyplot.coordinates.Numberline, toyplot.mark.Scatterplot, _RenderContext)
def _render(numberline, mark, context):
offset = numberline._child_offset[mark]
mark_xml = xml.SubElement(
context.parent,
"g",
style=_css_style(mark._style),
id=context.get_id(mark),
attrib={"class": "toyplot-mark-Scatterplot"},
)
if offset:
mark_xml.set("transform", "translate(0,%s)" % -offset)
context.add_data(mark, mark._table, title="Scatterplot Data", filename=mark._filename)
dimension1 = numpy.ma.column_stack([mark._table[key] for key in mark._coordinates])
X = numberline.axis.projection(dimension1)
for x, marker, msize, mfill, mstroke, mopacity, mtitle in zip(
X.T,
[mark._table[key] for key in mark._marker],
[mark._table[key] for key in mark._msize],
[mark._table[key] for key in mark._mfill],
[mark._table[key] for key in mark._mstroke],
[mark._table[key] for key in mark._mopacity],
[mark._table[key] for key in mark._mtitle],
):
not_null = numpy.invert(numpy.ma.getmaskarray(x))
series_xml = xml.SubElement(
mark_xml, "g", attrib={"class": "toyplot-Series"})
for dx, dmarker, dsize, dfill, dstroke, dopacity, dtitle in zip(
x[not_null],
marker[not_null],
msize[not_null],
mfill[not_null],
mstroke[not_null],
mopacity[not_null],
mtitle[not_null],
):
dstyle = toyplot.style.combine(
{
"fill": toyplot.color.to_css(dfill),
"stroke": toyplot.color.to_css(dstroke),
"opacity": dopacity,
},
mark._mstyle)
datum_xml = _draw_marker(
series_xml,
dx,
0,
dsize,
dmarker,
dstyle,
mark._mlstyle,
extra_class="toyplot-Datum",
title=dtitle,
)
@dispatch(toyplot.canvas.Canvas, toyplot.coordinates.Cartesian, _RenderContext)
def _render(canvas, axes, context):
context.add_coordinate_system(axes)
cartesian_xml = xml.SubElement(context.parent, "g", id=context.get_id(
axes), attrib={"class": "toyplot-coordinates-Cartesian"})
clip_xml = xml.SubElement(cartesian_xml, "clipPath", id="t" + uuid.uuid4().hex)
xml.SubElement(
clip_xml,
"rect",
x=repr(axes._xmin_range - axes.padding),
y=repr(axes._ymin_range - axes.padding),
width=repr(axes._xmax_range - axes._xmin_range + axes.padding * 2),
height=repr(axes._ymax_range - axes._ymin_range + axes.padding * 2),
)
children_xml = xml.SubElement(
cartesian_xml,
"g",
attrib={"clip-path" : "url(#%s)" % clip_xml.get("id")},
)
for child in axes._children:
_render(axes, child._finalize(), context.copy(parent=children_xml))
if axes._show:
_render(canvas, axes.x, context.copy(parent=cartesian_xml))
_render(canvas, axes.y, context.copy(parent=cartesian_xml))
_draw_text(
root=cartesian_xml,
text=axes.label._text,
x=(axes._xmin_range + axes._xmax_range) * 0.5,
y=axes._ymin_range,
style=axes.label._style,
)
@dispatch(toyplot.canvas.Canvas, toyplot.coordinates.Table, _RenderContext)
def _render(canvas, axes, context):
axes_xml = xml.SubElement(context.parent, "g", id=context.get_id(
axes), attrib={"class": "toyplot-coordinates-Table"})
context.add_data(axes, axes._cell_data, title="Table Data", filename=axes._filename)
# Render title
_draw_text(
root=axes_xml,
text=axes._label._text,
x=(axes._xmin_range + axes._xmax_range) * 0.5,
y=axes._ymin_range,
style=axes._label._style,
)
# For each unique group of cells.
for cell_group in numpy.unique(axes._cell_group):
cell_selection = (axes._cell_group == cell_group)
# Skip hidden groups.
cell_show = axes._cell_show[cell_selection][0]
if not cell_show:
continue
# Identify the closed range of rows and columns that contain the cell.
cell_rows, cell_columns = numpy.nonzero(cell_selection)
row_min = cell_rows.min()
row_max = cell_rows.max()
column_min = cell_columns.min()
column_max = cell_columns.max()
# Render the cell background.
cell_style = axes._cell_style[cell_selection][0]
if cell_style is not None:
# Compute the cell boundaries.
cell_top = axes._cell_top[row_min]
cell_bottom = axes._cell_bottom[row_max]
cell_left = axes._cell_left[column_min]
cell_right = axes._cell_right[column_max]
cell_xml = xml.SubElement(
axes_xml,
"rect",
x=repr(cell_left),
y=repr(cell_top),
width=repr(cell_right - cell_left),
height=repr(cell_bottom - cell_top),
style=_css_style({"fill":"none", "stroke":"none"}, cell_style),
)
cell_title = axes._cell_title[cell_selection][0]
if cell_title is not None:
xml.SubElement(cell_xml, "title").text = str(cell_title)
# Render the cell data.
cell_data = axes._cell_data[cell_selection][0]
if cell_data is not None:
# Compute the cell boundaries.
padding = 5
cell_top = axes._cell_top[row_min]
cell_bottom = axes._cell_bottom[row_max]
cell_left = axes._cell_left[column_min] + padding
cell_right = axes._cell_right[column_max] - padding
# Compute the text placement within the cell boundaries.
cell_align = axes._cell_align[cell_selection][0]
if cell_align is None:
cell_align = "left"
cell_angle = axes._cell_angle[cell_selection][0]
y = (cell_top + cell_bottom) / 2
# Format the cell data.
cell_format = axes._cell_format[cell_selection][0]
prefix, separator, suffix = cell_format.format(cell_data)
# Get the cell style.
cell_lstyle = axes._cell_lstyle[cell_selection][0]
# Render the cell data.
if cell_align == "left":
x = cell_left
_draw_text(
root=axes_xml,
x=x,
y=y,
style=toyplot.style.combine(cell_lstyle, {"text-anchor": "begin"}),
text=prefix + separator + suffix,
)
elif cell_align == "center":
x = (cell_left + cell_right) / 2
_draw_text(
root=axes_xml,
x=x,
y=y,
angle=cell_angle,
style=toyplot.style.combine(cell_lstyle, {"text-anchor": "middle"}),
text=prefix + separator + suffix,
)
elif cell_align == "right":
x = cell_right
_draw_text(
root=axes_xml,
x=x,
y=y,
style=toyplot.style.combine(cell_lstyle, {"text-anchor": "end"}),
text=prefix + separator + suffix,
)
elif cell_align is "separator":
x = (cell_left + cell_right) / 2
_draw_text(
root=axes_xml,
x=x - 2,
y=y,
style=toyplot.style.combine(cell_lstyle, {"text-anchor": "end"}),
text=prefix,
)
_draw_text(
root=axes_xml,
x=x,
y=y,
style=toyplot.style.combine(cell_lstyle, {"text-anchor": "middle"}),
text=separator,
)
_draw_text(
root=axes_xml,
x=x + 2,
y=y,
style=toyplot.style.combine(cell_lstyle, {"text-anchor": "begin"}),
text=suffix,
)
# Render children.
for child in axes._axes:
_render(axes._parent, child._finalize(), context.copy(parent=axes_xml))
# Render grid lines.
row_boundaries = axes._row_boundaries
column_boundaries = axes._column_boundaries
separation = axes._separation / 2
def contiguous(a):
i = 0
result = []
for (k, g) in itertools.groupby(a.ravel()):
n = len(list(g))
if k:
result.append((i, i + n, k))
i += n
return result
hlines = numpy.copy(axes._hlines)
hlines[numpy.logical_not(axes._hlines_show)] = False
for row_index, row in enumerate(hlines):
y = row_boundaries[row_index]
for start, end, line_type in contiguous(row):
if line_type == "single":
xml.SubElement(
axes_xml,
"line",
x1=repr(column_boundaries[start]),
y1=repr(y),
x2=repr(column_boundaries[end]),
y2=repr(y),
style=_css_style(axes._gstyle),
)
elif line_type == "double":
xml.SubElement(
axes_xml,
"line",
x1=repr(
column_boundaries[start]),
y1=repr(
y - separation),
x2=repr(
column_boundaries[end]),
y2=repr(
y - separation),
style=_css_style(
axes._gstyle))
xml.SubElement(
axes_xml,
"line",
x1=repr(
column_boundaries[start]),
y1=repr(
y + separation),
x2=repr(
column_boundaries[end]),
y2=repr(
y + separation),
style=_css_style(
axes._gstyle))
vlines = numpy.copy(axes._vlines)
vlines[numpy.logical_not(axes._vlines_show)] = False
for column_index, column in enumerate(vlines.T):
x = column_boundaries[column_index]
for start, end, line_type in contiguous(column):
if line_type == "single":
xml.SubElement(
axes_xml,
"line",
x1=repr(x),
y1=repr(row_boundaries[start]),
x2=repr(x),
y2=repr(row_boundaries[end]),
style=_css_style(axes._gstyle),
)
elif line_type == "double":
xml.SubElement(
axes_xml,
"line",
x1=repr(x - separation),
y1=repr(row_boundaries[start]),
x2=repr(x - separation),
y2=repr(row_boundaries[end]),
style=_css_style(axes._gstyle),
)
xml.SubElement(
axes_xml,
"line",
x1=repr(x + separation),
y1=repr(row_boundaries[start]),
x2=repr(x + separation),
y2=repr(row_boundaries[end]),
style=_css_style(axes._gstyle),
)
@dispatch((toyplot.mark.BarBoundaries, toyplot.mark.BarMagnitudes))
def _legend_markers(mark):
markers = []
for fill, opacity in zip(
[mark._table[key] for key in mark._fill],
[mark._table[key] for key in mark._opacity],
):
markers.append(
{
"shape": "s",
"mstyle": toyplot.style.combine(
{
"fill": toyplot.color.to_css(fill[0]),
"fill-opacity": opacity[0],
},
mark._style,
),
})
return markers
@dispatch(toyplot.coordinates.Cartesian, type(None), _RenderContext)
def _render(axes, mark, context):
pass
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.BarBoundaries, _RenderContext)
def _render(axes, mark, context):
left = mark._table[mark._left[0]]
right = mark._table[mark._right[0]]
boundaries = numpy.ma.column_stack(
[mark._table[key] for key in mark._boundaries])
if mark._coordinate_axes.tolist() == ["x", "y"]:
axis1 = "x"
axis2 = "y"
distance1 = "width"
distance2 = "height"
left = axes._project_x(left)
right = axes._project_x(right)
boundaries = axes._project_y(boundaries)
elif mark._coordinate_axes.tolist() == ["y", "x"]:
axis1 = "y"
axis2 = "x"
distance1 = "height"
distance2 = "width"
left = axes._project_y(left)
right = axes._project_y(right)
boundaries = axes._project_x(boundaries)
mark_xml = xml.SubElement(
context.parent,
"g",
style=_css_style(
mark._style),
id=context.get_id(mark),
attrib={
"class": "toyplot-mark-BarBoundaries"})
context.add_data(mark, mark._table, title="Bar Data", filename=mark._filename)
for boundary1, boundary2, fill, opacity, title in zip(
boundaries.T[:-1],
boundaries.T[1:],
[mark._table[key] for key in mark._fill],
[mark._table[key] for key in mark._opacity],
[mark._table[key] for key in mark._title],
):
not_null = numpy.invert(
numpy.logical_or(
numpy.ma.getmaskarray(boundary1),
numpy.ma.getmaskarray(boundary2)))
series_xml = xml.SubElement(
mark_xml, "g", attrib={"class": "toyplot-Series"})
for dleft, dright, dboundary1, dboundary2, dfill, dopacity, dtitle in zip(
left[not_null],
right[not_null],
boundary1[not_null],
boundary2[not_null],
fill[not_null],
opacity[not_null],
title[not_null],
):
dstyle = toyplot.style.combine({
"fill": toyplot.color.to_css(dfill),
"opacity": dopacity,
}, mark._style)
datum_xml = xml.SubElement(
series_xml,
"rect",
attrib={
"class": "toyplot-Datum",
axis1: repr(min(dleft, dright)),
axis2: repr(min(dboundary1, dboundary2)),
distance1: repr(numpy.abs(dleft - dright)),
distance2: repr(numpy.abs(dboundary1 - dboundary2)),
},
style=_css_style(dstyle),
)
if dtitle is not None:
xml.SubElement(datum_xml, "title").text = str(dtitle)
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.BarMagnitudes, _RenderContext)
def _render(axes, mark, context):
left = mark._table[mark._left[0]]
right = mark._table[mark._right[0]]
boundaries = numpy.ma.cumsum(numpy.ma.column_stack(
[mark._table[mark._baseline[0]]] + [mark._table[key] for key in mark._magnitudes]), axis=1)
not_null = numpy.invert(
numpy.ma.any(numpy.ma.getmaskarray(boundaries), axis=1))
if mark._coordinate_axes.tolist() == ["x", "y"]:
axis1 = "x"
axis2 = "y"
distance1 = "width"
distance2 = "height"
left = axes._project_x(left)
right = axes._project_x(right)
boundaries = axes._project_y(boundaries)
elif mark._coordinate_axes.tolist() == ["y", "x"]:
axis1 = "y"
axis2 = "x"
distance1 = "height"
distance2 = "width"
left = axes._project_y(left)
right = axes._project_y(right)
boundaries = axes._project_x(boundaries)
mark_xml = xml.SubElement(
context.parent,
"g",
style=_css_style(
mark._style),
id=context.get_id(mark),
attrib={
"class": "toyplot-mark-BarMagnitudes"})
context.add_data(mark, mark._table, title="Bar Data", filename=mark._filename)
for boundary1, boundary2, fill, opacity, title in zip(
boundaries.T[:-1],
boundaries.T[1:],
[mark._table[key] for key in mark._fill],
[mark._table[key] for key in mark._opacity],
[mark._table[key] for key in mark._title],
):
series_xml = xml.SubElement(
mark_xml, "g", attrib={"class": "toyplot-Series"})
for dleft, dright, dboundary1, dboundary2, dfill, dopacity, dtitle in zip(
left[not_null],
right[not_null],
boundary1[not_null],
boundary2[not_null],
fill[not_null],
opacity[not_null],
title[not_null],
):
dstyle = toyplot.style.combine(
{"fill": toyplot.color.to_css(dfill), "opacity": dopacity}, mark._style)
datum_xml = xml.SubElement(
series_xml,
"rect",
attrib={
"class": "toyplot-Datum",
axis1: repr(min(dleft, dright)),
axis2: repr(min(dboundary1, dboundary2)),
distance1: repr(numpy.abs(dleft - dright)),
distance2: repr(numpy.abs(dboundary1 - dboundary2)),
},
style=_css_style(dstyle),
)
if dtitle is not None:
xml.SubElement(datum_xml, "title").text = str(dtitle)
@dispatch((toyplot.mark.FillBoundaries, toyplot.mark.FillMagnitudes))
def _legend_markers(mark):
markers = []
for fill, opacity in zip(
mark._fill,
mark._opacity,
):
markers.append(
{
"shape": "s",
"mstyle": toyplot.style.combine(
{
"fill": toyplot.color.to_css(fill),
"fill-opacity": opacity,
},
mark._style,
),
})
return markers
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.FillBoundaries, _RenderContext)
def _render(axes, mark, context):
boundaries = numpy.ma.column_stack(
[mark._table[key] for key in mark._boundaries])
if mark._coordinate_axes.tolist() == ["x", "y"]:
position = axes._project_x(mark._table[mark._position[0]])
boundaries = axes._project_y(boundaries)
elif mark._coordinate_axes.tolist() == ["y", "x"]:
position = axes._project_y(mark._table[mark._position[0]])
boundaries = axes._project_x(boundaries)
mark_xml = xml.SubElement(
context.parent,
"g",
style=_css_style(
mark._style),
id=context.get_id(mark),
attrib={
"class": "toyplot-mark-FillBoundaries"})
context.add_data(mark, mark._table, title="Fill Data", filename=mark._filename)
for boundary1, boundary2, fill, opacity, title in zip(
boundaries.T[:-1], boundaries.T[1:], mark._fill, mark._opacity, mark._title):
not_null = numpy.invert(
numpy.logical_or(
numpy.ma.getmaskarray(boundary1),
numpy.ma.getmaskarray(boundary2)))
segments = _flat_contiguous(not_null)
series_style = toyplot.style.combine(
{"fill": toyplot.color.to_css(fill), "opacity": opacity}, mark._style)
for segment in segments:
if mark._coordinate_axes[0] == "x":
coordinates = zip(
numpy.concatenate((position[segment], position[segment][::-1])),
numpy.concatenate((boundary1[segment], boundary2[segment][::-1])))
elif mark._coordinate_axes[0] == "y":
coordinates = zip(
numpy.concatenate((boundary1[segment], boundary2[segment][::-1])),
numpy.concatenate((position[segment], position[segment][::-1])))
series_xml = xml.SubElement(mark_xml, "polygon", points=" ".join(
["%r,%r" % (xi, yi) for xi, yi in coordinates]), style=_css_style(series_style))
if title is not None:
xml.SubElement(series_xml, "title").text = str(title)
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.FillMagnitudes, _RenderContext)
def _render(axes, mark, context):
magnitudes = numpy.ma.column_stack(
[mark._table[mark._baseline[0]]] + [mark._table[key] for key in mark._magnitudes])
boundaries = numpy.ma.cumsum(magnitudes, axis=1)
not_null = numpy.invert(
numpy.ma.any(numpy.ma.getmaskarray(boundaries), axis=1))
segments = _flat_contiguous(not_null)
if mark._coordinate_axes.tolist() == ["x", "y"]:
position = axes._project_x(mark._table[mark._position[0]])
boundaries = axes._project_y(boundaries)
elif mark._coordinate_axes.tolist() == ["y", "x"]:
position = axes._project_y(mark._table[mark._position[0]])
boundaries = axes._project_x(boundaries)
mark_xml = xml.SubElement(
context.parent,
"g",
style=_css_style(
mark._style),
id=context.get_id(mark),
attrib={
"class": "toyplot-mark-FillMagnitudes"})
context.add_data(mark, mark._table, title="Fill Data", filename=mark._filename)
for boundary1, boundary2, fill, opacity, title in zip(
boundaries.T[:-1], boundaries.T[1:], mark._fill, mark._opacity, mark._title):
series_style = toyplot.style.combine(
{"fill": toyplot.color.to_css(fill), "opacity": opacity}, mark._style)
for segment in segments:
if mark._coordinate_axes[0] == "x":
coordinates = zip(
numpy.concatenate((position[segment], position[segment][::-1])),
numpy.concatenate((boundary1[segment], boundary2[segment][::-1])))
elif mark._coordinate_axes[0] == "y":
coordinates = zip(
numpy.concatenate((boundary1[segment], boundary2[segment][::-1])),
numpy.concatenate((position[segment], position[segment][::-1])))
series_xml = xml.SubElement(mark_xml, "polygon", points=" ".join(
["%r,%r" % (xi, yi) for xi, yi in coordinates]), style=_css_style(series_style))
if title is not None:
xml.SubElement(series_xml, "title").text = str(title)
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.AxisLines, _RenderContext)
def _render(axes, mark, context):
if mark._coordinate_axes[0] == "x":
p1 = "x1"
p2 = "x2"
b1 = "y1"
b2 = "y2"
position = axes._project_x(mark._table[mark._coordinates[0]])
boundary1 = axes._ymin_range
boundary2 = axes._ymax_range
elif mark._coordinate_axes[0] == "y":
p1 = "y1"
p2 = "y2"
b1 = "x1"
b2 = "x2"
position = axes._project_y(mark._table[mark._coordinates[0]])
boundary1 = axes._xmin_range
boundary2 = axes._xmax_range
mark_xml = xml.SubElement(
context.parent,
"g",
style=_css_style(
mark._style),
id=context.get_id(mark),
attrib={
"class": "toyplot-mark-AxisLines"})
series_xml = xml.SubElement(
mark_xml, "g", attrib={"class": "toyplot-Series"})
for dposition, dstroke, dopacity, dtitle in zip(
position,
mark._table[mark._stroke[0]],
mark._table[mark._opacity[0]],
mark._table[mark._title[0]],
):
dstyle = toyplot.style.combine(
{"stroke": toyplot.color.to_css(dstroke), "opacity": dopacity}, mark._style)
datum_xml = xml.SubElement(
series_xml,
"line",
attrib={
"class": "toyplot-Datum",
p1: repr(dposition),
p2: repr(dposition),
b1: repr(boundary1),
b2: repr(boundary2),
},
style=_css_style(dstyle),
)
if dtitle is not None:
xml.SubElement(datum_xml, "title").text = str(dtitle)
@dispatch(toyplot.mark.Mark)
def _legend_markers(mark):
return []
@dispatch((toyplot.canvas.Canvas, toyplot.coordinates.Cartesian), toyplot.mark.Legend, _RenderContext)
def _render(canvas, legend, context):
if not legend._entries:
return
entries = []
for entry in legend._entries:
label, spec = entry
if isinstance(spec, toyplot.mark.Mark):
markers = _legend_markers(spec)
elif isinstance(spec, list):
markers = spec
else:
markers = [spec]
entries.append((label, markers))
x = legend._xmin
y = legend._ymin
width = legend._xmax - legend._xmin
height = legend._ymax - legend._ymin
marker_height = (height - (legend._gutter * (len(entries) + 1))) / len(entries)
marker_width = marker_height
label_offset = (numpy.amax([len(markers) for label, markers in entries]) * (legend._gutter + marker_width)) + legend._gutter
xml.SubElement(
context.parent,
"rect",
x=repr(x),
y=repr(y),
width=repr(width),
height=repr(height),
style=_css_style(legend._style),
id=context.get_id(legend),
attrib={"class": "toyplot-mark-Legend"},
)
for i, (label, markers) in enumerate(entries):
marker_y = y + ((i + 1) * legend._gutter) + (i * marker_height)
for j, marker in enumerate(markers):
marker_x = x + label_offset - (len(markers) * (marker_width + legend._gutter)) + (j * (marker_width + legend._gutter))
_draw_marker(
context.parent,
marker_x + (marker_width / 2),
marker_y + (marker_height / 2),
min(marker_width, marker_height),
marker,
{},#computed_style,
{},
)
_draw_text(
root=context.parent,
text=label,
x=x + label_offset,
y=y + ((i + 1) * legend._gutter) + (i * marker_height) + (marker_height / 2),
style=legend._lstyle,
)
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.Graph, _RenderContext)
def _render(axes, mark, context): # pragma: no cover
# Project edge coordinates
for i in range(2):
if mark._coordinate_axes[i] == "x":
x = axes._project_x(mark._ecoordinates.T[i])
elif mark._coordinate_axes[i] == "y":
y = axes._project_y(mark._ecoordinates.T[i])
mark_xml = xml.SubElement(context.parent, "g", id=context.get_id(mark), attrib={"class": "toyplot-mark-Graph"})
#context.add_data(mark, mark._vtable, title="Graph Vertex Data", filename=mark._vertex_filename)
#context.add_data(mark, mark._etable, title="Graph Edge Data", filename=mark._edge_filename)
coordinate_index = 0
edge_xml = xml.SubElement(mark_xml, "g", attrib={"class": "toyplot-Edges"})
for esource, etarget, eshape, ecolor, ewidth, eopacity in zip(
mark._etable[mark._esource[0]],
mark._etable[mark._etarget[0]],
mark._etable[mark._eshape[0]],
mark._etable[mark._ecolor[0]],
mark._etable[mark._ewidth[0]],
mark._etable[mark._eopacity[0]],
):
estyle = toyplot.style.combine(
{
"fill": "none",
"stroke": toyplot.color.to_css(ecolor),
"stroke-width": ewidth,
"stroke-opacity": eopacity,
},
mark._estyle)
path = []
for segment in eshape:
if segment == "M":
count = 1
elif segment == "L":
count = 1
elif segment == "Q":
count = 2
elif segment == "C":
count = 3
path.append(segment)
for i in range(count):
path.append(str(x[coordinate_index]))
path.append(str(y[coordinate_index]))
coordinate_index += 1
xml.SubElement(
edge_xml,
"path",
d=" ".join(path),
style=_css_style(estyle),
)
# Project vertex coordinates
for i in range(2):
if mark._coordinate_axes[i] == "x":
x = axes._project_x(mark._vtable[mark._vcoordinates[i]])
elif mark._coordinate_axes[i] == "y":
y = axes._project_y(mark._vtable[mark._vcoordinates[i]])
vertex_xml = xml.SubElement(mark_xml, "g", attrib={"class": "toyplot-Vertices"})
for vx, vy, vmarker, vsize, vcolor, vopacity, vtitle in zip(
x,
y,
mark._vtable[mark._vmarker[0]],
mark._vtable[mark._vsize[0]],
mark._vtable[mark._vcolor[0]],
mark._vtable[mark._vopacity[0]],
mark._vtable[mark._vtitle[0]],
):
vstyle = toyplot.style.combine(
{
"fill": toyplot.color.to_css(vcolor),
"stroke": toyplot.color.to_css(vcolor),
"opacity": vopacity,
},
mark._vstyle)
_draw_marker(
vertex_xml,
vx,
vy,
vsize,
vmarker,
vstyle,
mark._vlstyle,
extra_class="toyplot-Datum",
title=vtitle,
)
# Render vertex labels
if mark._vlshow:
vlabel_xml = xml.SubElement(mark_xml, "g", attrib={"class": "toyplot-Labels"})
for dx, dy, dtext in zip(x, y, mark._vtable[mark._vlabel[0]]):
_draw_text(
root=vlabel_xml,
text=toyplot.compatibility.unicode_type(dtext),
x=dx,
y=dy,
style=mark._vlstyle,
attributes={"class": "toyplot-Datum"},
)
@dispatch(toyplot.mark.Plot)
def _legend_markers(mark):
markers = []
for stroke, stroke_width, stroke_opacity in zip(mark._stroke.T, mark._stroke_width.T, mark._stroke_opacity.T):
markers.append(
{
"shape": "/",
"mstyle": toyplot.style.combine(
{
"stroke": toyplot.color.to_css(stroke),
"stroke-width": stroke_width,
"stroke-opacity": stroke_opacity,
},
mark._style,
),
})
return markers
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.Plot, _RenderContext)
def _render(axes, mark, context):
position = mark._table[mark._coordinates[0]]
series = numpy.ma.column_stack([mark._table[key] for key in mark._series])
if mark._coordinate_axes[0] == "x":
position = axes._project_x(position)
series = axes._project_y(series)
elif mark._coordinate_axes[0] == "y":
position = axes._project_y(position)
series = axes._project_x(series)
mark_xml = xml.SubElement(
context.parent,
"g",
style=_css_style(toyplot.style.combine({"fill":"none"}, mark._style)),
id=context.get_id(mark),
attrib={
"class": "toyplot-mark-Plot"})
context.add_data(mark, mark._table, title="Plot Data", filename=mark._filename)
for series, stroke, stroke_width, stroke_opacity, stroke_title, marker, msize, mfill, mstroke, mopacity, mtitle in zip(
series.T,
mark._stroke.T,
mark._stroke_width.T,
mark._stroke_opacity.T,
mark._stroke_title.T,
[mark._table[key] for key in mark._marker],
[mark._table[key] for key in mark._msize],
[mark._table[key] for key in mark._mfill],
[mark._table[key] for key in mark._mstroke],
[mark._table[key] for key in mark._mopacity],
[mark._table[key] for key in mark._mtitle],
):
not_null = numpy.invert(numpy.logical_or(
numpy.ma.getmaskarray(position), numpy.ma.getmaskarray(series)))
segments = _flat_contiguous(not_null)
stroke_style = toyplot.style.combine(
{
"stroke": toyplot.color.to_css(stroke),
"stroke-width": stroke_width,
"stroke-opacity": stroke_opacity},
mark._style)
if mark._coordinate_axes[0] == "x":
x = position
y = series
elif mark._coordinate_axes[0] == "y":
x = series
y = position
series_xml = xml.SubElement(
mark_xml, "g", attrib={"class": "toyplot-Series"})
if stroke_title is not None:
xml.SubElement(series_xml, "title").text = str(stroke_title)
d = []
for segment in segments:
start, stop, step = segment.indices(len(not_null))
for i in range(start, start + 1):
d.append("M %r %r" % (x[i], y[i]))
for i in range(start + 1, stop):
d.append("L %r %r" % (x[i], y[i]))
xml.SubElement(
series_xml,
"path",
d=" ".join(d),
style=_css_style(stroke_style))
for dx, dy, dmarker, dsize, dfill, dstroke, dopacity, dtitle in zip(
x[not_null],
y[not_null],
marker[not_null],
msize[not_null],
mfill[not_null],
mstroke[not_null],
mopacity[not_null],
mtitle[not_null],
):
dstyle = toyplot.style.combine(
{
"fill": toyplot.color.to_css(dfill),
"stroke": toyplot.color.to_css(dstroke),
"opacity": dopacity},
mark._mstyle)
datum_xml = _draw_marker(
series_xml,
dx,
dy,
dsize,
dmarker,
dstyle,
mark._mlstyle,
extra_class="toyplot-Datum",
title=dtitle,
)
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.Rect, _RenderContext)
def _render(axes, mark, context):
if mark._coordinate_axes.tolist() == ["x", "y"]:
x1 = axes._project_x(mark._table[mark._left[0]])
x2 = axes._project_x(mark._table[mark._right[0]])
y1 = axes._project_y(mark._table[mark._top[0]])
y2 = axes._project_y(mark._table[mark._bottom[0]])
elif mark._coordinate_axes.tolist() == ["y", "x"]:
x1 = axes._project_x(mark._table[mark._top[0]])
x2 = axes._project_x(mark._table[mark._bottom[0]])
y1 = axes._project_y(mark._table[mark._left[0]])
y2 = axes._project_y(mark._table[mark._right[0]])
mark_xml = xml.SubElement(
context.parent,
"g",
style=_css_style(
mark._style),
id=context.get_id(mark),
attrib={
"class": "toyplot-mark-Rect"})
context.add_data(mark, mark._table, title="Rect Data", filename=mark._filename)
series_xml = xml.SubElement(
mark_xml, "g", attrib={"class": "toyplot-Series"})
for dx1, dx2, dy1, dy2, dfill, dopacity, dtitle in zip(
x1,
x2,
y1,
y2,
mark._table[mark._fill[0]],
mark._table[mark._opacity[0]],
mark._table[mark._title[0]],
):
dstyle = toyplot.style.combine(
{"fill": toyplot.color.to_css(dfill), "opacity": dopacity}, mark._style)
datum_xml = xml.SubElement(
series_xml,
"rect",
attrib={"class": "toyplot-Datum"},
x=repr(min(dx1, dx2)),
y=repr(min(dy1, dy2)),
width=repr(numpy.abs(dx1 - dx2)),
height=repr(numpy.abs(dy1 - dy2)),
style=_css_style(dstyle),
)
if dtitle is not None:
xml.SubElement(datum_xml, "title").text = str(dtitle)
@dispatch(toyplot.mark.Scatterplot)
def _legend_markers(mark):
markers = []
for marker, mfill, mstroke, mopacity in zip(
[mark._table[key] for key in mark._marker],
[mark._table[key] for key in mark._mfill],
[mark._table[key] for key in mark._mstroke],
[mark._table[key] for key in mark._mopacity],
):
for dmarker, dfill, dstroke, dopacity in zip(
marker,
mfill,
mstroke,
mopacity,
):
if isinstance(dmarker, toyplot.compatibility.string_type):
dmarker = {"shape": dmarker}
dmarker["mstyle"] = toyplot.style.combine(
dmarker.get("mstyle", None),
{
"fill": toyplot.color.to_css(dfill),
"stroke": toyplot.color.to_css(dstroke),
"opacity": dopacity,
},
mark._mstyle,
)
dmarker["lstyle"] = toyplot.style.combine(
dmarker.get("lstyle", None),
mark._mlstyle,
)
markers.append(dmarker)
break
return markers
@dispatch(toyplot.coordinates.Cartesian, toyplot.mark.Scatterplot, _RenderContext)
def _render(axes, mark, context):
dimension1 = numpy.ma.column_stack([mark._table[key] for key in mark._coordinates[0::2]])
dimension2 = numpy.ma.column_stack([mark._table[key] for key in mark._coordinates[1::2]])
if mark._coordinate_axes[0] == "x":
X = axes._project_x(dimension1)
Y = axes._project_y(dimension2)
elif mark._coordinate_axes[0] == "y":
X = axes._project_x(dimension2)
Y = axes._project_y(dimension1)
mark_xml = xml.SubElement(
context.parent,
"g",
style=_css_style(mark._style),
id=context.get_id(mark),
attrib={"class": "toyplot-mark-Scatterplot"},
)
context.add_data(mark, mark._table, title="Scatterplot Data", filename=mark._filename)
for x, y, marker, msize, mfill, mstroke, mopacity, mtitle in zip(
X.T,
Y.T,
[mark._table[key] for key in mark._marker],
[mark._table[key] for key in mark._msize],
[mark._table[key] for key in mark._mfill],
[mark._table[key] for key in mark._mstroke],
[mark._table[key] for key in mark._mopacity],
[mark._table[key] for key in mark._mtitle],
):
not_null = numpy.invert(numpy.logical_or(
numpy.ma.getmaskarray(x), numpy.ma.getmaskarray(y)))
series_xml = xml.SubElement(
mark_xml, "g", attrib={"class": "toyplot-Series"})
for dx, dy, dmarker, dsize, dfill, dstroke, dopacity, dtitle in zip(
x[not_null],
y[not_null],
marker[not_null],
msize[not_null],
mfill[not_null],
mstroke[not_null],
mopacity[not_null],
mtitle[not_null],
):
dstyle = toyplot.style.combine(
{
"fill": toyplot.color.to_css(dfill),
"stroke": toyplot.color.to_css(dstroke),
"opacity": dopacity,
},
mark._mstyle)
datum_xml = _draw_marker(
series_xml,
dx,
dy,
dsize,
dmarker,
dstyle,
mark._mlstyle,
extra_class="toyplot-Datum",
title=dtitle,
)
@dispatch((toyplot.canvas.Canvas, toyplot.coordinates.Cartesian), toyplot.mark.Text, _RenderContext)
def _render(parent, mark, context):
x = mark._table[mark._coordinates[numpy.where(mark._coordinate_axes == "x")[0][0]]]
y = mark._table[mark._coordinates[numpy.where(mark._coordinate_axes == "y")[0][0]]]
if isinstance(parent, toyplot.coordinates.Cartesian):
x = parent._project_x(x)
y = parent._project_y(y)
mark_xml = xml.SubElement(
context.parent,
"g",
style=_css_style(
mark._style),
id=context.get_id(mark),
attrib={
"class": "toyplot-mark-Text"})
context.add_data(mark, mark._table, title="Text Data", filename=mark._filename)
series_xml = xml.SubElement(
mark_xml, "g", attrib={"class": "toyplot-Series"})
for dx, dy, dtext, dangle, dfill, dopacity, dtitle in zip(
x,
y,
mark._table[mark._text[0]],
mark._table[mark._angle[0]],
mark._table[mark._fill[0]],
mark._table[mark._opacity[0]],
mark._table[mark._title[0]],
):
_draw_text(
root=series_xml,
text=toyplot.compatibility.unicode_type(dtext),
x=dx,
y=dy,
angle=dangle,
attributes={"class": "toyplot-Datum"},
style=toyplot.style.combine({"fill": toyplot.color.to_css(dfill), "opacity": dopacity}, mark._style),
title=dtitle,
)
@dispatch((toyplot.canvas.Canvas), toyplot.mark.Image, _RenderContext)
def _render(parent, mark, context):
import png
buffer = io.BytesIO()
data = mark._data
toyplot.log.debug("Image data: %s %s", data.shape, data.dtype)
if data.dtype == toyplot.color.dtype:
data = numpy.dstack((data["r"], data["g"], data["b"], data["a"]))
if issubclass(data.dtype.type, numpy.bool_):
bitdepth=1
elif issubclass(data.dtype.type, numpy.floating):
data = (data * 255.0).astype("uint8")
bitdepth=8
else:
bitdepth=8
width = data.shape[1]
height = data.shape[0]
greyscale = data.shape[2] < 3
alpha = data.shape[2] == 2 or data.shape[2] == 4
writer = png.Writer(width=width, height=height, greyscale=greyscale, alpha=alpha, bitdepth=bitdepth)
writer.write(buffer, numpy.reshape(data, (-1, data.shape[1] * data.shape[2])))
encoded = base64.standard_b64encode(buffer.getvalue()).decode("ascii")
mark_xml = xml.SubElement(
context.parent,
"g",
id=context.get_id(mark),
attrib={"class": "toyplot-mark-Image"},
)
xml.SubElement(
mark_xml,
"image",
x=repr(mark._xmin_range),
y=repr(mark._ymin_range),
width=repr(mark._xmax_range - mark._xmin_range),
height=repr(mark._ymax_range - mark._ymin_range),
attrib={"xlink:href": "data:image/png;base64," + encoded},
)