Source code for toyplot.mark

# 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

import collections
import itertools
import numpy
import toyplot.color
import toyplot.require


##########################################################################
# Basic Toyplot marks

[docs]class Mark(object): """Base class for all Toyplot marks. """ def __init__(self, annotation=False): self._annotation = False self.annotation = annotation @property def annotation(self): return self._annotation @annotation.setter def annotation(self, value): self._annotation = True if value else False def _finalize(self): return self
[docs] def domain(self, axis): """Return minimum and maximum domain values for the mark along the given axis. Parameters ---------- axis: string, required Name of an axis along which to return domain values. Returns ------- minimum: minimum domain value along the given axis, or `None`. maximum: maximum domain value along the given axis, or `None`. """ return (None, None)
[docs] def extents(self, axes): """Return range extents for the mark using the given axes. Parameters ---------- axes: sequence of strings, required Specifies the order in which domain coordinates must be returned. Returns ------- coordinates: tuple containing arrays of coordinates, in the order specified by the `axes` parameter. extents: (left, right, top, bottom) tuple of arrays containing the extents of each datum in range-space, relative to the domain coordinates. """ empty = numpy.array([]) return tuple([empty] * len(axes)), tuple([empty] * 4)
[docs]class AxisLines(Mark): """Render multiple lines parallel to an axis. Do not create AxisLines instances directly. Use factory methods such as :meth:`toyplot.coordinates.Cartesian.hlines` and :meth:`toyplot.coordinates.Cartesian.vlines` instead. """ def __init__( self, coordinate_axes, table, coordinates, stroke, opacity, title, style, annotation, ): Mark.__init__(self, annotation) # 1 axis identifier self._coordinate_axes = toyplot.require.string_vector(coordinate_axes, length=1) self._table = toyplot.require.instance(table, toyplot.data.Table) # 1 coordinate column self._coordinates = toyplot.require.table_keys(table, coordinates, length=1) # 1 stroke color column self._stroke = toyplot.require.table_keys(table, stroke, length=1) # 1 opacity column self._opacity = toyplot.require.table_keys(table, opacity, length=1) # 1 title column self._title = toyplot.require.table_keys(table, title, length=1) # Line style self._style = toyplot.require.style(style, allowed=toyplot.require.style.line)
[docs] def domain(self, axis): if axis == self._coordinate_axes: return toyplot.data.minimax([self._table[self._coordinates[0]]]) return (None, None)
[docs]class BarBoundaries(Mark): """Render multiple stacked bars defined by bar boundaries. Do not create BarBoundaries instances directly. Use factory methods such as :func:`toyplot.bars` or :meth:`toyplot.coordinates.Cartesian.bars` instead. """ def __init__( self, coordinate_axes, table, left, right, boundaries, fill, opacity, title, style, filename, ): Mark.__init__(self) # 2 axis identifiers self._coordinate_axes = toyplot.require.string_vector(coordinate_axes, length=2) self._table = toyplot.require.instance(table, toyplot.data.Table) # 1 coordinate column self._left = toyplot.require.table_keys(table, left, length=1) # 1 coordinate column self._right = toyplot.require.table_keys(table, right, length=1) # N bar boundary columns self._boundaries = toyplot.require.table_keys(table, boundaries, min_length=2) # N-1 fill color columns self._fill = toyplot.require.table_keys(table, fill, length=len(boundaries) - 1) # N-1 opacity columns self._opacity = toyplot.require.table_keys(table, opacity, length=len(boundaries) - 1) # N-1 title columns self._title = toyplot.require.table_keys(table, title, length=len(boundaries) - 1) # Bar style self._style = toyplot.require.style(style, allowed=toyplot.require.style.fill) # Export filename self._filename = toyplot.require.filename(filename)
[docs] def domain(self, axis): if axis == self._coordinate_axes[0]: return toyplot.data.minimax([self._table[self._left[0]], self._table[self._right[0]]]) if axis == self._coordinate_axes[1]: return toyplot.data.minimax([self._table[key] for key in self._boundaries])
[docs]class BarMagnitudes(Mark): """Render multiple stacked bars defined by bar magnitudes. Do not create BarMagnitudes instances directly. Use factory methods such as :func:`toyplot.bars` or :meth:`toyplot.coordinates.Cartesian.bars` instead. """ def __init__( self, coordinate_axes, table, left, right, baseline, magnitudes, fill, opacity, title, style, filename, ): Mark.__init__(self) # 2 axis identifiers self._coordinate_axes = toyplot.require.string_vector(coordinate_axes, length=2) self._table = toyplot.require.instance(table, toyplot.data.Table) # 1 coordinate column self._left = toyplot.require.table_keys(table, left, length=1) # 1 coordinate column self._right = toyplot.require.table_keys(table, right, length=1) # 1 baseline column self._baseline = toyplot.require.table_keys(table, baseline, length=1) # N bar magnitude columns self._magnitudes = toyplot.require.table_keys(table, magnitudes, min_length=1) # N fill color columns self._fill = toyplot.require.table_keys(table, fill, length=len(magnitudes)) # N opacity columns self._opacity = toyplot.require.table_keys(table, opacity, length=len(magnitudes)) # N title columns self._title = toyplot.require.table_keys(table, title, length=len(magnitudes)) # Bar style self._style = toyplot.require.style(style, allowed=toyplot.require.style.fill) # Export filename self._filename = toyplot.require.filename(filename)
[docs] def domain(self, axis): if axis == self._coordinate_axes[0]: return toyplot.data.minimax([self._table[self._left[0]], self._table[self._right[0]]]) if axis == self._coordinate_axes[1]: boundaries = numpy.column_stack([self._table[key] for key in self._magnitudes]) boundaries = numpy.column_stack((self._table[self._baseline[0]], boundaries)) boundaries = numpy.cumsum(boundaries, axis=1) return toyplot.data.minimax([boundaries])
[docs]class FillBoundaries(Mark): """Render multiple stacked fill regions defined by boundaries. Do not create FillBoundaries instances directly. Use factory methods such as :func:`toyplot.fill` or :meth:`toyplot.coordinates.Cartesian.fill` instead. """ def __init__( self, coordinate_axes, table, position, boundaries, fill, opacity, title, style, annotation, filename, ): Mark.__init__(self, annotation) # 2 axis identifiers self._coordinate_axes = toyplot.require.string_vector(coordinate_axes, length=2) self._table = toyplot.require.instance(table, toyplot.data.Table) # 1 coordinate column self._position = toyplot.require.table_keys(table, position, length=1) # N fill boundary columns self._boundaries = toyplot.require.table_keys(table, boundaries, min_length=2) # N-1 fill colors self._fill = toyplot.require.vector(fill, length=len(boundaries) - 1) # N-1 opacities self._opacity = toyplot.require.scalar_vector(opacity, length=len(boundaries) - 1) # N-1 titles #self._title = toyplot.require.object_vector(title, length=len(boundaries) - 1) self._title = title # Fill style self._style = toyplot.require.style(style, allowed=toyplot.require.style.fill) # Export filename self._filename = toyplot.require.filename(filename)
[docs] def domain(self, axis): if axis == self._coordinate_axes[0]: return toyplot.data.minimax([self._table[self._position[0]]]) if axis == self._coordinate_axes[1]: return toyplot.data.minimax([self._table[key] for key in self._boundaries])
[docs]class FillMagnitudes(Mark): """Render multiple stacked fill regions defined by magnitudes. Do not create FillMagnitudes instances directly. Use factory methods such as :func:`toyplot.fill` or :meth:`toyplot.coordinates.Cartesian.fill` instead. """ def __init__( self, coordinate_axes, table, position, baseline, magnitudes, fill, opacity, title, style, annotation, filename, ): Mark.__init__(self, annotation) # 2 axis identifiers self._coordinate_axes = toyplot.require.string_vector(coordinate_axes, length=2) self._table = toyplot.require.instance(table, toyplot.data.Table) # 1 coordinate column self._position = toyplot.require.table_keys(table, position, length=1) # 1 baseline column self._baseline = toyplot.require.table_keys(table, baseline, length=1) # N fill magnitude columns self._magnitudes = toyplot.require.table_keys(table, magnitudes) # N fill colors self._fill = toyplot.require.vector(fill, length=len(magnitudes)) # N opacities self._opacity = toyplot.require.scalar_vector(opacity, length=len(magnitudes)) # N titles #self._title = toyplot.require.object_vector(title, length=len(magnitudes)) self._title = title # Fill style self._style = toyplot.require.style(style, allowed=toyplot.require.style.fill) # Export filename self._filename = toyplot.require.filename(filename)
[docs] def domain(self, axis): if axis == self._coordinate_axes[0]: return toyplot.data.minimax([self._table[self._position[0]]]) if axis == self._coordinate_axes[1]: boundaries = numpy.column_stack([self._table[key] for key in self._magnitudes]) boundaries = numpy.column_stack((self._table[self._baseline[0]], boundaries)) boundaries = numpy.cumsum(boundaries, axis=1) return toyplot.data.minimax([boundaries])
[docs]class Graph(Mark): # pragma: no cover """Plot a graph (collection of vertices and edges). Do not create Graph instances directly. Use factory methods such as :meth:`toyplot.coordinates.Cartesian.graph` instead. """ def __init__( self, coordinate_axes, vtable, vid, vlabel, vcoordinates, vmarker, vsize, vcolor, vopacity, vtitle, vstyle, vlstyle, vlshow, etable, esource, etarget, eshape, ecoordinates, ecolor, ewidth, eopacity, estyle, ): Mark.__init__(self) # D axis identifiers self._coordinate_axes = toyplot.require.string_vector(coordinate_axes, min_length=1) self._vtable = toyplot.require.instance(vtable, toyplot.data.Table) # 1 vertex id column self._vid = toyplot.require.table_keys(vtable, vid, length=1) # 1 vertex label column self._vlabel = toyplot.require.table_keys(vtable, vlabel, length=1) # D coordinate columns self._vcoordinates = toyplot.require.table_keys(vtable, vcoordinates, length=len(self._coordinate_axes)) # 1 vertex marker column self._vmarker = toyplot.require.table_keys(vtable, vmarker, length=1) # 1 vertex marker size column self._vsize = toyplot.require.table_keys(vtable, vsize, length=1) # 1 vertex marker color column self._vcolor = toyplot.require.table_keys(vtable, vcolor, length=1) # 1 marker opacity column self._vopacity = toyplot.require.table_keys(vtable, vopacity, length=1) # 1 vertex titles column self._vtitle = toyplot.require.table_keys(vtable, vtitle, length=1) # Vertex marker style self._vstyle = toyplot.require.style(vstyle, allowed=toyplot.require.style.marker) # Vertex marker label style self._vlstyle = toyplot.require.style(vlstyle, allowed=toyplot.require.style.text) # Draw vertex labels self._vlshow = vlshow self._etable = toyplot.require.instance(etable, toyplot.data.Table) # 1 edge source column self._esource = toyplot.require.table_keys(etable, esource, length=1) # 1 edge target column self._etarget = toyplot.require.table_keys(etable, etarget, length=1) # 1 edge shape column self._eshape = toyplot.require.table_keys(etable, eshape, length=1) ecoordinate_count = 0 for shape in self._etable[self._eshape[0]]: for segment in shape: if segment == "M": ecoordinate_count += 1 elif segment == "L": ecoordinate_count += 1 elif segment == "Q": ecoordinate_count += 2 elif segment == "C": ecoordinate_count += 3 else: raise ValueError("Unknown edge shape segment: %s" % segment) # C x D edge coordinate columns self._ecoordinates = toyplot.require.scalar_matrix(ecoordinates, rows=ecoordinate_count, columns=len(self._coordinate_axes)) # 1 edge color column self._ecolor = toyplot.require.table_keys(etable, ecolor, length=1) # 1 edge width column self._ewidth = toyplot.require.table_keys(etable, ewidth, length=1) # 1 edge opacity column self._eopacity = toyplot.require.table_keys(etable, eopacity, length=1) # Edge style self._estyle = toyplot.require.style(estyle, allowed=toyplot.require.style.line)
[docs] def domain(self, axis): index = numpy.flatnonzero(self._coordinate_axes == axis)[0] return toyplot.data.minimax([self._vtable[self._vcoordinates[index]], self._ecoordinates[:, index]])
@property def vcount(self): """Return the number of vertices in the graph.""" return len(self._vtable) @property def vids(self): """Returns the graph vertex identifiers.""" return self._vtable[self._vid[0]] @property def vcoordinates(self): """Return the graph vertex coordinates.""" return numpy.column_stack([self._vtable[column] for column in self._vcoordinates]) @property def ecount(self): """Return the number of edges in the graph.""" return len(self._etable) @property def esources(self): return self._etable[self._esource[0]] @property def etargets(self): return self._etable[self._etarget[0]] @property def eshapes(self): return self._etable[self._eshape[0]] @property def ecoordinates(self): return self._ecoordinates @property def edges(self): """Return the graph edges as a :math:`E \\times 2` matrix of source, target indices.""" return numpy.column_stack((self._etable[self._esource[0]], self._etable[self._etarget[0]]))
[docs]class Image(Mark): """Plot a bitmap image. Do not create Image instances directly. Use factory methods such as :func:`toyplot.image` and :func:`toyplot.canvas.Canvas.image` instead. """ def __init__( self, xmin_range, xmax_range, ymin_range, ymax_range, data, ): Mark.__init__(self) self._xmin_range = xmin_range self._xmax_range = xmax_range self._ymin_range = ymin_range self._ymax_range = ymax_range data = numpy.atleast_3d(data) if data.ndim != 3: raise ValueError("Image must be a 1D, 2D or 3D array.") if data.shape[2] < 1 or data.shape[2] > 4: raise ValueError("Image must contain 1, 2, 3, or 4 channels.") if issubclass(data.dtype.type, (numpy.object_, numpy.complexfloating, numpy.flexible)) and data.dtype != toyplot.color.dtype: raise ValueError("Unsupported image dtype: %s" % data.dtype) self._data = data
[docs]class Plot(Mark): """Plot multiple bivariate data series using lines and/or markers. Do not create Plot instances directly. Use factory methods such as :func:`toyplot.plot`, :func:`toyplot.scatterplot`, :meth:`toyplot.coordinates.Cartesian.plot` and :meth:`toyplot.coordinates.Cartesian.scatterplot` instead. """ def __init__( self, coordinate_axes, table, coordinates, series, stroke, stroke_width, stroke_opacity, stroke_title, marker, msize, mfill, mstroke, mopacity, mtitle, style, mstyle, mlstyle, filename, ): Mark.__init__(self) # 2 axis identifiers self._coordinate_axes = toyplot.require.string_vector(coordinate_axes, length=2) self._table = toyplot.require.instance(table, toyplot.data.Table) # 1 coordinate column self._coordinates = toyplot.require.table_keys(table, coordinates, length=1) # N coordinate columns self._series = toyplot.require.table_keys(table, series, min_length=1) # N stroke colors self._stroke = toyplot.require.vector(stroke, length=len(series)) # N stroke widths self._stroke_width = toyplot.require.scalar_vector(stroke_width, length=len(series)) # N stroke opacities self._stroke_opacity = toyplot.require.scalar_vector(stroke_opacity, length=len(series)) # N stroke titles self._stroke_title = stroke_title # N marker columns self._marker = toyplot.require.table_keys(table, marker, length=len(series)) # N marker size columns self._msize = toyplot.require.table_keys(table, msize, length=len(series)) # N marker fill color columns self._mfill = toyplot.require.table_keys(table, mfill, length=len(series)) # N marker stroke color columns self._mstroke = toyplot.require.table_keys(table, mstroke, length=len(series)) # N marker opacity columns self._mopacity = toyplot.require.table_keys(table, mopacity, length=len(series)) # N marker title columns self._mtitle = toyplot.require.table_keys(table, mtitle, length=len(series)) # Line style self._style = toyplot.require.style(style, allowed=toyplot.require.style.line) # Marker style self._mstyle = toyplot.require.style(mstyle, allowed=toyplot.require.style.marker) # Marker label style self._mlstyle = toyplot.require.style(mlstyle, allowed=toyplot.require.style.text) # Export filename self._filename = toyplot.require.filename(filename)
[docs] def domain(self, axis): if axis == self._coordinate_axes[0]: return toyplot.data.minimax([self._table[self._coordinates[0]]]) if axis == self._coordinate_axes[1]: return toyplot.data.minimax([self._table[key] for key in self._series])
[docs]class Rect(Mark): """Plot axis-aligned rectangles. Do not create Rect instances directly. Use factory methods such as :meth:`toyplot.coordinates.Cartesian.rects` instead. """ def __init__( self, coordinate_axes, table, left, right, top, bottom, fill, opacity, title, style, filename, ): Mark.__init__(self) # 2 axis identifiers self._coordinate_axes = toyplot.require.string_vector(coordinate_axes, length=2) self._table = toyplot.require.instance(table, toyplot.data.Table) # 1 coordinate column self._left = toyplot.require.table_keys(table, left, length=1) # 1 coordinate column self._right = toyplot.require.table_keys(table, right, length=1) # 1 coordinate column self._top = toyplot.require.table_keys(table, top, length=1) # 1 coordinate column self._bottom = toyplot.require.table_keys(table, bottom, length=1) # 1 fill color column self._fill = toyplot.require.table_keys(table, fill, length=1) # 1 opacity column self._opacity = toyplot.require.table_keys(table, opacity, length=1) # 1 title column self._title = toyplot.require.table_keys(table, title, length=1) # Rectangle style self._style = toyplot.require.style(style, allowed=toyplot.require.style.fill) # Export filename self._filename = toyplot.require.filename(filename)
[docs] def domain(self, axis): if axis == self._coordinate_axes[0]: return toyplot.data.minimax([self._table[self._left[0]], self._table[self._right[0]]]) if axis == self._coordinate_axes[1]: return toyplot.data.minimax([self._table[self._top[0]], self._table[self._bottom[0]]])
[docs]class Scatterplot(Mark): """Plot multivariate data series using markers. Do not create Scatterplot instances directly. Use factory methods such as :func:`toyplot.scatterplot` and :meth:`toyplot.coordinates.Cartesian.scatterplot` instead. """ def __init__( self, coordinate_axes, table, coordinates, marker, msize, mfill, mstroke, mopacity, mtitle, style, mstyle, mlstyle, filename, ): Mark.__init__(self) # D axis identifiers self._coordinate_axes = toyplot.require.string_vector(coordinate_axes, min_length=1) D = len(self._coordinate_axes) self._table = toyplot.require.instance(table, toyplot.data.Table) # D * N coordinate columns self._coordinates = toyplot.require.table_keys(table, coordinates, modulus=D) N = len(self._coordinates) / D # N marker columns self._marker = toyplot.require.table_keys(table, marker, length=N) # N marker size columns self._msize = toyplot.require.table_keys(table, msize, length=N) # N marker fill color columns self._mfill = toyplot.require.table_keys(table, mfill, length=N) # N marker stroke color columns self._mstroke = toyplot.require.table_keys(table, mstroke, length=N) # N marker opacity columns self._mopacity = toyplot.require.table_keys(table, mopacity, length=N) # N marker title columns self._mtitle = toyplot.require.table_keys(table, mtitle, length=N) # Global style self._style = toyplot.require.style(style, allowed=set()) # Marker style self._mstyle = toyplot.require.style(mstyle, allowed=toyplot.require.style.marker) # Marker label style self._mlstyle = toyplot.require.style(mlstyle, allowed=toyplot.require.style.text) # Export filename self._filename = toyplot.require.filename(filename)
[docs] def domain(self, axis): columns = [coordinate_column for coordinate_axis, coordinate_column in zip(itertools.cycle(self._coordinate_axes), self._coordinates) if coordinate_axis == axis] return toyplot.data.minimax([self._table[column] for column in columns])
[docs]class Text(Mark): """Render text. Do not create Text instances directly. Use factory methods such as :meth:`toyplot.canvas.Canvas.text` or :meth:`toyplot.coordinates.Cartesian.text` instead. """ def __init__( self, coordinate_axes, table, coordinates, text, angle, fill, opacity, title, style, annotation, filename, ): Mark.__init__(self, annotation) # D axis identifiers self._coordinate_axes = toyplot.require.string_vector(coordinate_axes, min_length=1) self._table = toyplot.require.instance(table, toyplot.data.Table) # D coordinate columns self._coordinates = toyplot.require.table_keys(table, coordinates, length=len(self._coordinate_axes)) # 1 text column self._text = toyplot.require.table_keys(table, text, length=1) # 1 angle column self._angle = toyplot.require.table_keys(table, angle, length=1) # 1 fill color column self._fill = toyplot.require.table_keys(table, fill, length=1) # 1 opacity column self._opacity = toyplot.require.table_keys(table, opacity, length=1) # 1 title column self._title = toyplot.require.table_keys(table, title, length=1) # Text style self._style = toyplot.style.combine( { "alignment-baseline": "middle", "font-size": "12px", "font-weight": "normal", "stroke": "none", "text-anchor": "middle", }, toyplot.require.style(style, allowed=toyplot.require.style.text), ) # Export filename self._filename = toyplot.require.filename(filename)
[docs] def domain(self, axis): for index, coordinate_axis in enumerate(self._coordinate_axes): if coordinate_axis == axis: return toyplot.data.minimax(self._table[self._coordinates[index]])
[docs] def extents(self, axes): axis_map = {key: index for index, key in enumerate(self._coordinate_axes)} coordinates = tuple([self._table[self._coordinates[axis_map[axis]]] for axis in axes]) extents = toyplot.text.extents(self._table[self._text[0]], self._table[self._angle[0]], self._style) return coordinates, extents
########################################################################## # More specialized marks
[docs]class Legend(Mark): """Render a figure legend (a collection of markers and labels). Do not create Legend instances directly. Use factory methods such as :meth:`toyplot.canvas.Canvas.legend` or :meth:`toyplot.coordinates.Cartesian.legend` instead. """ def __init__(self, xmin, xmax, ymin, ymax, entries, style, lstyle): Mark.__init__(self) self._xmin = xmin self._xmax = xmax self._ymin = ymin self._ymax = ymax self._gutter = 10 self._entries = entries # Styles the box surrounding the legend self._style = toyplot.style.combine( { "fill": "none", "stroke": "none", }, toyplot.require.style(style, allowed=toyplot.require.style.fill), ) # Styles the legend labels self._lstyle = toyplot.style.combine( { "alignment-baseline": "middle", "font-size":"12px", "stroke":"none", }, toyplot.require.style(lstyle, allowed=toyplot.require.style.text), )