Created
September 30, 2015 22:05
-
-
Save seehuhn/39a13ef38813d8c10c2f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /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