Skip to content

Instantly share code, notes, and snippets.

@seehuhn
Created September 30, 2015 22:05
Show Gist options
  • Save seehuhn/39a13ef38813d8c10c2f to your computer and use it in GitHub Desktop.
Save seehuhn/39a13ef38813d8c10c2f to your computer and use it in GitHub Desktop.
#! /usr/bin/env python3
import faulthandler; faulthandler.enable()
import numpy as np
import cairocffi as cairo
UNITS = {
'in': 1,
'cm': 1 / 2.54,
'mm': 1 / 25.4,
'bp': 1 / 72,
'pt': 1 / 72.27,
}
default = {
'affine_lw': ('dim', '$lw', 'default line width for straight lines'),
'axis_font_size': ('dim', '$font_size', 'font size for axis labels'),
'axis_label_dist': ('dim', '3pt', 'default distance between tick labels and tick marks'),
'axis_label_font_size': ('dim', '$font_size', 'font size for axis labels'),
'axis_lw': ('dim', '$lw_thick', 'line width of the axis boxes'),
'axis_margin_bottom': ('dim', '7mm', 'default axis bottom margin'),
'axis_margin_left': ('dim', '14mm', 'default axis left margin'),
'axis_margin_right': ('dim', '2mm', 'default axis right margin'),
'axis_margin_top': ('dim', '2mm', 'default axis top margin'),
'axis_tick_length': ('dim', '3pt', 'length of axis tick marks'),
'axis_tick_opt_spacing': ('dim', '2cm', 'optimal spacing for axis tick marks'),
'axis_tick_width': ('dim', '$lw_medium', 'line width for axis tick marks'),
'axis_x_label_dist': ('dim', '$axis_label_dist', 'vertical distance between labels and tick marks on the x-axis'),
'axis_x_label_sep': ('dim', '8pt', 'minimum horizonal separation of x-axis labels'),
'axis_y_label_dist': ('dim', '$axis_label_dist', 'horizontal distance between labels and tick marks on the y-axis'),
'font_size': ('dim', '10pt', 'default font size'),
'hist_col': ('col', '$line_col', 'line color for histogram boxes'),
'hist_fill_col': ('col', '#CCC', 'default fill color for histogram bars'),
'hist_lw': ('dim', '$lw_thin', 'default line width for histogram bars'),
'line_col': ('col', 'black', 'default line color'),
'lw': ('dim', '$lw_medium', 'line width'),
'lw_medium': ('dim', '.8pt', 'default width for medium thick lines'),
'lw_thick': ('dim', '1pt', 'default width for thick lines'),
'lw_thin': ('dim', '.6pt', 'default width for thin lines'),
'margin': ('dim', '2mm', 'default margin around the plotting area'),
'margin.bottom': ('height', '$margin', 'margin below the plotting area'),
'margin.left': ('width', '$margin', 'margin to the left of the plotting area'),
'margin.right': ('width', '$margin', 'margin to the right of the plotting area'),
'margin.top': ('height', '$margin', 'margin above the plotting area'),
'plot_col': ('col', '$line_col', 'default plot line color'),
'plot_lw': ('dim', '$lw', 'default line width for plots'),
'plot_point_col': ('col', '$line_col', 'default point color for scatter plots'),
'plot_point_separate': ('bool', False, 'whether to draw points in a scatter plot individually'),
'plot_point_size': ('dim', '2pt', 'point size for scatter plots'),
'text_bg': ('col', 'rgba(255,255,255,.8)', 'default text background color'),
'text_col': ('col', 'black', 'default text color'),
'text_font_size': ('dim', '$font_size', 'default text font size'),
'title_font_size': ('dim', '$font_size', 'font size for titles'),
'title_top_margin': ('dim', '2mm', 'distance of title to top edge of canvas'),
}
def _check_vec(v, n, broadcast=False):
if isinstance(v, str):
if not broadcast:
raise TypeError('string "%s" used as vec%d' % (v, n))
return [v] * n
try:
k = len(v)
except TypeError:
if not broadcast:
tmpl = "%s used as vec%d, but does not have a length"
raise TypeError(tmpl % (repr(v), n))
k = 1
v = [v]
if broadcast and 1 <= k < n and n % k == 0:
return list(v) * (n // k)
elif k != n:
tmpl = "%s used as vec%d, but has length %s != %d"
raise ValueError(tmpl % (repr(v), n, k, n))
return v
def _convert_dim(dim, res, parent_length=None, allow_none=False):
if allow_none and dim is None:
return None
unit = 1
try:
for pfx, scale in UNITS.items():
if not dim.endswith(pfx):
continue
dim = dim[:-len(pfx)]
unit = scale
else:
if dim.endswith('px'):
if res is None:
raise ValueError(
'pixel length %s in invalid context' % dim)
dim = dim[:-2]
unit = 1 / res
elif dim.endswith('%'):
if parent_length is None:
raise ValueError(
'relative length %s in invalid context' % dim)
dim = dim[:-1]
unit = parent_length / 100 / res
except AttributeError:
pass
return float(dim) * unit * res
class Canvas:
def __init__(self, x, y, w, h, *, res, parent, style={}):
self.x = x
self.y = y
self.width = w
self.height = h
self._orig_x = self.x
self._orig_y = self.y
self._orig_width = self.width
self._orig_height = self.height
self.res = res
self.parent = parent
self.style = style
self.offset = None
self.scale = None
self.axes = None
def get_param(self, name, style={}):
res = self._get_param(name, style)
print(name, "=", res)
return res
def _get_param(self, name, style={}):
info = default.get(name)
if info is None:
msg = "unknown parameter '%s'" % name
raise errors.InvalidParameterName(msg)
def styles():
if style:
yield style
elem = self
while elem:
yield elem.style
elem = elem.parent
def get(x):
for s in styles():
if x in s:
return s[x]
else:
return info[1]
names = [name]
while True:
value = get(name)
if isinstance(value, str) and value.startswith('$'):
name = value[1:]
if name in names:
msg = ' -> '.join(names + [name])
raise errors.WrongUsage("infinite parameter loop: " + msg)
names.append(name)
info = default.get(name)
else:
break
if info[0] == 'width':
return _convert_dim(value, self.res, self.width)
elif info[0] == 'height':
return _convert_dim(value, self.res, self.height)
elif info[0] == 'dim':
return _convert_dim(value, self.res)
elif info[0] == 'col':
return (0.0, 0.0, 0.0, 1.0)
elif info[0] == 'bool':
return bool(value)
else:
raise NotImplementedError("parameter type '%s'" % info[0])
def add_padding(self, padding):
if self.axes is not None:
msg = "cannot add padding once axes are present"
raise errors.WrongUsage(msg)
padding = _check_vec(padding, 4, True)
p_top = _convert_dim(padding[0], self.res, self.height)
p_right = _convert_dim(padding[1], self.res, self.width)
p_bottom = _convert_dim(padding[2], self.res, self.height)
p_left = _convert_dim(padding[3], self.res, self.width)
self.x += p_left
self.width -= p_left + p_right
self.y += p_bottom
self.height -= p_bottom + p_top
def set_limits(self, x_lim, y_lim):
x_scale = self.width / (x_lim[1] - x_lim[0])
x_offset = self.x - x_lim[0] * x_scale
# The vertical coordinates are similar:
y_scale = self.height / (y_lim[1] - y_lim[0])
y_offset = self.y - y_lim[0] * y_scale
self.offset = (x_offset, y_offset)
self.scale = (x_scale, y_scale)
return x_lim, y_lim
def _viewport(self, width, height, margin, border, padding, style):
width = _convert_dim(width, self.res, self.width,
allow_none=True)
height = _convert_dim(height, self.res, self.height,
allow_none=True)
margin = _check_vec(margin, 4, True)
m_top = _convert_dim(margin[0], self.res, self.height,
allow_none=True)
m_right = _convert_dim(margin[1], self.res, self.width,
allow_none=True)
m_bottom = _convert_dim(margin[2], self.res, self.height,
allow_none=True)
m_left = _convert_dim(margin[3], self.res, self.width,
allow_none=True)
if isinstance(border, str):
border = border.split()
border_width = border[0]
else:
border_width = border
border_width = _convert_dim(border_width, self.res)
if border_width < 0:
raise ValueError('negative border width in "%s"' % border)
padding = _check_vec(padding, 4, True)
p_top = _convert_dim(padding[0], self.res, self.height)
p_right = _convert_dim(padding[1], self.res, self.width)
p_bottom = _convert_dim(padding[2], self.res, self.height)
p_left = _convert_dim(padding[3], self.res, self.width)
total_w = sum(x for x in [m_left, border_width, p_left, width,
p_right, border_width, m_right]
if x is not None)
if total_w > 1.001 * self.width:
raise ValueError("total width %f > %f" % (total_w, self.width))
if width is None:
width = self.width - total_w
spare_w = self.width - 2*border_width - p_left - width - p_right
if m_left is None and m_right is None:
m_left = max(spare_w / 2, 0)
elif m_left is None:
m_left = spare_w - m_right
total_h = sum(x for x in [m_bottom, border_width, p_bottom, height,
p_top, border_width, m_top]
if x is not None)
if total_h > 1.001 * self.height:
raise ValueError("total height %f > %f" % (total_h, self.height))
if height is None:
height = self.height - total_h
spare_h = self.height - 2*border_width - p_bottom - height - p_top
if m_bottom is None and m_top is None:
m_bottom = max(spare_h / 2, 0)
elif m_bottom is None:
m_bottom = spare_h - m_top
border_rect = [
self.x + m_left + 0.5*border_width,
self.y + m_bottom + 0.5*border_width,
border_width + p_left + width + p_right,
border_width + p_bottom + height + p_top
]
x = self.x + m_left + border_width
y = self.y + m_bottom + border_width
w = p_left + width + p_right
h = p_bottom + height + p_top
res = Canvas(x, y, w, h, res=self.res, parent=self, style=style)
res.add_padding([p_top / self.res, p_right / self.res,
p_bottom / self.res, p_left / self.res])
return res, border_rect
def _layout_labels(self, x_lim, y_lim, aspect):
opt_spacing = 55.0
x_label_sep = 8.0
best_xlim = (0, 1)
best_xsteps = (0, 0.5, 1)
best_xlabels = ("0.0", "0.5", "1.0")
best_ylim = (0, 1)
best_ysteps = (0, 0.5, 1)
best_ylabels = ("0.0", "0.5", "1.0")
return [
best_xlim, zip(best_xsteps, best_xlabels),
best_ylim, zip(best_ysteps, best_ylabels),
]
def draw_axes(self, x_lim, y_lim, *, x_label=None, y_label=None,
aspect=None, width=None, height=None, margin=None,
border=None, padding=None, style={}):
margin = ['1cm'] * 4
border = 1
if padding is None:
padding = "2mm"
axes, rect = self._viewport(width, height, margin, border, padding,
style)
tick_width = 0.8
tick_length = 3.0
x_label_dist = 3.0
y_label_dist = 3.0
tick_font_size = 10
cairo.Matrix(tick_font_size, 0, 0, -tick_font_size, 0, 0)
ascent = 7.7
descent = 2.3
x_tick_bottom = rect[1] - tick_length - x_label_dist - ascent + descent
x_lim, x_labels, y_lim, y_labels = axes._layout_labels(x_lim, y_lim, aspect)
axes.set_limits(x_lim, y_lim)
axes.x_lim = x_lim
axes.x_labels = x_labels
axes.y_lim = y_lim
axes.y_labels = y_labels
self.axes = axes
return axes
def draw_points(self, x, y=None, *, style={}):
lw = 2.0
col = (0.0, 0.0, 0.0, 1.0)
separate = self.get_param('plot_point_separate', style)
def scatter_plot(self, x, y=None, *, x_lim=None, y_lim=None,
x_label=None, y_label=None, aspect=None, width=None,
height=None, margin=None, border=None, padding=None,
style={}):
if self.axes is None:
# x, y = _check_coords(x, y)
x_lim = (0, 1)
y_lim = (0, 1)
self.draw_axes(x_lim, y_lim, x_label=x_label,
y_label=y_label, aspect=aspect, width=width,
height=height, margin=margin, border=border,
padding=padding, style=style)
self.axes.draw_points(x, y)
class Fake():
def __getattr__(self, name):
def fake_method(*args, **kwargs):
pass
return fake_method
class Plot(Canvas):
def __init__(self):
res = 72
w = 324.0
h = 324.0
surface = cairo.PDFSurface("crash.pdf", w, h)
super().__init__(0, 0, w, h, res=res, parent=None)
self.surface = surface
fig = Plot()
fig.scatter_plot([1,2,3], [1,2,3], aspect=1, margin="1cm")
print("done")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment