import math
import re
import numpy as np
def svg(chunks, size=200, **kwargs):
"""Convert chunks from Dask Array into an SVG Image
Parameters
----------
chunks: tuple
size: int
Rough size of the image
Returns
-------
text: An svg string depicting the array as a grid of chunks
"""
shape = tuple(map(sum, chunks))
if np.isnan(shape).any(): # don't support unknown sizes
raise NotImplementedError(
"Can't generate SVG with unknown chunk sizes.\n\n"
" A possible solution is with x.compute_chunk_sizes()"
)
if not all(shape):
raise NotImplementedError("Can't generate SVG with 0-length dimensions")
if len(chunks) == 0:
raise NotImplementedError("Can't generate SVG with 0 dimensions")
if len(chunks) == 1:
return svg_1d(chunks, size=size, **kwargs)
elif len(chunks) == 2:
return svg_2d(chunks, size=size, **kwargs)
elif len(chunks) == 3:
return svg_3d(chunks, size=size, **kwargs)
else:
return svg_nd(chunks, size=size, **kwargs)
text_style = 'font-size="1.0rem" font-weight="100" text-anchor="middle"'
def svg_2d(chunks, offset=(0, 0), skew=(0, 0), size=200, sizes=None):
shape = tuple(map(sum, chunks))
sizes = sizes or draw_sizes(shape, size=size)
y, x = grid_points(chunks, sizes)
lines, (min_x, max_x, min_y, max_y) = svg_grid(
x, y, offset=offset, skew=skew, size=size
)
header = (
'"
if shape[0] >= 100:
rotate = -90
else:
rotate = 0
text = [
"",
" ",
' %d'
% (max_x / 2, max_y + 20, text_style, shape[1]),
' %d'
% (max_x + 20, max_y / 2, text_style, rotate, max_x + 20, max_y / 2, shape[0]),
]
return header + "\n".join(lines + text) + footer
def svg_3d(chunks, size=200, sizes=None, offset=(0, 0)):
shape = tuple(map(sum, chunks))
sizes = sizes or draw_sizes(shape, size=size)
x, y, z = grid_points(chunks, sizes)
ox, oy = offset
xy, (mnx, mxx, mny, mxy) = svg_grid(
x / 1.7, y, offset=(ox + 10, oy + 0), skew=(1, 0), size=size
)
zx, (_, _, _, max_x) = svg_grid(
z, x / 1.7, offset=(ox + 10, oy + 0), skew=(0, 1), size=size
)
zy, (min_z, max_z, min_y, max_y) = svg_grid(
z, y, offset=(ox + max_x + 10, oy + max_x), skew=(0, 0), size=size
)
header = (
'"
if shape[1] >= 100:
rotate = -90
else:
rotate = 0
text = [
"",
" ",
' %d'
% ((min_z + max_z) / 2, max_y + 20, text_style, shape[2]),
' %d'
% (
max_z + 20,
(min_y + max_y) / 2,
text_style,
rotate,
max_z + 20,
(min_y + max_y) / 2,
shape[1],
),
' %d'
% (
(mnx + mxx) / 2 - 10,
mxy - (mxx - mnx) / 2 + 20,
text_style,
(mnx + mxx) / 2 - 10,
mxy - (mxx - mnx) / 2 + 20,
shape[0],
),
]
return header + "\n".join(xy + zx + zy + text) + footer
def svg_nd(chunks, size=200):
if len(chunks) % 3 == 1:
chunks = ((1,),) + chunks
shape = tuple(map(sum, chunks))
sizes = draw_sizes(shape, size=size)
chunks2 = chunks
sizes2 = sizes
out = []
left = 0
total_height = 0
while chunks2:
n = len(chunks2) % 3 or 3
o = svg(chunks2[:n], sizes=sizes2[:n], offset=(left, 0))
chunks2 = chunks2[n:]
sizes2 = sizes2[n:]
lines = o.split("\n")
header = lines[0]
height = float(re.search(r'height="(\d*\.?\d*)"', header).groups()[0])
total_height = max(total_height, height)
width = float(re.search(r'width="(\d*\.?\d*)"', header).groups()[0])
left += width + 10
o = "\n".join(lines[1:-1]) # remove header and footer
out.append(o)
header = (
'"
return header + "\n\n".join(out) + footer
def svg_lines(x1, y1, x2, y2, max_n=20):
"""Convert points into lines of text for an SVG plot
Examples
--------
>>> svg_lines([0, 1], [0, 0], [10, 11], [1, 1]) # doctest: +NORMALIZE_WHITESPACE
[' ',
' ']
"""
n = len(x1)
if n > max_n:
indices = np.linspace(0, n - 1, max_n, dtype="int")
else:
indices = range(n)
lines = [
' ' % (x1[i], y1[i], x2[i], y2[i])
for i in indices
]
lines[0] = lines[0].replace(" /", ' style="stroke-width:2" /')
lines[-1] = lines[-1].replace(" /", ' style="stroke-width:2" /')
return lines
def svg_grid(x, y, offset=(0, 0), skew=(0, 0), size=200):
"""Create lines of SVG text that show a grid
Parameters
----------
x: numpy.ndarray
y: numpy.ndarray
offset: tuple
translational displacement of the grid in SVG coordinates
skew: tuple
"""
# Horizontal lines
x1 = np.zeros_like(y) + offset[0]
y1 = y + offset[1]
x2 = np.full_like(y, x[-1]) + offset[0]
y2 = y + offset[1]
if skew[0]:
y2 += x.max() * skew[0]
if skew[1]:
x1 += skew[1] * y
x2 += skew[1] * y
min_x = min(x1.min(), x2.min())
min_y = min(y1.min(), y2.min())
max_x = max(x1.max(), x2.max())
max_y = max(y1.max(), y2.max())
max_n = size // 6
h_lines = ["", " "] + svg_lines(x1, y1, x2, y2, max_n)
# Vertical lines
x1 = x + offset[0]
y1 = np.zeros_like(x) + offset[1]
x2 = x + offset[0]
y2 = np.full_like(x, y[-1]) + offset[1]
if skew[0]:
y1 += skew[0] * x
y2 += skew[0] * x
if skew[1]:
x2 += skew[1] * y.max()
v_lines = ["", " "] + svg_lines(x1, y1, x2, y2, max_n)
color = "ECB172" if len(x) < max_n and len(y) < max_n else "8B4903"
corners = f"{x1[0]},{y1[0]} {x1[-1]},{y1[-1]} {x2[-1]},{y2[-1]} {x2[0]},{y2[0]}"
rect = [
"",
" ",
f' ',
]
return h_lines + v_lines + rect, (min_x, max_x, min_y, max_y)
def svg_1d(chunks, sizes=None, **kwargs):
return svg_2d(((1,),) + chunks, **kwargs)
def grid_points(chunks, sizes):
cumchunks = [np.cumsum((0,) + c) for c in chunks]
points = [x * size / x[-1] for x, size in zip(cumchunks, sizes)]
return points
def draw_sizes(shape, size=200):
"""Get size in pixels for all dimensions"""
mx = max(shape)
ratios = [mx / max(0.1, d) for d in shape]
ratios = [ratio_response(r) for r in ratios]
return tuple(size / r for r in ratios)
def ratio_response(x):
"""How we display actual size ratios
Common ratios in sizes span several orders of magnitude,
which is hard for us to perceive.
We keep ratios in the 1-3 range accurate, and then apply a logarithm to
values up until about 100 or so, at which point we stop scaling.
"""
if x < math.e:
return x
elif x <= 100:
return math.log(x + 12.4) # f(e) == e
else:
return math.log(100 + 12.4)