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