├── after_changing.png ├── default_report.png ├── .report.json ├── README.md ├── Untitled.ipynb ├── index.html └── index.js /after_changing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carreau/pytest-json-report-viewer/HEAD/after_changing.png -------------------------------------------------------------------------------- /default_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Carreau/pytest-json-report-viewer/HEAD/default_report.png -------------------------------------------------------------------------------- /.report.json: -------------------------------------------------------------------------------- 1 | {"created": 1652346151.077373, "duration": 0.012223005294799805, "exitcode": 4, "root": "/Users/bussonniermatthias/dev/pytest-json-report-viewer", "environment": {"Python": "3.10.0", "Platform": "macOS-12.3.1-x86_64-i386-64bit", "Packages": {"pytest": "7.1.2", "py": "1.11.0", "pluggy": "1.0.0"}, "Plugins": {"qt": "4.0.2", "metadata": "2.0.1", "json-report": "1.5.0", "hypothesis": "6.46.2", "napari-plugin-engine": "0.2.0", "order": "1.0.1", "napari": "0.4.16rc2.dev19+g47f98cc8f"}}, "summary": {"total": 0, "collected": 0}, "tests": []} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pytest Json Report Viewer. 2 | 3 | You run Pytest on many github action matrix element. 4 | 5 | You want a breakdown on the elements that take the most time. 6 | 7 | You use pytest with ``--duration`` but it's still hard to distinguish what is slow, especially with parametrized test. 8 | 9 | Fear not, this is for you. This will give you a `treemap` breakdown of test durations, 10 | and you can choose how to breakdown the levels, for example you can aggregate time for each tests, 11 | regardless of GitHub action matrix element. Or decide to look only at parametrized tests. 12 | 13 | Here is a screen shot example of napari test suite time breakdown, default view: 14 | 15 | 16 | ![](default_report.png) 17 | 18 | Removing the file in which the test are defined, summing across all 11 matrix element of github action, 19 | and adding a breakdown per item in `@pytest.markparametrize`: 20 | 21 | ![](after_changing.png) 22 | 23 | 24 | # configure GH Action to get artifacts: 25 | 26 | Dependencies: 27 | - add `pytest-json-report` to your test suite. 28 | 29 | 30 | 31 | ## Tox: 32 | 33 | If you are using tox in GHA add: 34 | 35 | ``` 36 | --json-report --json-report-file={toxinidir}/report-{envname}.json 37 | ``` 38 | 39 | ## Non TOX: 40 | 41 | TODO 42 | 43 | ## upload artifacts: 44 | 45 | After running the test: 46 | ``` 47 | - uses: actions/upload-artifact@v3 48 | with: 49 | name: upload pytest timing reports as json 50 | path: | 51 | ./report-*.json 52 | ``` 53 | 54 | You will be able to download artifacts from the test summary "artifact download" section. 55 | 56 | 57 | -------------------------------------------------------------------------------- /Untitled.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 13, 6 | "id": "9bfd8fa2", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "from numpy import array\n", 11 | "\n", 12 | "class Toto:\n", 13 | " \n", 14 | " def __array__(self):\n", 15 | " return array([1,1])\n", 16 | " \n", 17 | " def __add__(self, other):\n", 18 | " print('Toto __add__ called')\n", 19 | " # tell Python to look at other.__radd__(self)\n", 20 | " # Pythno do the same if hasattr(self, '__add__') == False\n", 21 | " return NotImplemented # != NotImplementedError, \n", 22 | " \n", 23 | " def __radd__(self, other):\n", 24 | " print('Toto __radd__ called')\n", 25 | " return NotImplemented # we'll likely raise a typerror, as __radd__ is called second.\n", 26 | " \n", 27 | "class Tata:\n", 28 | " \n", 29 | " def __array__(self):\n", 30 | " return array([1,1])\n", 31 | " \n", 32 | " def __add__(self, other):\n", 33 | " print('Tata __add__ called')\n", 34 | " # tell Python to look at other.__radd__(self)\n", 35 | " # Pythno do the same if hasattr(self, '__add__') == False\n", 36 | " return NotImplemented # != NotImplementedError, \n", 37 | " \n", 38 | " def __radd__(self, other):\n", 39 | " print('Tata __radd__ called')\n", 40 | " return array([3,3])\n", 41 | " " 42 | ] 43 | }, 44 | { 45 | "cell_type": "code", 46 | "execution_count": 14, 47 | "id": "4001f4b3", 48 | "metadata": {}, 49 | "outputs": [ 50 | { 51 | "name": "stdout", 52 | "output_type": "stream", 53 | "text": [ 54 | "Toto __add__ called\n", 55 | "Tata __radd__ called\n" 56 | ] 57 | }, 58 | { 59 | "data": { 60 | "text/plain": [ 61 | "array([3, 3])" 62 | ] 63 | }, 64 | "execution_count": 14, 65 | "metadata": {}, 66 | "output_type": "execute_result" 67 | } 68 | ], 69 | "source": [ 70 | "Toto() + Tata()" 71 | ] 72 | }, 73 | { 74 | "cell_type": "code", 75 | "execution_count": 15, 76 | "id": "5b5210c7", 77 | "metadata": {}, 78 | "outputs": [ 79 | { 80 | "name": "stdout", 81 | "output_type": "stream", 82 | "text": [ 83 | "Tata __add__ called\n", 84 | "Toto __radd__ called\n" 85 | ] 86 | }, 87 | { 88 | "ename": "TypeError", 89 | "evalue": "unsupported operand type(s) for +: 'Tata' and 'Toto'", 90 | "output_type": "error", 91 | "traceback": [ 92 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 93 | "\u001b[0;31mTypeError\u001b[0m Traceback (most recent call last)", 94 | "Input \u001b[0;32mIn [15]\u001b[0m, in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43mTata\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m+\u001b[39;49m\u001b[43mToto\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", 95 | "\u001b[0;31mTypeError\u001b[0m: unsupported operand type(s) for +: 'Tata' and 'Toto'" 96 | ] 97 | } 98 | ], 99 | "source": [ 100 | "Tata()+Toto()" 101 | ] 102 | }, 103 | { 104 | "cell_type": "code", 105 | "execution_count": null, 106 | "id": "82ff863e", 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [] 110 | } 111 | ], 112 | "metadata": { 113 | "kernelspec": { 114 | "display_name": "Python 3 (ipykernel)", 115 | "language": "python", 116 | "name": "python3" 117 | }, 118 | "language_info": { 119 | "codemirror_mode": { 120 | "name": "ipython", 121 | "version": 3 122 | }, 123 | "file_extension": ".py", 124 | "mimetype": "text/x-python", 125 | "name": "python", 126 | "nbconvert_exporter": "python", 127 | "pygments_lexer": "ipython3", 128 | "version": "3.10.0" 129 | } 130 | }, 131 | "nbformat": 4, 132 | "nbformat_minor": 5 133 | } 134 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | Pytest Json-report viewer 10 | 11 | 12 | 13 | 17 | 18 | 19 | 23 | 24 | 25 | 29 | 30 | 34 | 118 | 119 | 120 | 125 |
126 | 134 | 142 | 143 | 151 | 152 | 160 | 161 | 169 |
170 | 171 | 179 | 180 |
181 | 182 |

183 | Drag pytest json report files and select the you decomposition you like 184 | above. 185 |

186 |

187 | You can generate report with 188 | pytest --json-report --json-report-file=report.json 189 |

190 |
191 | 192 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | DATA = null; 2 | 3 | function timeformat(value) { 4 | if (value > 120 * 1000) { 5 | // 2 min 6 | return d3.format(".3r")(value / 1000 / 60) + "min"; 7 | } else if (value > 1500) { 8 | return d3.format(".3r")(value / 1000) + "s"; 9 | } else { 10 | return d3.format(".3r")(value) + "ms"; 11 | } 12 | } 13 | 14 | function onlyUnique(value, index, self) { 15 | return self.indexOf(value) === index; 16 | } 17 | 18 | function process(raw, name) { 19 | // process raw data from pytest-json-report, and flatten it a bit 20 | // for latter nesting. 21 | // name: will be the name of the uploaded file. 22 | 23 | var data = []; 24 | 25 | for (test of raw["tests"]) { 26 | var xa = test["nodeid"].split("::"); 27 | file = xa[0]; 28 | group = xa[1]; 29 | for (k of ["call", "setup", "teardown"]) { 30 | try { 31 | var item = {}; 32 | item.key = file; 33 | item.group = group; 34 | item.kind = k; 35 | item.value = test[k].duration * 1000; 36 | item.outcome = test[k].outcome; 37 | item.name = name; 38 | data.push(item); 39 | } catch (e) { 40 | //console.log( 41 | // "test", 42 | // k, 43 | // "may not have duration ? likely skipped test", 44 | // test, 45 | // k 46 | //); 47 | } 48 | } 49 | } 50 | return data; 51 | } 52 | 53 | function readFileAsync(file) { 54 | return new Promise((resolve, reject) => { 55 | let reader = new FileReader(); 56 | 57 | reader.onload = () => { 58 | resolve(reader.result); 59 | }; 60 | 61 | reader.onerror = reject; 62 | 63 | reader.readAsArrayBuffer(file); 64 | }); 65 | } 66 | 67 | function dropHandler(ev) { 68 | // we let user drop file, 69 | // and will process them. 70 | 71 | // Prevent default behavior (Prevent file from being opened) 72 | ev.preventDefault(); 73 | 74 | // right now we'll use the global DATA declaration. 75 | DATA = []; 76 | 77 | if (ev.dataTransfer.items) { 78 | // Use DataTransferItemList interface to access the file(s) 79 | var processing_count = ev.dataTransfer.items.length; 80 | var processed = 0; 81 | for (var i = 0; i < ev.dataTransfer.items.length; i++) { 82 | // If dropped items aren't files, reject them 83 | if (ev.dataTransfer.items[i].kind === "file") { 84 | var file = ev.dataTransfer.items[i].getAsFile(); 85 | const name = file.name; 86 | const fr = new FileReader(); 87 | fr.onloadend = function () { 88 | raw = JSON.parse(this.result); 89 | 90 | DATA = DATA.concat(process(raw, name)); 91 | processed += 1; 92 | if (processed == processing_count) { 93 | init(); 94 | } 95 | }; 96 | 97 | fr.readAsText(file); 98 | } 99 | } 100 | } else { 101 | // Use DataTransfer interface to access the file(s) 102 | } 103 | } 104 | 105 | function dragOverHandler(ev) { 106 | // Prevent default behavior (Prevent file from being opened) 107 | ev.preventDefault(); 108 | } 109 | 110 | window.addEventListener("message", function (e) { 111 | var opts = e.data.opts, 112 | data = e.data.data; 113 | 114 | return main(opts, data); 115 | }); 116 | 117 | var defaults = { 118 | margin: { 119 | top: 24, 120 | right: 10, 121 | bottom: 0, 122 | left: 10, 123 | }, 124 | rootname: "TOP", 125 | format: ".3r", 126 | title: "", 127 | }; 128 | 129 | function main(opts, data) { 130 | var root, 131 | opts = $.extend(true, {}, defaults, opts), 132 | formatNumber = d3.format(opts.format), 133 | rname = opts.rootname, 134 | margin = opts.margin, 135 | theight = 1; 136 | 137 | var ww = window.innerWidth - 20; 138 | var hh = window.innerHeight - 110; 139 | $("#chart").width(ww).height(hh); 140 | let width = ww - margin.left - margin.right + 22; 141 | let height = hh - margin.top - margin.bottom - theight; 142 | let transitioning; 143 | 144 | var color = d3.scale.category10(); 145 | //var color = d3.scale.linear(0, 100); 146 | 147 | var x = d3.scale.linear().domain([0, width]).range([0, width]); 148 | 149 | var y = d3.scale.linear().domain([0, height]).range([0, height]); 150 | 151 | var treemap = d3.layout 152 | .treemap() 153 | //.tile(d3[document.getElementById("layout").value]) 154 | .children(function (d, depth) { 155 | return depth ? null : d._children; 156 | }) 157 | .sort(function (a, b) { 158 | return a.value - b.value; 159 | }) 160 | .ratio((height / width) * 0.5 * (1 + Math.sqrt(5))) 161 | .round(false); 162 | 163 | d3.select("#chart>svg").remove(); 164 | d3.select("#chart>p").remove(); 165 | var svg = d3 166 | .select("#chart") 167 | .append("svg") 168 | .style("font", "14px sans-serif") 169 | .style("fill", "white") 170 | .attr("width", width + margin.left + margin.right) 171 | .attr("height", height + margin.bottom + margin.top) 172 | .style("margin-left", -margin.left + "px") 173 | .style("margin-right", -margin.right + "px") 174 | .append("g") 175 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")") 176 | .style("shape-rendering", "crispEdges"); 177 | 178 | var grandparent = svg.append("g").attr("class", "grandparent"); 179 | 180 | grandparent 181 | .append("rect") 182 | .attr("y", -margin.top) 183 | .attr("width", width) 184 | .attr("height", margin.top); 185 | 186 | grandparent 187 | .append("text") 188 | .attr("x", 6) 189 | .attr("y", 6 - margin.top) 190 | .attr("dy", ".75em"); 191 | 192 | if (opts.title) { 193 | $("#chart").prepend("

" + opts.title + "

"); 194 | } 195 | if (data instanceof Array) { 196 | console.info("INIT", data); 197 | root = { 198 | key: rname, 199 | _compute: data, 200 | }; 201 | } else { 202 | root = data; 203 | } 204 | 205 | initialize(root); 206 | flatten(root); 207 | layout(root, 2); 208 | display(root); 209 | 210 | if (window.parent !== window) { 211 | var myheight = 212 | document.documentElement.scrollHeight || document.body.scrollHeight; 213 | window.parent.postMessage( 214 | { 215 | height: myheight, 216 | }, 217 | "*" 218 | ); 219 | } 220 | 221 | function initialize(root) { 222 | root.x = root.y = 0; 223 | root.dx = width; 224 | root.dy = height; 225 | root.depth = 0; 226 | } 227 | 228 | // Aggregate the values for internal nodes. This is normally done by the 229 | // treemap layout, but not here because of our custom implementation. 230 | // We also take a snapshot of the original children (_children) to avoid 231 | // the children being overwritten when when layout is computed. 232 | 233 | function flatten(d) { 234 | // not an array 235 | if (d.value >= 0) { 236 | return [[d.value], [d.outcome]]; 237 | } 238 | acc = d.values 239 | ? d.values.reduce( 240 | function (previous, current, index, array) { 241 | _t = flatten(current); 242 | value = _t[0]; 243 | outcome = _t[1]; 244 | return [previous[0].concat(value), previous[1].concat(outcome)]; 245 | }, 246 | [[], []] 247 | ) 248 | : [[0], ["passed"]]; 249 | let total = acc[0].reduce((a, b) => a + b); 250 | d.value = total; 251 | //d.value = Math.max(...acc[0]); 252 | d.outcome = acc[1].reduce(function (p, c) { 253 | if (p == c) { 254 | return c; 255 | } else { 256 | console.log("PC", p, c); 257 | return "mixed"; 258 | } 259 | }, "passed"); 260 | 261 | if (d.outcome == "mixed") { 262 | console.log(acc[1].filter(onlyUnique)); 263 | } 264 | d._children = d.values ? d.values : d.value; 265 | d._children.forEach((element) => { 266 | element.prct = element.value / total; 267 | }); 268 | //d._children = d.values ? d.values : d.value; 269 | return acc; 270 | } 271 | 272 | // Compute the treemap layout recursively such that each group of siblings 273 | // uses the same size (1×1) rather than the dimensions of the parent cell. 274 | // This optimizes the layout for the current zoom state. Note that a wrapper 275 | // object is created for the parent node for each group of siblings so that 276 | // the parent’s dimensions are not discarded as we recurse. Since each group 277 | // of sibling was laid out in 1×1, we must rescale to fit using absolute 278 | // coordinates. This lets us use a viewport to zoom. 279 | function layout(d) { 280 | if (d._children) { 281 | treemap.nodes({ 282 | _children: d._children, 283 | }); 284 | d._children.forEach(function (c) { 285 | c.x = d.x + c.x * d.dx; 286 | c.y = d.y + c.y * d.dy; 287 | c.dx *= d.dx; 288 | c.dy *= d.dy; 289 | c.parent = d; 290 | layout(c); 291 | }); 292 | } 293 | } 294 | 295 | function display(d) { 296 | grandparent 297 | .datum(d.parent) 298 | .on("click", transition) 299 | .select("text") 300 | .text(name(d)); 301 | 302 | var g1 = svg.insert("g", ".grandparent").datum(d).attr("class", "depth"); 303 | 304 | var g = g1.selectAll("g").data(d._children).enter().append("g"); 305 | 306 | g.filter(function (d) { 307 | return d._children; 308 | }) 309 | .classed("children", true) 310 | .on("click", transition); 311 | 312 | var children = g 313 | .selectAll(".child") 314 | .data(function (d) { 315 | return d._children || [d]; 316 | }) 317 | .enter() 318 | .append("g"); 319 | 320 | children 321 | .append("rect") 322 | .attr("class", "child") 323 | .call(rect) 324 | .append("title") 325 | .text(function (d) { 326 | return ( 327 | d.parent.key + 328 | "\n" + 329 | //'Group:'+d.goup+'\n'+ 330 | //'Kind:'+d.kind+'\n'+ 331 | //'Name:'+d.name+"\n"+ 332 | "(" + 333 | timeformat(d.parent.value) + 334 | " - " + 335 | d3.format(".3r")(d.parent.prct * 100) + 336 | "%" + 337 | ")" + 338 | "\n--\n" + 339 | d.key + 340 | "\n" + 341 | //'Group:'+d.goup+'\n'+ 342 | //'Kind:'+d.kind+'\n'+ 343 | //'Name:'+d.name+"\n"+ 344 | "(" + 345 | timeformat(d.value) + 346 | " - " + 347 | d3.format(".3r")(d.prct * 100) + 348 | "%" + 349 | ")" 350 | ); 351 | }); 352 | children 353 | .append("text") 354 | .attr("class", "ctext") 355 | .text(function (d) { 356 | return d.key; 357 | }) 358 | .call(text2); 359 | 360 | g.append("rect").attr("class", "parent").call(rect); 361 | 362 | var t = g.append("text").attr("class", "ptext").attr("dy", ".75em"); 363 | 364 | t.append("tspan").text(function (d) { 365 | return d.key; 366 | }); 367 | t.append("tspan") 368 | .attr("dy", "1.0em") 369 | .text(function (d) { 370 | return ( 371 | timeformat(d.value) + " - " + d3.format(".3r")(d.prct * 100) + "%" 372 | ); 373 | }); 374 | t.call(text); 375 | 376 | g.selectAll("rect").style("fill", function (d) { 377 | //return "#e53935"; 378 | return d.outcome == "passed" 379 | ? "#00cc00" 380 | : d.outcome == "failed" 381 | ? "#cc0000" 382 | : "#FFA500"; 383 | return color(1); 384 | }); 385 | 386 | function transition(d) { 387 | if (transitioning || !d) return; 388 | transitioning = true; 389 | 390 | var g2 = display(d), 391 | t1 = g1.transition().duration(250), 392 | t2 = g2.transition().duration(250); 393 | 394 | // Update the domain only after entering new elements. 395 | x.domain([d.x, d.x + d.dx]); 396 | y.domain([d.y, d.y + d.dy]); 397 | 398 | // Enable anti-aliasing during the transition. 399 | svg.style("shape-rendering", null); 400 | 401 | // Draw child nodes on top of parent nodes. 402 | svg.selectAll(".depth").sort(function (a, b) { 403 | return a.depth - b.depth; 404 | }); 405 | 406 | // Fade-in entering text. 407 | g2.selectAll("text").style("fill-opacity", 0); 408 | 409 | // Transition to the new view. 410 | t1.selectAll(".ptext").call(text).style("fill-opacity", 0); 411 | t1.selectAll(".ctext").call(text2).style("fill-opacity", 0); 412 | t2.selectAll(".ptext").call(text).style("fill-opacity", 1); 413 | t2.selectAll(".ctext").call(text2).style("fill-opacity", 1); 414 | t1.selectAll("rect").call(rect); 415 | t2.selectAll("rect").call(rect); 416 | 417 | // Remove the old node when the transition is finished. 418 | t1.remove().each("end", function () { 419 | svg.style("shape-rendering", "crispEdges"); 420 | transitioning = false; 421 | }); 422 | } 423 | 424 | return g; 425 | } 426 | 427 | function text(text) { 428 | text.selectAll("tspan").attr("x", function (d) { 429 | return x(d.x) + 6; 430 | }); 431 | text 432 | .attr("x", function (d) { 433 | return x(d.x) + 6; 434 | }) 435 | .attr("y", function (d) { 436 | return y(d.y) + 6; 437 | }) 438 | .style("opacity", function (d) { 439 | return this.getComputedTextLength() < x(d.x + d.dx) - x(d.x) ? 1 : 0; 440 | }); 441 | } 442 | 443 | function text2(text) { 444 | text 445 | .attr("x", function (d) { 446 | return x(d.x + d.dx) - this.getComputedTextLength() - 6; 447 | }) 448 | .attr("y", function (d) { 449 | return y(d.y + d.dy) - 6; 450 | }) 451 | .style("opacity", function (d) { 452 | return this.getComputedTextLength() < x(d.x + d.dx) - x(d.x) ? 1 : 0; 453 | }); 454 | } 455 | 456 | function rect(rect) { 457 | rect 458 | .attr("x", function (d) { 459 | return x(d.x); 460 | }) 461 | .attr("y", function (d) { 462 | return y(d.y); 463 | }) 464 | .attr("width", function (d) { 465 | return x(d.x + d.dx) - x(d.x); 466 | }) 467 | .attr("height", function (d) { 468 | return y(d.y + d.dy) - y(d.y); 469 | }); 470 | } 471 | 472 | function name(d) { 473 | return d.parent 474 | ? name(d.parent) + 475 | " / " + 476 | d.key + 477 | " (" + 478 | timeformat(d.value) + 479 | " - " + 480 | d3.format(".3r")(d.prct * 100) + 481 | "%)" 482 | : d.key + " (" + timeformat(d.value) + ")"; 483 | } 484 | } 485 | 486 | var init = function (err, res) { 487 | res = JSON.parse(JSON.stringify(DATA)); 488 | 489 | res.map(function (res) { 490 | let ind = res.group.indexOf("["); 491 | if (ind != -1) { 492 | res.param = res.group.slice(ind + 1, res.group.length - 1); 493 | res.group = res.group.slice(0, ind); 494 | } else { 495 | res.param = "No-parameters"; 496 | } 497 | }); 498 | 499 | var n = d3.nest(); 500 | 501 | for (const i of ["sZ", "sA", "sB", "sC", "sD"]) { 502 | let vv = document.getElementById(i).value; 503 | if (vv == "rollup") { 504 | continue; 505 | } 506 | 507 | n = n.key(function (d) { 508 | return d[vv]; 509 | }); 510 | } 511 | n = n.rollup(function (v) { 512 | res = d3.sum(v, (x) => x.value); 513 | red = v.reduce(function (previous, current) { 514 | if (current.outcome == "skipped") { 515 | return "passed"; 516 | } 517 | return previous == current.outcome ? previous : "mixed"; 518 | }, v[0].outcome); 519 | return [ 520 | { 521 | key: "", 522 | outcome: red, 523 | value: res, 524 | }, 525 | ]; 526 | }); 527 | var data = n.entries(res); 528 | main( 529 | { 530 | //title: "Pytest Time breakdown" 531 | }, 532 | { 533 | key: "Total", 534 | values: data, 535 | } 536 | ); 537 | }; 538 | 539 | //if (window.location.hash === "") { 540 | // //d3.json("x.json", function(err, data) { 541 | // // DATA = data; 542 | // // console.log("HERE", DATA) 543 | // // init() 544 | // //}); 545 | //} 546 | 547 | window.onresize = init; 548 | document.getElementById("sZ").addEventListener("change", init); 549 | document.getElementById("sA").addEventListener("change", init); 550 | document.getElementById("sB").addEventListener("change", init); 551 | document.getElementById("sC").addEventListener("change", init); 552 | document.getElementById("sD").addEventListener("change", init); 553 | //document.getElementById("layout").addEventListener("change", init) 554 | --------------------------------------------------------------------------------