1import math
2import re
3
4import numpy as np
5
6
7def svg(chunks, size=200, **kwargs):
8    """Convert chunks from Dask Array into an SVG Image
9
10    Parameters
11    ----------
12    chunks: tuple
13    size: int
14        Rough size of the image
15
16    Returns
17    -------
18    text: An svg string depicting the array as a grid of chunks
19    """
20    shape = tuple(map(sum, chunks))
21    if np.isnan(shape).any():  # don't support unknown sizes
22        raise NotImplementedError(
23            "Can't generate SVG with unknown chunk sizes.\n\n"
24            " A possible solution is with x.compute_chunk_sizes()"
25        )
26    if not all(shape):
27        raise NotImplementedError("Can't generate SVG with 0-length dimensions")
28    if len(chunks) == 0:
29        raise NotImplementedError("Can't generate SVG with 0 dimensions")
30    if len(chunks) == 1:
31        return svg_1d(chunks, size=size, **kwargs)
32    elif len(chunks) == 2:
33        return svg_2d(chunks, size=size, **kwargs)
34    elif len(chunks) == 3:
35        return svg_3d(chunks, size=size, **kwargs)
36    else:
37        return svg_nd(chunks, size=size, **kwargs)
38
39
40text_style = 'font-size="1.0rem" font-weight="100" text-anchor="middle"'
41
42
43def svg_2d(chunks, offset=(0, 0), skew=(0, 0), size=200, sizes=None):
44    shape = tuple(map(sum, chunks))
45    sizes = sizes or draw_sizes(shape, size=size)
46    y, x = grid_points(chunks, sizes)
47
48    lines, (min_x, max_x, min_y, max_y) = svg_grid(
49        x, y, offset=offset, skew=skew, size=size
50    )
51
52    header = (
53        '<svg width="%d" height="%d" style="stroke:rgb(0,0,0);stroke-width:1" >\n'
54        % (max_x + 50, max_y + 50)
55    )
56    footer = "\n</svg>"
57
58    if shape[0] >= 100:
59        rotate = -90
60    else:
61        rotate = 0
62
63    text = [
64        "",
65        "  <!-- Text -->",
66        '  <text x="%f" y="%f" %s >%d</text>'
67        % (max_x / 2, max_y + 20, text_style, shape[1]),
68        '  <text x="%f" y="%f" %s transform="rotate(%d,%f,%f)">%d</text>'
69        % (max_x + 20, max_y / 2, text_style, rotate, max_x + 20, max_y / 2, shape[0]),
70    ]
71
72    return header + "\n".join(lines + text) + footer
73
74
75def svg_3d(chunks, size=200, sizes=None, offset=(0, 0)):
76    shape = tuple(map(sum, chunks))
77    sizes = sizes or draw_sizes(shape, size=size)
78    x, y, z = grid_points(chunks, sizes)
79    ox, oy = offset
80
81    xy, (mnx, mxx, mny, mxy) = svg_grid(
82        x / 1.7, y, offset=(ox + 10, oy + 0), skew=(1, 0), size=size
83    )
84
85    zx, (_, _, _, max_x) = svg_grid(
86        z, x / 1.7, offset=(ox + 10, oy + 0), skew=(0, 1), size=size
87    )
88    zy, (min_z, max_z, min_y, max_y) = svg_grid(
89        z, y, offset=(ox + max_x + 10, oy + max_x), skew=(0, 0), size=size
90    )
91
92    header = (
93        '<svg width="%d" height="%d" style="stroke:rgb(0,0,0);stroke-width:1" >\n'
94        % (max_z + 50, max_y + 50)
95    )
96    footer = "\n</svg>"
97
98    if shape[1] >= 100:
99        rotate = -90
100    else:
101        rotate = 0
102
103    text = [
104        "",
105        "  <!-- Text -->",
106        '  <text x="%f" y="%f" %s >%d</text>'
107        % ((min_z + max_z) / 2, max_y + 20, text_style, shape[2]),
108        '  <text x="%f" y="%f" %s transform="rotate(%d,%f,%f)">%d</text>'
109        % (
110            max_z + 20,
111            (min_y + max_y) / 2,
112            text_style,
113            rotate,
114            max_z + 20,
115            (min_y + max_y) / 2,
116            shape[1],
117        ),
118        '  <text x="%f" y="%f" %s transform="rotate(45,%f,%f)">%d</text>'
119        % (
120            (mnx + mxx) / 2 - 10,
121            mxy - (mxx - mnx) / 2 + 20,
122            text_style,
123            (mnx + mxx) / 2 - 10,
124            mxy - (mxx - mnx) / 2 + 20,
125            shape[0],
126        ),
127    ]
128
129    return header + "\n".join(xy + zx + zy + text) + footer
130
131
132def svg_nd(chunks, size=200):
133    if len(chunks) % 3 == 1:
134        chunks = ((1,),) + chunks
135    shape = tuple(map(sum, chunks))
136    sizes = draw_sizes(shape, size=size)
137
138    chunks2 = chunks
139    sizes2 = sizes
140    out = []
141    left = 0
142    total_height = 0
143    while chunks2:
144        n = len(chunks2) % 3 or 3
145        o = svg(chunks2[:n], sizes=sizes2[:n], offset=(left, 0))
146        chunks2 = chunks2[n:]
147        sizes2 = sizes2[n:]
148
149        lines = o.split("\n")
150        header = lines[0]
151        height = float(re.search(r'height="(\d*\.?\d*)"', header).groups()[0])
152        total_height = max(total_height, height)
153        width = float(re.search(r'width="(\d*\.?\d*)"', header).groups()[0])
154        left += width + 10
155        o = "\n".join(lines[1:-1])  # remove header and footer
156
157        out.append(o)
158
159    header = (
160        '<svg width="%d" height="%d" style="stroke:rgb(0,0,0);stroke-width:1" >\n'
161        % (left, total_height)
162    )
163    footer = "\n</svg>"
164    return header + "\n\n".join(out) + footer
165
166
167def svg_lines(x1, y1, x2, y2, max_n=20):
168    """Convert points into lines of text for an SVG plot
169
170    Examples
171    --------
172    >>> svg_lines([0, 1], [0, 0], [10, 11], [1, 1])  # doctest: +NORMALIZE_WHITESPACE
173    ['  <line x1="0" y1="0" x2="10" y2="1" style="stroke-width:2" />',
174     '  <line x1="1" y1="0" x2="11" y2="1" style="stroke-width:2" />']
175    """
176    n = len(x1)
177
178    if n > max_n:
179        indices = np.linspace(0, n - 1, max_n, dtype="int")
180    else:
181        indices = range(n)
182
183    lines = [
184        '  <line x1="%d" y1="%d" x2="%d" y2="%d" />' % (x1[i], y1[i], x2[i], y2[i])
185        for i in indices
186    ]
187
188    lines[0] = lines[0].replace(" /", ' style="stroke-width:2" /')
189    lines[-1] = lines[-1].replace(" /", ' style="stroke-width:2" /')
190    return lines
191
192
193def svg_grid(x, y, offset=(0, 0), skew=(0, 0), size=200):
194    """Create lines of SVG text that show a grid
195
196    Parameters
197    ----------
198    x: numpy.ndarray
199    y: numpy.ndarray
200    offset: tuple
201        translational displacement of the grid in SVG coordinates
202    skew: tuple
203    """
204    # Horizontal lines
205    x1 = np.zeros_like(y) + offset[0]
206    y1 = y + offset[1]
207    x2 = np.full_like(y, x[-1]) + offset[0]
208    y2 = y + offset[1]
209
210    if skew[0]:
211        y2 += x.max() * skew[0]
212    if skew[1]:
213        x1 += skew[1] * y
214        x2 += skew[1] * y
215
216    min_x = min(x1.min(), x2.min())
217    min_y = min(y1.min(), y2.min())
218    max_x = max(x1.max(), x2.max())
219    max_y = max(y1.max(), y2.max())
220    max_n = size // 6
221
222    h_lines = ["", "  <!-- Horizontal lines -->"] + svg_lines(x1, y1, x2, y2, max_n)
223
224    # Vertical lines
225    x1 = x + offset[0]
226    y1 = np.zeros_like(x) + offset[1]
227    x2 = x + offset[0]
228    y2 = np.full_like(x, y[-1]) + offset[1]
229
230    if skew[0]:
231        y1 += skew[0] * x
232        y2 += skew[0] * x
233    if skew[1]:
234        x2 += skew[1] * y.max()
235
236    v_lines = ["", "  <!-- Vertical lines -->"] + svg_lines(x1, y1, x2, y2, max_n)
237
238    color = "ECB172" if len(x) < max_n and len(y) < max_n else "8B4903"
239    corners = f"{x1[0]},{y1[0]} {x1[-1]},{y1[-1]} {x2[-1]},{y2[-1]} {x2[0]},{y2[0]}"
240    rect = [
241        "",
242        "  <!-- Colored Rectangle -->",
243        f'  <polygon points="{corners}" style="fill:#{color}A0;stroke-width:0"/>',
244    ]
245
246    return h_lines + v_lines + rect, (min_x, max_x, min_y, max_y)
247
248
249def svg_1d(chunks, sizes=None, **kwargs):
250    return svg_2d(((1,),) + chunks, **kwargs)
251
252
253def grid_points(chunks, sizes):
254    cumchunks = [np.cumsum((0,) + c) for c in chunks]
255    points = [x * size / x[-1] for x, size in zip(cumchunks, sizes)]
256    return points
257
258
259def draw_sizes(shape, size=200):
260    """Get size in pixels for all dimensions"""
261    mx = max(shape)
262    ratios = [mx / max(0.1, d) for d in shape]
263    ratios = [ratio_response(r) for r in ratios]
264    return tuple(size / r for r in ratios)
265
266
267def ratio_response(x):
268    """How we display actual size ratios
269
270    Common ratios in sizes span several orders of magnitude,
271    which is hard for us to perceive.
272
273    We keep ratios in the 1-3 range accurate, and then apply a logarithm to
274    values up until about 100 or so, at which point we stop scaling.
275    """
276    if x < math.e:
277        return x
278    elif x <= 100:
279        return math.log(x + 12.4)  # f(e) == e
280    else:
281        return math.log(100 + 12.4)
282