# Copyright (C) 2016-2024 Jan Berges
# This program is free software under the terms of the BSD Zero Clause License.
"""Figure object."""
from __future__ import division
import math
import re
from .calc import power_of_ten, xround_mantissa, multiples
from .color import Color, colormap, colorize
from .convert import inch, pt, csv
from .cut import relevant, shortcut, cut2d, jump
from .fatband import fatband, miter_butt
from .files import goto, typeset, rasterize
from .group import islands, groups
from .png import save
[docs]
class Plot():
"""Plot object.
Parameters
----------
width : float, default None
Figure width in cm. A negative value is interpreted as the inner width
(without left and right margins). If zero, the x-axis scale is set
equal to the y-axis scale and the width is inferred from the height. By
default, the single-column width for the chosen `style` is used.
height : float, default None
Figure height in cm. A negative value is interpreted as the inner
height (without bottom and top margins). If zero, the y-axis scale is
set equal to the x-axis scale and the height is inferred from the
width. By default, it is inferred from `width` for a 4:3 aspect ratio.
margin : float, default None
Default margin in cm. If ``None``, margins are set automatically. This
is not always the best option.
xyaxes : bool, default True
Draw x and y axes?
style : str, default None
Predefined style. Possible values are ``'APS'``, ``'NanoLett'``,
``'NatCommun'``, and ``'Nature'``. This changes some of the below
default values.
rounded : bool, default True
Use ``round`` as default value for ``line cap`` and ``line join``?
Otherwise the TikZ initial values ``miter`` and ``butt`` are used.
**more
Initial values of attributes (see below) or global TikZ options.
Attributes
----------
left : float, default `margin`
Left margin in cm.
right : float, default `margin`
Right margin in cm.
bottom : float, default `margin`
Bottom margin in cm.
top : float, default `margin`
Top margin in cm.
margmin : float, default 0.15
Minimum automatic margin in cm.
ratio : float, default None
Figure width divided by figure height. The desired aspect ratio is
obtained by adding extra margins as needed.
align : float, default 0.5
If `ratio` is used, this value aligns the original plot relative to the
new viewport. A value of ``0.0``, ``0.5``, and ``1.0`` moves it to the
lower side, to the center, and to the upper side, respectively.
xlabel, ylabel, zlabel : str, default None
Axis labels.
xticks, yticks, zticks : list, default None
List of ticks, e.g., ``[0, (0.5, '$\\\\frac12$'), 1]``. If the label is
``None``, the tick mark is not drawn (but possibe grid lines are). If it
otherwise evaluates to ``False``, the tick mark but no label is drawn.
xmarks, ymarks, zmarks : bool, default True
Show tick marks and labels?
xlabels, ylabels, zlabels : bool, default True
Show tick labels?
xspacing, yspacing, zspacing : float, default 1.0
Approximate tick spacing in cm.
xstep, ystep, zstep : float, default None
Exact tick increment.
xminorticks, yminorticks : list, default None
List of minor-tick positions. There is currently no intelligent way to
set the minor ticks depending on the major ticks. However, minor ticks
that fall on major ticks are omitted.
xminormarks, yminormarks : bool, default False
Show minor tick marks?
xminorspacing, yminorspacing : float, default None
Approximate minor-tick spacing in cm.
xminorstep, yminorstep : float, default None
Exact minor-tick increment.
xmin, ymin, zmin : float, default None
Lower axis limit.
xmax, ymax, zmax : float, default None
Upper axis limit.
dleft, dright, dbottom, dtop : float, default 0.0
Protrusion of x- and y-axis limits into margins in cm.
xpadding, ypadding, zpadding : float, default 0.0
Padding between data and axes in data units.
xclose, yclose, zclose : bool, default False
Place axis labels in space reserved for tick labels.
xformat, yformat, zformat : function
Tick formatter. Takes tick position as argument.
lower : str, default 'blue'
Lower color of colorbar. Can also be of type ``Color`` as long as
`upper` has the same type.
upper : str, default 'red'
Upper color of colorbar. Can also be of type ``Color`` as long as
`lower` has the same type.
cmap : function, default None
Colormap for colorbar used instead of `lower` and `upper`.
title : str, default None
Plot title.
label : str, default None
Subfigure label, e.g., ``'a'``.
labelsize : int, default None
Different font size for subfigure label in pt.
labelformat : function, default None
Formatter for subfigure label. Takes `label` as argument.
labelopt : str, default 'inner sep=0pt, below right'
Label options, e.g., for orientation.
labelpos : str, default 'LT'
Label position, a combination of ``lcrbmtLCRBMT`` or a tuple of data
coordinates.
lali : str, default 'center'
Alignment of legend entries.
lbls : str, default '\\\\\\\\baselineskip'
Line height of legend entries.
lbox : bool, default False
Draw box around legend?
lcol : int, default 1
Number of columns in legend.
llen : str, default '4mm'
Length of example lines next to labels.
lopt : str, default None
Legend options, e.g., for orientation.
lpos : str, default 'cm'
Legend position, a combination of ``lcrbmtLCRBMT`` or a tuple of data
coordinates.
lput : bool, default True
Draw legend?
lrow : int, default 0
Number of rows in legend.
lsep : str, default None
Space between legend title and entries, e.g., ``'6pt'``.
ltop : str, default None
Legend title. If `lbox` is used, you might want to prevent that TikZ
pictures in the legend inherit the ``rounded corners`` of the box by
prepending ``\\tikzset{sharp corners}`` to this value.
lwid : float, default 4.0
Width of legend columns in units of `llen`.
tick : float, default 0.07
Length of tick marks in cm.
minortick : float, default 0.04
Length of minor tick marks in cm.
gap : float, default 0.15
Gap between plot area and colorbar in cm.
bar : float, default 0.15
Width of color bar in cm.
tip : float, default 0.1
Overlap of axis tips in cm.
xaxis : bool, default `xyaxes`
Draw x axis?
yaxis : bool, default `xyaxes`
Draw y axis?
xorigin : float, default None
Horizontal position of y axis in data coordinates. A reasonable value
could be zero. By default, the y axis is shown on the left.
yorigin : float, default None
Vertical position of x axis in data coordinates. A reasonable value
could be zero. By default, the x axis is shown on the bottom.
frame : bool, default `xyaxes`
Draw frame around plot area?
grid : bool, default False
Add grid lines at tick positions?
minorgrid : bool, default False
Add grid lines at minor-tick positions?
colorbar : bool or str, default None
Draw colorbar? If ``None``, the colorbar is drawn if any line is given a
z value or if both `zmin` and `zmax` are given. Alternatively, the path
to an image with a color gradient can be specified. Here, an image width
of one pixel is sufficient.
outline : bool, default False
Draw dashed figure outline?
canvas : str, default None
Background color of whole document.
background : str, default None
Path to background image.
preamble : str, default ''
Definitions for standalone figures.
inputenc : str, default None
Text encoding, e.g., ``'utf8'``.
fontenc : str, default None
Font encoding. The default is ``'T1'`` if `font` is specified, none
otherwise.
font : str, default None
Predefined font selection. Imitates well-known fonts. Possible values
are ``'Gill Sans'``, ``'Helvetica'``, ``'Iwona'``, ``'Latin Modern'``,
``'Times'``, and ``'Utopia'``.
fontsize : int, default 10
Font size for standalone figures in pt.
single : float, default 8.0
Single-column width for the chosen `style`.
double : float, default 17.0
Full textwidth for the chosen `style`.
resolution : float, default 1e-3
Smallest distance in cm expected to be discernible when looking at the
plot. The default is acceptable when the plot is viewed or printed in
its original size. For zooming, smaller values may be necessary. This
parameter determines the number of vertices used to render a line and
thus affects the file size.
eps : float, default 1e-4
Distance from plot boundary in cm beyond which a mark or grid line is
considered to lie outside of the plot area (and is potentially cut off).
This tolerance is meant to make up for the limited numerical precision.
For example, using double preicision, ``3 * 0.1 > 0.3``.
lines : list
List of all line objects.
options : dict
Global TikZ options.
Notes
-----
In all textual attributes and parameters, numbers in angle brackets are
interpreted as values in y data units, e.g., ``line_width='<0.1>'``. For
the parameters `line_width` and `mark_size` this is also the case if an
integer or float is passed instead of a string.
"""
def __init__(self, width=None, height=None, margin=None, xyaxes=True,
style=None, rounded=True, **more):
self.width = width
self.height = height
self.left = margin
self.right = margin
self.bottom = margin
self.top = margin
self.dleft = 0.0
self.dright = 0.0
self.dbottom = 0.0
self.dtop = 0.0
self.margmin = 0.15
self.ratio = None
self.align = 0.5
for x in 'xyz':
setattr(self, x + 'label', None)
setattr(self, x + 'ticks', None)
setattr(self, x + 'marks', True)
setattr(self, x + 'labels', True)
setattr(self, x + 'spacing', 1.0)
setattr(self, x + 'step', None)
setattr(self, x + 'min', None)
setattr(self, x + 'max', None)
setattr(self, x + 'padding', 0.0)
setattr(self, x + 'close', False)
setattr(self, x + 'format',
lambda x: ('%g' % x).replace('-', '\\smash{\\llap\\textminus}'))
for x in 'xy':
setattr(self, x + 'minorticks', None)
setattr(self, x + 'minormarks', False)
setattr(self, x + 'minorspacing', None)
setattr(self, x + 'minorstep', None)
self.lower = 'blue'
self.upper = 'red'
self.cmap = None
self.title = None
self.label = None
self.labelsize = None
self.labelformat = None
self.labelopt = 'inner sep=0pt, below right'
self.labelpos = 'LT'
self.lali = 'center'
self.lbls = '\\baselineskip'
self.lbox = False
self.lcol = 1
self.llen = '4mm'
self.lopt = None
self.lpos = 'cm'
self.lput = True
self.lrow = 0
self.lsep = None
self.ltop = None
self.lwid = 4.0
self.tick = 0.07
self.minortick = 0.04
self.gap = 0.15
self.bar = 0.15
self.tip = 0.1
self.xaxis = xyaxes
self.yaxis = xyaxes
self.xorigin = None
self.yorigin = None
self.frame = xyaxes
self.grid = False
self.minorgrid = False
self.colorbar = None
self.outline = False
self.canvas = None
self.background = None
self.preamble = ''
self.inputenc = None
self.fontenc = None
self.font = None
self.fontsize = 10
self.single = 8.0
self.double = 17.0
self.resolution = 1e-3
self.eps = 0.1 * self.resolution
self.lines = []
self.options = dict(mark_size='0.05cm')
if rounded:
self.options.update(line_cap='round', line_join='round')
if style is not None:
if style == 'APS':
self.font = 'Times'
self.fontsize = 9
self.labelformat = lambda x: '(%s)' % x
self.single = 8.6
self.double = 17.8
elif style == 'NanoLett':
self.font = 'Helvetica'
self.fontsize = 9
self.labelformat = lambda x: '(%s)' % x
self.single = 3.33 * inch
self.double = 7.0 * inch
elif style == 'NatCommun':
self.font = 'Helvetica'
self.fontsize = 8
self.labelsize = 9
self.labelformat = lambda x: '\\textbf{%s}' % x
self.single = 8.8
self.double = 18.0
elif style == 'Nature':
self.font = 'Helvetica'
self.fontsize = 7
self.labelsize = 8
self.labelformat = lambda x: '\\textbf{%s}' % x
self.single = 8.9
self.double = 18.3
if self.width is None:
self.width = self.single
if self.height is None:
self.height = 3 * self.width / 4
for name, value in more.items():
if hasattr(self, name):
setattr(self, name, value)
else:
self.options[name] = value
[docs]
def line(self,
x=[],
y=[],
z=None,
axes=False,
code=None,
cut=False,
frame=False,
grid=False,
join=None,
jump=0,
label=None,
miter=False,
nib=None,
omit=None,
protrusion=0,
sgn=+1,
shifts=None,
shortcut=0,
shortcut_rel=0.5,
thickness=0.05,
weights=None,
xref=None,
yref=None,
zindex=None,
**options):
"""Add line/curve.
Parameters
----------
x, y : list or float
Coordinates of data points.
z : float, default None
z value for entire line, represented by color.
axes : bool, default False
Draw axes at current z index? By default, the axes are drawn first,
i.e., below all data.
code : str, default None
Literal TikZ code to be inserted at current position.
cut : bool or tuple, default False
Cut off line segments beyond plotting range? It is also possible to
pass the clipping window as a tuple ``(xmin, xmax, ymin, ymax)`` in
data coordinates, where ``None`` is replaced by the plot bounds.
frame : bool, default False
Draw frame at current z index? By default, the frame is drawn just
below the axes.
grid : bool, default False
Add grid lines (at positions of major and possibly minor ticks) at
current z index? By default, the grid is drawn just below the frame.
join : bool, default None
Join cut-up line segments along edge of plotting range? By default,
this is ``True`` if any ``fill`` is specified, ``False`` otherwise.
jump : float, default 0
Shortest distance between consecutive data points that is
considered as a discontinuity.
label : str, default None
Label for legend entry. The special label ``*next*`` adds a second
example line with the current line style to the next legend entry.
miter : bool, default False
Draw fatbands using `miter_butt` function? If ``False``, the
`fatband` function is used.
nib : float, default None
Angle of broad pen nib. If ``None``, the nib is held perpendicular
to the direction of the current line segment.
omit : bool, default None
Remove irrelevant vertices of linear spline? The default is
``False`` if the TikZ option ``mark`` is set, ``True`` otherwise.
protrusion : float, default 0
Extend curve linearly at both ends? This may improve the appearance
of fatbands ending at the edge of the plotting range.
sgn : integer, default +1
Direction of fatband outline. Coinciding outlines should be drawn
in the same direction if `omit` is ``True``.
shifts : list of float
Displacements in weight direction of fatband.
shortcut : float, default 0
Maximum length of loop to be cut off.
shortcut_rel : float, default 0.5
Maximum length of loop to be cut off relative to the total length
of the curve. This is only used if `shortcut` is nonzero.
thickness : float, default 0.05
Fatband linewidth in cm.
weights : list of float
Fatband weights.
xref, yref : float, default None
Reference values for filled curves. This is useful to visualize
integrands such as a density of states.
zindex : int, default None
Index of list of lines where new line is inserted. By default, the
new line is appended to the list, i.e., has the highest `zindex`.
**options
Local TikZ options.
"""
if not hasattr(x, '__len__'):
x = [x]
if not hasattr(y, '__len__'):
y = [y]
new_line = dict(
x=x,
y=y,
z=z,
axes=axes,
code=code,
cut=cut,
frame=frame,
grid=grid,
join=join,
jump=jump,
label=label,
miter=miter,
nib=nib,
omit=omit,
protrusion=protrusion,
sgn=sgn,
shifts=shifts,
shortcut=shortcut,
shortcut_rel=shortcut_rel,
thickness=thickness,
weights=weights,
xref=xref,
yref=yref,
options=options,
)
if zindex is None:
self.lines.append(new_line)
else:
self.lines.insert(zindex, new_line)
[docs]
def fatband(self, x, y, weights=1.0, shifts=0.0, fill=True, draw='none',
**options):
"""Draw fatband.
Parameters
----------
x, y : list
Vertices of linear spline.
weights : list of float or float, default 1.0
Weights of `x` and `y`.
shifts : list of float or float, default 0.0
Displacements in weight direction.
fill, draw : str or Color
TikZ line options (filled without outline by default).
**options
Options passed to `line` function.
"""
try:
iter(weights)
except TypeError:
weights = [weights] * len(x)
try:
iter(shifts)
except TypeError:
shifts = [shifts] * len(x)
for island in islands(len(weights),
lambda n: any(weights[max(n - 1, 0):n + 2])):
if len(island) > 1:
n = slice(island[0], island[-1] + 1)
self.line(x[n], y[n], weights=weights[n], shifts=shifts[n],
fill=fill, draw=draw, **options)
[docs]
def compline(self, x, y, weights=1.0, colors=True, threshold=0.0,
**options):
"""Represent points of multiple weights as composite fatband.
Parameters
----------
x, y : list of float
Coordinates of line vertices.
weights : list of tuple of float, list of float, or float, default 1.0
Weights of vertices. The corresponding linewidth is always measured
perpendicular to the direction of the line; This ensures that lines
of the same weight have the same thickness regardless of direction.
colors : list of str or str, default True
Colors of different components. Any objects whose representations
as a string are valid LaTeX colors can be used. If ``True``, the
fill color is the same as the stroke color.
threshold : float, default 0.0
Minimum displayed weight.
**options
Further line options.
"""
try:
weights = [[0 if part < threshold else part for part in parts]
for parts in weights]
except TypeError:
try:
weights = [[0 if part < threshold else part]
for part in weights]
except TypeError:
weights = [[0 if weights < threshold else weights]] * len(x)
try:
iter(colors)
except TypeError:
colors = [colors] * len(x)
shifts = []
for parts in weights:
shifts.append([0])
shift = shifts[-1]
for part in parts:
shift.append(shift[-1] + part)
for m, part in enumerate(parts):
shift[m] -= (shift[-1] - part) / 2
sgn = +1
for weights, shifts, color in zip(zip(*weights), zip(*shifts), colors):
self.fatband(x, y, weights, shifts, fill=color, sgn=sgn, **options)
sgn *= -1
[docs]
def node(self, x, y, content, name=None, **options):
"""Draw (text) node at given position.
Parameters
----------
x, y : float
Node position in data coordinates.
content : str
Node content.
name : str
Name/label to refer back to the node.
**options
TikZ options of the node, e.g., ``above left=True``.
"""
self.code('\\node%s%s at (<x=%.14g>, <y=%.14g>) {%s};'
% (' (%s)' % name if name else '', csv(options), x, y, content))
[docs]
def cut(self, x=0.0, y=0.0):
"""Indicate broken axis.
Parameters
----------
x, y : float
Position of the break symbol.
"""
self.axes()
for cmd, color, to in ('fill', 'white', '--'), ('draw', 'black', ' '):
self.code('\\%s [%s, xshift=<x=%.14g>cm, yshift=<y=%.14g>cm] '
'(-0.1, -0.15) -- (0.1, 0.05) %s (0.1, 0.15) -- (-0.1, -0.05);'
% (cmd, color, x, y, to))
[docs]
def point(self, x, y, name):
"""Define point.
Parameters
----------
x, y : float
Point position in data coordinates.
name : str
Name/label to refer back to the point.
"""
self.code('\\coordinate (%s) at (<x=%.14g>, <y=%.14g>);' % (name, x, y))
[docs]
def image(self, filename, x1, y1, x2, y2, **options):
"""Insert image between given data coordinates.
Parameters
----------
filename : str
File name of image.
x1, y1 : float
Position of bottom-left corner in data coordinates.
x2, y2 : float
Position of top-right corner in data coordinates.
**options
Options passed to `line`.
"""
if x1 < x2:
xscale = 1
else:
xscale = -1
x1, x2 = x2, x1
if y1 < y2:
yscale = 1
else:
yscale = -1
y1, y2 = y2, y1
graphics = ('\\includegraphics[width=<dx=%g>cm, height=<dy=%g>cm]{%s}'
% (x2 - x1, y2 - y1, filename))
if not xscale == yscale == 1:
graphics = '\\scalebox{%s}[%s]{%s}' % (xscale, yscale, graphics)
self.code('\\node at (<x=%g>, <y=%g>) '
'[anchor=south west, inner sep=0, outer sep=0] {%s};'
% (x1, y1, graphics), **options)
[docs]
def code(self, data, **options):
"""Insert literal TikZ code.
Parameters
----------
data : str
TikZ code. Positions and distances in data coordinates and units
can be specified using angle brackets, e.g., ``(<x=1>, <y=2>)`` or
``+(<dx=1>, <dy=2>)``.
**options
Options passed to `line`.
"""
self.line(code=data, **options)
[docs]
def axes(self, **options):
"""Draw axes at current z index."""
self.line(axes=True, **options)
[docs]
def nolabel(self):
"""Pass empty entry to legend (as spacer between entries)."""
self.line(draw='none', label='')
[docs]
def clear(self):
"""Remove all lines from plot."""
self.lines = []
[docs]
def save(self, filename, external=False, standalone=False, pdf=False,
png=False, dpi=300.0, width=0, height=0, rewrite=False,
engine='pdflatex'):
"""Save plot to file.
Parameters
----------
filename : str
File name. If no period is contained, the ``.tex`` extension
may be omitted.
external : bool, default False
Provide file name to TikZ library ``external``.
standalone : bool, default False
Create file that can be typeset with ``pdflatex``, i.e., include
document header etc.?
pdf : bool, default False
Typeset TeX file via ``pdflatex``? This implies `standalone`.
Automatically set to ``True`` if `filename` ends with ``.pdf``.
png : bool, default False
Rasterize PDF file via ``pdftoppm``? This implies `pdf`.
Automatically set to ``True`` if `filename` ends with ``.png``.
dpi : float, default 300.0
Image resolution in dots per inch.
width, height : int
Image dimensions in pixels. If either `width` or `height` is zero,
it will be determined by the aspect ratio of the image. If both are
zero, they will also be determined by `dpi`.
rewrite : bool, default False
Rewrite resulting PNG file using StoryLines? This will remove
possible metadata and may reduce the file size but currently is
quite slow.
engine : str, default 'pdflatex'
TeX typesetting engine.
"""
# determine data limits:
lower = {}
upper = {}
for x in 'xyz':
xmin = getattr(self, x + 'min')
xmax = getattr(self, x + 'max')
if xmin is None or xmax is None:
if x == 'z':
X = [line[x] for line in self.lines if line[x] is not None]
else:
X = [value for line in self.lines for value in line[x]]
if x == 'z' and self.colorbar is None:
self.colorbar = xmin is not None and xmax is not None or bool(X)
lower[x] = xmin if xmin is not None else min(X) if X else 0.0
upper[x] = xmax if xmax is not None else max(X) if X else 0.0
lower[x] -= getattr(self, x + 'padding')
upper[x] += getattr(self, x + 'padding')
# embed zero- and one-dimensional data:
if lower[x] == upper[x]:
padding = power_of_ten(lower[x])
lower[x] -= padding
upper[x] += padding
# choose automatic margins:
baselineskip = 1.2 * self.fontsize * pt
if self.bottom is None:
self.bottom = self.margmin
if self.xaxis:
xticks = ((self.xticks is None or bool(self.xticks))
and self.xmarks and self.xlabels)
if self.xlabel or xticks:
self.bottom += self.tick + baselineskip
if self.xlabel and xticks and not self.xclose:
self.bottom += baselineskip
if self.left is None:
self.left = self.margmin
if self.yaxis:
yticks = ((self.yticks is None or bool(self.yticks))
and self.ymarks and self.ylabels)
if self.ylabel or yticks:
self.left += self.tick + baselineskip
if self.ylabel and yticks and not self.yclose:
self.left += baselineskip
if self.right is None:
self.right = self.margmin
if self.colorbar:
self.right += self.gap + self.bar
zticks = ((self.zticks is None or bool(self.zticks))
and self.zmarks and self.zlabels)
if self.zlabel or zticks:
self.right += baselineskip
if self.zlabel and zticks and not self.zclose:
self.right += baselineskip
if self.top is None:
self.top = self.margmin
if self.title:
self.top += baselineskip * (self.title.count('\\\\') + 1)
# interpret negative as inner dimensions:
if self.width < 0:
self.width = -self.width + self.left + self.right
if self.height < 0:
self.height = -self.height + self.bottom + self.top
# temporarily subtract protrusions from margins:
self.left -= self.dleft
self.right -= self.dright
self.bottom -= self.dbottom
self.top -= self.dtop
# determine extent of the plotting area:
extent = {}
extent['x'] = self.width - self.left - self.right
extent['y'] = self.height - self.bottom - self.top
# determine width or height for proportional plot:
if not self.height:
extent['y'] = (upper['y'] - lower['y']) * extent['x'] \
/ (upper['x'] - lower['x'])
self.height = extent['y'] + self.bottom + self.top
elif not self.width:
extent['x'] = (upper['x'] - lower['x']) * extent['y'] \
/ (upper['y'] - lower['y'])
self.width = extent['x'] + self.left + self.right
# determine scale:
scale = {}
for x in extent.keys():
# how many centimeters correspond to one unit of the axis?
scale[x] = extent[x] / (upper[x] - lower[x])
# add protrusions back to margins:
self.left += self.dleft
self.right += self.dright
self.bottom += self.dbottom
self.top += self.dtop
lower['x'] += self.dleft / scale['x']
upper['x'] -= self.dright / scale['x']
lower['y'] += self.dbottom / scale['y']
upper['y'] -= self.dtop / scale['y']
extent['x'] -= self.dleft + self.dright
extent['y'] -= self.dbottom + self.dtop
# take care of z-axis:
extent['z'] = extent['y']
scale['z'] = extent['z'] / (upper['z'] - lower['z'])
# set aspect ratio by adjusting margins:
if self.ratio is not None:
ratio = self.width / self.height
if ratio < self.ratio:
diff = self.height * self.ratio - self.width
self.width += diff
self.left += self.align * diff
self.right += (1 - self.align) * diff
elif self.ratio < ratio:
diff = self.width / self.ratio - self.height
self.height += diff
self.bottom += self.align * diff
self.top += (1 - self.align) * diff
# determine tick positions:
ticks = {}
for x in extent.keys():
# use ticks or choose ticks with a spacing close to the given one:
xformat = getattr(self, x + 'format')
if getattr(self, x + 'ticks') is not None:
ticks[x] = [(scale[x] * (n - lower[x]), label) for n, label in
[tick if hasattr(tick, '__len__') else (tick, xformat(tick))
for tick in getattr(self, x + 'ticks')]]
ticks[x] = [(position, label) for position, label in ticks[x]
if -self.eps <= position <= extent[x] + self.eps]
else:
ticks[x] = [(scale[x] * (n - lower[x]), xformat(n))
for n in multiples(lower[x], upper[x],
getattr(self, x + 'step') or xround_mantissa(
getattr(self, x + 'spacing') / scale[x]))]
if not getattr(self, x + 'labels'):
ticks[x] = [(position, False) for position, label in ticks[x]]
# determine minor-tick positions:
minorticks = {}
for x in 'xy':
if getattr(self, x + 'minormarks') or self.minorgrid:
positions = getattr(self, x + 'minorticks')
if positions is None:
if (getattr(self, x + 'minorstep') is None and
getattr(self, x + 'minorspacing') is None):
positions = []
else:
positions = multiples(lower[x], upper[x],
getattr(self, x + 'minorstep') or xround_mantissa(
getattr(self, x + 'minorspacing') / scale[x]))
minorticks[x] = [scale[x] * (n - lower[x]) for n in positions]
minorticks[x] = [minor for minor in minorticks[x]
if not any(abs(major - minor) < self.resolution
for major, label in ticks[x])]
minorticks[x] = [position for position in minorticks[x]
if -self.eps <= position <= extent[x] + self.eps]
# handle horizontal and vertical lines:
for x, y in 'xy', 'yx':
for line in self.lines:
if not len(line[x]) and len(line[y]) == 1:
line[x] = [lower[x], upper[x]]
line[y] = [line[y][0]] * 2
line['options'].setdefault('line_cap', 'butt')
# create simple colormap from special lower and upper colors:
if self.cmap is None:
if isinstance(self.upper, Color) and isinstance(self.lower, Color):
self.cmap = colormap((0, self.lower), (1, self.upper))
# build LaTeX file:
labels = []
stem, typ, home = goto(filename)
png = png or typ == 'png'
pdf = pdf or typ == 'pdf' or png
if pdf:
standalone = True
with open('%s.tex' % stem, 'w') as file:
# print premable and open document:
if standalone:
file.write('\\documentclass[class=%s, %dpt]{standalone}\n'
% ('article' if 10 <= self.fontsize <= 12 else 'scrartcl',
self.fontsize))
file.write('\\usepackage{tikz}\n')
if self.inputenc and 'inputenc' not in self.preamble:
file.write('\\usepackage[%s]{inputenc}\n' % self.inputenc)
if self.font is not None:
texfonts = {
'Gill Sans':
'\\usepackage[math]{iwona}\n'
'\\usepackage[sfdefault]{cabin}\n'
'\\usepackage[italic, noplusnominus]{mathastext}\n',
'Helvetica':
'\\usepackage{sansmathfonts}\n'
'\\usepackage[scaled]{helvet}\n'
'\\let\\familydefault\\sfdefault\n'
'\\usepackage[italic]{mathastext}\n',
'Iwona':
'\\usepackage[math]{iwona}\n',
'Latin Modern':
'\\usepackage{lmodern}\n',
'Times':
'\\usepackage{newtxtext, newtxmath}\n',
'Utopia':
'\\usepackage{fourier}',
}
if self.font in texfonts:
file.write(texfonts[self.font])
if self.fontenc is None:
self.fontenc = 'T1'
else:
file.write('\\usepackage{mathspec}\n')
file.write('\\setallmainfonts{%s}\n' % self.font)
engine = 'xelatex'
if self.fontenc and 'fontenc' not in self.preamble:
file.write('\\usepackage[%s]{fontenc}\n' % self.fontenc)
if self.preamble:
file.write('%s\n' % self.preamble.strip())
for line in self.lines:
if ('mark' in line['options'] and line['options']['mark']
not in ['*', '+', 'x', 'ball']):
file.write('\\usetikzlibrary{plotmarks}\n')
break
file.write('\\begin{document}\n\\noindent\n')
# set filename for externalization:
elif external:
file.write('\\tikzsetnextfilename{%s}\n%%\n' % stem)
# open TikZ environment:
file.write('\\begin{tikzpicture}%s' % csv(self.options, '[%s]'))
# set bounding box:
bbox = ('\n (%.3f, %.3f) rectangle +(%.3f, %.3f);'
% (-self.left, -self.bottom, self.width, self.height))
file.write('\n\\useasboundingbox')
file.write(bbox)
if self.canvas is not None:
file.write('\n\\draw%s'
% csv(dict(color=self.canvas, line_width='1mm', fill=True)))
file.write(bbox)
if self.outline:
file.write('\n\\draw%s'
% csv(dict(color='gray', very_thin=True, dashed=True)))
file.write(bbox)
# add background image:
if self.background is not None:
file.write('\n\\node '
'[anchor=south west, inner sep=0, outer sep=0] '
'{\\includegraphics[width=%.3fcm, height=%.3fcm]{%s}};'
% (extent['x'], extent['y'], self.background))
# draw coordinate system:
origin = {}
for x in 'xy':
xorigin = getattr(self, x + 'origin')
if xorigin is None:
origin[x] = 0
else:
origin[x] = scale[x] * (xorigin - lower[x])
for x, y in 'xy', 'yx':
if not origin[y]:
continue
ticks[x] = [(position, label) for position, label in ticks[x]
if not abs(position - origin[x]) < self.resolution]
if getattr(self, x + 'minorticks'):
minorticks[x] = [position for position in minorticks[x]
if not abs(position - origin[x]) < self.resolution]
def draw_grid():
if draw_grid.done:
return
lines = {}
for x in 'xy':
lines[x] = set()
if getattr(self, x + 'axis'):
lines[x].add(origin[x])
if self.frame:
lines[x].add(0.0)
lines[x].add(extent[x])
if self.minorgrid:
file.write('\n\\draw [lightgray!50, line cap=rect]')
for x in minorticks['x']:
if not any(abs(x - line) < self.resolution
for line in lines['x']):
file.write('\n (%.3f, 0) -- +(0, %.3f)'
% (x, extent['y']))
for y in minorticks['y']:
if not any(abs(y - line) < self.resolution
for line in lines['y']):
file.write('\n (0, %.3f) -- +(%.3f, 0)'
% (y, extent['x']))
file.write(';')
file.write('\n\\draw [lightgray, line cap=rect]')
for x, label in ticks['x']:
if not any(abs(x - line) < self.resolution
for line in lines['x']):
file.write('\n (%.3f, 0) -- +(0, %.3f)'
% (x, extent['y']))
for y, label in ticks['y']:
if not any(abs(y - line) < self.resolution
for line in lines['y']):
file.write('\n (0, %.3f) -- +(%.3f, 0)'
% (y, extent['x']))
file.write(';')
draw_grid.done = True
draw_grid.done = False
def draw_frame():
if draw_frame.done:
return
file.write('\n\\draw [gray, line cap=rect]\n ')
if not self.xaxis or origin['y']:
file.write('(0, 0) -- ')
file.write('(%.3f, 0) -- (%.3f, %.3f) -- (0, %.3f)'
% tuple(extent[x] for x in 'xxyy'))
if not self.yaxis or origin['x']:
file.write(' -- (0, 0)')
file.write(';')
draw_frame.done = True
draw_frame.done = False
def draw_axes():
if draw_axes.done:
return
# paint colorbar:
if self.colorbar:
if self.cmap is not None:
dots = max(2, int(round(extent['z'] / inch * dpi)))
colorbar = colorize([[n / (dots - 1.0)]
for n in reversed(range(dots))], self.cmap)
self.colorbar = '%s.bar.png' % stem
save(self.colorbar, colorbar)
if isinstance(self.colorbar, str):
file.write('\n\\node at (%.3f, 0) '
'[anchor=south west, inner sep=0, outer sep=0] '
'{\\includegraphics[width=%.3fcm, height=%.3fcm]'
'{%s}};' % (extent['x'] + self.gap,
self.bar, extent['y'], self.colorbar))
else:
file.write('\n\\shade [bottom color=%s, top color=%s]'
% (self.lower, self.upper))
file.write('\n (%.3f, 0) rectangle (%.3f, %.3f);'
% (extent['x'] + self.gap,
extent['x'] + self.gap + self.bar,
extent['z']))
if self.zmarks:
for z, label in ticks['z']:
if label is None:
continue
file.write('\n\\node '
'[rotate=90, below] at (%.3f, %.3f) {%s};'
% (extent['x'] + self.gap + self.bar, z, label))
if self.grid:
draw_grid()
if self.frame:
draw_frame()
if self.xaxis or self.yaxis:
# draw tick marks and labels:
if (self.xaxis and (self.xmarks and ticks['x'] or
self.xminormarks and minorticks['x']) or
self.yaxis and (self.ymarks and ticks['y'] or
self.yminormarks and minorticks['y'])):
file.write('\n\\draw [line cap=butt]')
if self.xaxis and self.xmarks:
for x, label in ticks['x']:
if label is None:
continue
file.write('\n (%.3f, %.3f) -- +(0, %.3f)'
% (x, origin['y'], -self.tick))
if label:
file.write(' node [below] {%s}' % label)
if self.xaxis and self.xminormarks:
for x in minorticks['x']:
file.write('\n (%.3f, %.3f) -- +(0, %.3f)'
% (x, origin['y'], -self.minortick))
if self.yaxis and self.ymarks:
for y, label in ticks['y']:
if label is None:
continue
file.write('\n (%.3f, %.3f) -- +(%.3f, 0)'
% (origin['x'], y, -self.tick))
if label:
file.write(' node [%s] {%s}'
% ('left' if origin['x'] else
'rotate=90, above', label))
if self.yaxis and self.yminormarks:
for y in minorticks['y']:
file.write('\n (%.3f, %.3f) -- +(%.3f, 0)'
% (origin['x'], y, -self.minortick))
file.write(';')
# draw coordinate axes:
if origin['x'] or origin['y']:
file.write('\n\\draw [->, line cap=rect]\n '
'(0, %.3f) -- +(%.3f, 0);'
% (origin['y'], extent['x'] + self.tip))
file.write('\n\\draw [->, line cap=rect]\n '
'(%.3f, 0) -- +(0, %.3f);'
% (origin['x'], extent['y'] + self.tip))
else:
file.write('\n\\draw [%s-%s, line cap=butt]\n '
% ('<' * self.xaxis, '>' * self.yaxis))
if self.xaxis:
file.write('(%.3f, 0) -- '
% (extent['x'] + self.tip))
file.write('(0, 0)')
if self.yaxis:
file.write(' -- (0, %.3f)'
% (extent['y'] + self.tip))
file.write(';')
# label coordinate axes:
if self.xaxis and self.xlabel:
if origin['y']:
file.write('\n\\node [right] at (%.3f, %.3f)'
% (extent['x'] + self.tip, origin['y']))
else:
file.write('\n\\node [below')
if ticks['x'] and not self.xclose:
file.write('=\\baselineskip')
file.write('] at (%.3f, %.3f)'
% (extent['x'] / 2, -self.tick))
file.write('\n {%s};' % self.xlabel)
if self.yaxis and self.ylabel:
if origin['x']:
file.write('\n\\node [above] at (%.3f, %.3f)'
% (origin['x'], extent['y'] + self.tip))
else:
file.write('\n\\node [rotate=90, above')
if ticks['y'] and not self.yclose:
file.write('=\\baselineskip')
file.write('] at (%.3f, %.3f)'
% (-self.tick, extent['y'] / 2))
file.write('\n {%s};' % self.ylabel)
if self.colorbar and self.zlabel:
file.write('\n\\node [rotate=90, below')
if ticks['z'] and not self.zclose:
file.write('=\\baselineskip')
file.write('] at (%.3f, %.3f)'
% (extent['x'] + self.gap + self.bar, extent['y'] / 2))
file.write('\n {%s};' % self.zlabel)
draw_axes.done = True
draw_axes.done = False
if not any(line['axes'] for line in self.lines):
draw_axes()
# plot lines:
form = '(%%%d.3f, %%%d.3f)' % (
5 if extent['x'] < 10 else 6,
5 if extent['y'] < 10 else 6)
for line in self.lines:
if line['z'] is not None:
ratio = (line['z'] - lower['z']) / (upper['z'] - lower['z'])
line['options'].setdefault('color',
'%s!%.1f!%s' % (self.upper, 100 * ratio, self.lower)
if self.cmap is None else self.cmap(ratio))
if line['options'].get('mark') == 'ball':
line['options'].setdefault('ball_color',
line['options']['color'])
for option in 'line_width', 'mark_size':
if isinstance(line['options'].get(option), (float, int)):
line['options'][option] = ('%.3fcm'
% (line['options'][option] * scale['y']))
for option in line['options']:
if isinstance(line['options'][option], str):
line['options'][option] = re.sub('<([\\d.]+)>',
lambda match: '%.3f' % (float(match.group(1))
* scale['y']), line['options'][option])
if line['label'] is not None:
label = [line['options'], line['label']]
for previous in labels:
if label[1] and previous == label:
break
else:
labels.append(label)
if len(line['x']) and len(line['y']):
for x, y in 'xy', 'yx':
xref = line[x + 'ref']
if xref is not None:
line[x] = list(line[x])
line[y] = list(line[y])
line[x] = [xref] + line[x] + [xref]
line[y] = line[y][:1] + line[y] + line[y][-1:]
points = list(zip(*[[scale[x] * (n - lower[x])
for n in line[x]] for x in 'xy']))
if line['protrusion']:
for i, j in (1, 0), (-2, -1):
if line['weights'] is not None:
if not line['weights'][j]:
continue
dx = points[j][0] - points[i][0]
dy = points[j][1] - points[i][1]
dr = math.sqrt(dx * dx + dy * dy)
rescale = 1 + line['protrusion'] / dr
points[j] = (
points[i][0] + dx * rescale,
points[i][1] + dy * rescale,
)
if line['jump']:
segments = jump(points, distance=line['jump'])
else:
segments = [points]
if line['weights'] is not None:
segments = [(miter_butt if line['miter'] else fatband)(
segment, line['thickness'], line['weights'],
line['shifts'], line['nib'])
for segment in segments]
if line['cut']:
try:
xmin, xmax, ymin, ymax = line['cut']
except (TypeError, ValueError):
xmin = xmax = ymin = ymax = None
eps = self.eps if line['options'].get('mark') else 0
xmin = (scale['x'] * (xmin - lower['x'])
if xmin is not None else 0) - eps
xmax = (scale['x'] * (xmax - lower['x'])
if xmax is not None else extent['x']) + eps
ymin = (scale['y'] * (ymin - lower['y'])
if ymin is not None else 0) - eps
ymax = (scale['y'] * (ymax - lower['y'])
if ymax is not None else extent['y']) + eps
if line['join'] is None:
line['join'] = line['options'].get('fill',
'none') != 'none'
if line['options'].get('only_marks'):
segments = [[(x, y)
for segment in segments
for x, y in segment
if xmin <= x <= xmax and ymin <= y <= ymax]]
else:
segments = [segment
for segment in segments
for segment in cut2d(segment,
xmin, xmax, ymin, ymax, line['join'])]
if line['omit'] is None:
line['omit'] = 'mark' not in line['options']
for segment in segments:
options = line['options'].copy()
if line['omit']:
segment = relevant(segment[::line['sgn']],
self.resolution)
elif line['cut'] and line['options'].get('mark') \
and not line['options'].get('only_marks'):
options['mark_indices'] = '{%s}' % ','.join(str(n)
for n, point in enumerate(segment, 1)
if point in points)
if line['shortcut']:
segment = shortcut(segment, line['shortcut'],
line['shortcut_rel'])
file.write('\n\\draw%s plot coordinates {'
% csv(options))
for group in groups(segment):
file.write('\n ')
file.write(' '.join(form % point
for point in group))
file.write(' };')
# insert TikZ code with special coordinates:
if line['code']:
code = line['code'].strip()
for x in 'xy':
code = re.sub('<%s=(.*?)>' % x, lambda match: '%.3f'
% (scale[x] * (float(match.group(1)) - lower[x])),
code)
code = re.sub('<d%s=(.*?)>' % x, lambda match: '%.3f'
% (scale[x] * float(match.group(1))),
code)
file.write('\n%s' % code)
if line['grid']:
draw_grid()
if line['frame']:
draw_frame()
if line['axes']:
draw_axes()
draw_axes()
def position(pos):
if isinstance(pos, str):
x = []
y = []
positions = dict(
L=(x, -self.left),
B=(y, -self.bottom),
l=(x, 0.0),
b=(y, 0.0),
r=(x, extent['x']),
t=(y, extent['y']),
R=(x, extent['x'] + self.right),
T=(y, extent['y'] + self.top),
)
abbreviations = dict(c='lr', C='LR', m='bt', M='BT')
for abbreviation in abbreviations.items():
pos = pos.replace(*abbreviation)
for char in pos:
positions[char][0].append(positions[char][1])
x = sum(x) / len(x)
y = sum(y) / len(y)
else:
x, y = pos
x = scale['x'] * (x - lower['x'])
y = scale['y'] * (y - lower['y'])
return x, y
# add label:
if self.label is not None:
if self.labelformat is not None:
self.label = self.labelformat(self.label)
if self.labelsize is not None:
self.label = ('\\fontsize{%d}{%d}\\selectfont %s'
% (self.labelsize, self.labelsize, self.label))
file.write('\n\\node at (%.3f, %.3f)' % position(self.labelpos))
file.write(' [%s] {%s};' % (self.labelopt, self.label))
# add legend:
if self.lput and (self.ltop is not None or labels):
file.write('\n\\node [align=%s' % self.lali)
if self.lopt is not None:
file.write(', %s' % self.lopt)
if self.lbox:
file.write(', draw=gray, fill=white, rounded corners=1pt')
file.write('] at (%.3f, %.3f) {' % position(self.lpos))
if self.ltop:
file.write('\n %s' % self.ltop)
if labels:
file.write(' \\\\')
if self.lsep is not None:
file.write('[%s]' % self.lsep)
if labels:
file.write('\n \\begin{tikzpicture}[x=%s, y=-%s]'
% (self.llen, self.lbls))
lrow = self.lrow or 1 + (len(labels) - 1) // self.lcol
spacer = True
n = 0
for options, label in labels:
col = n // lrow
row = n % lrow
if label == '*next*':
label = None
row -= 0.2
else:
n += 1
if label:
file.write('\n \\node '
'[right] at (%.3f, %d) {%s};'
% (col * self.lwid + 1, row, label))
fill = options.get('fill', 'none') != 'none'
draw = not options.get('only_marks')
draw &= not options.get('draw') == 'none'
mark = 'mark' in options
if fill or draw or mark:
if (fill or draw) and mark:
options['mark_indices'] = '{2}'
file.write('\n \\draw%s' % csv(options))
file.write('\n plot coordinates ')
if fill:
top = row - 0.1
bot = row + 0.1
x = [0.0] + [0.5] * mark + [1.0, 1.0, 0.0, 0.0]
y = [bot] + [bot] * mark + [bot, top, top, bot]
elif draw:
x = [0.0] + [0.5] * mark + [1.0]
y = [row] + [row] * mark + [row]
elif mark:
x = [0.5]
y = [row]
file.write('{')
for m in range(len(x)):
file.write(' (%.3f, %g)'
% (col * self.lwid + x[m], y[m]))
file.write(' };')
if draw or fill and not col:
spacer = False
if spacer:
file.write('\n \\useasboundingbox (0, 0);')
file.write('\n \\end{tikzpicture}%')
file.write('\n };')
# add title:
if self.title is not None:
options = dict(above=True)
if '\\\\' in self.title:
options['align'] = 'center'
file.write('\n\\node%s at (%.3f, %.3f) {%s};'
% (csv(options), extent['x'] / 2, extent['y'], self.title))
# close TikZ environment:
file.write('\n\\end{tikzpicture}%')
# close document:
if standalone:
file.write('\n\\end{document}')
file.write('\n')
# typeset document and clean up:
if pdf:
typeset(stem, engine)
if png:
rasterize(stem, dpi, width, height, rewrite)
home()