github.com/wtsi-ssg/wrstat@v1.1.4-0.20221008232152-3030622a8cf8/server/static/tree/js/dtreemap.js (about) 1 require.config({ 2 paths: { 3 cookie: "js.cookie.min" 4 } 5 }) 6 7 define(["d3", "cookie"], function (d3, cookie) { 8 var current; 9 let allNodes = new Map(); 10 11 var margin = { top: 22, right: 0, bottom: 0, left: 0 }, 12 width = 960, 13 height = 500 - margin.top - margin.bottom, 14 transitioning; 15 16 var x = d3.scale.linear() 17 .domain([0, width]) 18 .range([0, width]); 19 20 var y = d3.scale.linear() 21 .domain([0, height]) 22 .range([0, height]); 23 24 var treemap = d3.layout.treemap() 25 .sort(function (a, b) { return a.value - b.value; }) 26 .ratio(height / width * 0.5 * (1 + Math.sqrt(5))) 27 .round(false); 28 29 var svg = d3.select("#chart").append("svg") 30 .attr("width", width + margin.left + margin.right) 31 .attr("height", height + margin.bottom + margin.top) 32 .style("margin-left", -margin.left + "px") 33 .style("margin.right", -margin.right + "px") 34 .append("g") 35 .attr("transform", "translate(" + margin.left + "," + margin.top + ")") 36 .style("shape-rendering", "crispEdges"); 37 38 var grandparent = svg.append("g") 39 .attr("class", "grandparent"); 40 41 grandparent.append("rect") 42 .attr("y", -margin.top) 43 .attr("width", width) 44 .attr("height", margin.top); 45 46 grandparent.append("text") 47 .attr("x", width - 6) 48 .attr("y", 6 - margin.top) 49 .attr("text-anchor", "end") 50 .attr("dy", ".75em"); 51 52 let hash = window.location.hash.substring(1); 53 let hasher = new URL('https://hasher.com') 54 hasher.search = hash 55 56 function getURLParam(name) { 57 return hasher.searchParams.get(name) 58 } 59 60 function setURLParams() { 61 hasher.searchParams.set('path', current.path); 62 63 let groups = d3.select('#groups_list').property('value'); 64 hasher.searchParams.set('groups', groups); 65 66 let users = d3.select('#users_list').property('value'); 67 hasher.searchParams.set('users', users); 68 69 let fts = d3.select('#ft_list').property('value'); 70 hasher.searchParams.set('fts', fts); 71 72 let area = d3.select('input[name="area"]:checked').property("value"); 73 hasher.searchParams.set('area', area); 74 75 hasher.searchParams.set('age', age_filter); 76 hasher.searchParams.set('supergroup', group_area); 77 78 window.location.hash = hasher.searchParams; 79 } 80 81 function initialize(root) { 82 root.x = root.y = 0; 83 root.dx = width; 84 root.dy = height; 85 root.depth = 0; 86 } 87 88 $('.flexdatalist').flexdatalist({ 89 selectionRequired: 1, 90 minLength: 0 91 }); 92 93 var $filter_inputs = $('.flexdatalist'); 94 95 var filterIDs = ['groups_list', 'users_list', 'ft_list']; 96 let filters = new Map(); 97 98 let age_filter = "0"; 99 let group_area = "-none-"; 100 let area_groups = []; 101 102 function getFilters() { 103 str = ""; 104 filterIDs.forEach(id => str += id + ':' + filters.get(id) + ';'); 105 str += "age:" + age_filter; 106 str += ";super:" + group_area; 107 return str; 108 } 109 110 var updateFilters = function ($target) { 111 $target.each(function () { 112 filters.set($(this).attr('id'), $(this).val()); 113 }); 114 }; 115 116 $filter_inputs 117 .on('change:flexdatalist', function (e, set) { 118 updateFilters($(this)); 119 }); 120 121 function storeFilters(node) { 122 currentFilters = getFilters(); 123 node.filters = currentFilters; 124 125 if (node.children) { 126 node.children.forEach(child => child.filters = currentFilters); 127 } 128 } 129 130 function atimeToDays(node) { 131 let atime = new Date(node.atime); 132 let now = new Date(); 133 return Math.round((now - atime) / (1000 * 60 * 60 * 24)); 134 } 135 136 function atimeToColorClass(node) { 137 let days = atimeToDays(node); 138 let c = "parent " 139 140 if (days >= 365 * 2) { 141 c += "age_2years" 142 } else if (days >= 365) { 143 c += "age_1year" 144 } else if (days >= 304) { 145 c += "age_10months" 146 } else if (days >= 243) { 147 c += "age_8months" 148 } else if (days >= 182) { 149 c += "age_6months" 150 } else if (days >= 91) { 151 c += "age_3months" 152 } else if (days >= 61) { 153 c += "age_2months" 154 } else if (days >= 30) { 155 c += "age_1month" 156 } else { 157 c += "age_1week" 158 } 159 160 return c 161 } 162 163 function atimePassesAgeFilter(node) { 164 let days = atimeToDays(node); 165 return days >= age_filter 166 } 167 168 // Compute the treemap layout recursively such that each group of siblings 169 // uses the same size (1×1) rather than the dimensions of the parent cell. 170 // This optimizes the layout for the current zoom state. Note that a wrapper 171 // object is created for the parent node for each group of siblings so that 172 // the parent’s dimensions are not discarded as we recurse. Since each group 173 // of sibling was laid out in 1×1, we must rescale to fit using absolute 174 // coordinates. This lets us use a viewport to zoom. 175 function layout(d) { 176 var children = getChildren(d); 177 if (children && children.length > 0) { 178 treemap.nodes({ children: children }); 179 children.forEach(function (c) { 180 c.x = d.x + c.x * d.dx; 181 c.y = d.y + c.y * d.dy; 182 c.dx *= d.dx; 183 c.dy *= d.dy; 184 c.parent = d; 185 layout(c); 186 }); 187 } 188 } 189 190 function display(d) { 191 let gt = grandparent 192 .datum(d.parent) 193 .on("click", transition) 194 .select("text"); 195 196 if (d.path == "/") { 197 grandparent.style("cursor", "default") 198 gt.text('') 199 } else { 200 grandparent.style("cursor", "pointer") 201 gt.text('↵') 202 } 203 204 var g1 = svg.insert("g", ".grandparent") 205 .datum(d) 206 .classed("depth", true); 207 208 var g = g1.selectAll("g") 209 .data(getChildren(d)) 210 .enter().append("g"); 211 212 g.filter(function (d) { return d.has_children; }) 213 .classed("children", true) 214 .style("cursor", "pointer") 215 .on("click", transition); 216 217 g.filter(function (d) { return !d.has_children; }) 218 .classed("childless", true) 219 .style("cursor", "default"); 220 221 g.selectAll(".child") 222 .data(function (d) { return getChildren(d) || [d]; }) 223 .enter().append("rect") 224 .classed("child", true) 225 .call(rect); 226 227 g.append("rect") 228 .attr("class", function (d) { return atimeToColorClass(d) }) // also sets parent class 229 .call(rect); 230 231 var titlesvg = g.append("svg") 232 .classed("parent_title", true) 233 .attr("viewBox", "-100 -10 200 20") 234 .attr("preserveAspectRatio", "xMidYMid meet") 235 .call(rect); 236 237 titlesvg.append("text") 238 .attr("font-size", 16) 239 .attr("x", 0) 240 .attr("y", 0) 241 .attr("width", 200) 242 .attr("height", 20) 243 .attr("dy", ".3em") 244 .style("text-anchor", "middle") 245 .text(function (d) { return d.name; }); 246 247 g.on("mouseover", mouseover).on("mouseout", mouseout); 248 249 d3.selectAll("#select_area input").on("change", function () { 250 areaBasedOnSize = this.value == "size" 251 setAllValues() 252 transition(current); 253 }); 254 255 d3.select("#filterButton").on('click', function () { 256 age_filter = $("#age_filter").val(); 257 258 // console.log('getting fresh filtered data for ', data.path); 259 getData(current.path, function (data) { 260 cloneProperties(data, current) 261 setValues(current); 262 storeFilters(current); 263 setURLParams(); 264 transition(current); 265 }); 266 }); 267 268 function mouseover(g) { 269 showDetails(g) 270 } 271 272 function mouseout() { 273 showCurrentDetails() 274 } 275 276 function transition(d) { 277 if (!d && current.path !== "/") { 278 parent = current.path.substring(0, current.path.lastIndexOf('/')); 279 current.path = parent; 280 setURLParams(); 281 window.location.reload(false); 282 } 283 284 if (transitioning || !d) return; 285 transitioning = true; 286 287 function do_transition(d) { 288 layout(d); 289 290 var g2 = display(d), 291 t1 = g1.transition().duration(250), 292 t2 = g2.transition().duration(250); 293 294 // Update the domain only after entering new elements. 295 x.domain([d.x, d.x + d.dx]); 296 y.domain([d.y, d.y + d.dy]); 297 298 // Enable anti-aliasing during the transition. 299 svg.style("shape-rendering", null); 300 301 // Draw child nodes on top of parent nodes. 302 svg.selectAll(".depth").sort(function (a, b) { return a.depth - b.depth; }); 303 304 // Fade-in entering text. 305 g2.selectAll("text").style("fill-opacity", 0); 306 307 // Transition to the new view. 308 t1.selectAll(".parent_title").call(rect); 309 t2.selectAll(".parent_title").call(rect); 310 t1.selectAll("text").style("fill-opacity", 0); 311 t2.selectAll("text").style("fill-opacity", 1); 312 t1.selectAll("rect").call(rect); 313 t2.selectAll("rect").call(rect); 314 315 // Remove the old node when the transition is finished. 316 t1.remove().each("end", function () { 317 svg.style("shape-rendering", "crispEdges"); 318 transitioning = false; 319 }); 320 } 321 322 if (d.children && d.filters == getFilters()) { 323 // console.log('using old data for ', d.path); 324 do_transition(d) 325 updateDetails(d); 326 setAllFilterOptions(d); 327 setURLParams(); 328 createBreadcrumbs(d.path); 329 } else { 330 // console.log('getting fresh data for ', d.path); 331 getData(d.path, function (data) { 332 cloneProperties(data, d); 333 setValues(d); 334 storeFilters(d); 335 do_transition(d); 336 updateDetails(d); 337 setAllFilterOptions(d); 338 setURLParams(); 339 }); 340 } 341 } 342 343 return g; 344 } 345 346 function rect(rect) { 347 rect.attr("x", function (d) { return x(d.x); }) 348 .attr("y", function (d) { return y(d.y); }) 349 .attr("width", function (d) { return x(d.x + d.dx) - x(d.x); }) 350 .attr("height", function (d) { return y(d.y + d.dy) - y(d.y); }); 351 } 352 353 function getChildren(parent) { 354 return parent.children 355 } 356 357 function constructAPIURL(path) { 358 let url = "/rest/v1/auth/tree?path=" + path 359 360 let groups = d3.select('#groups_list').property('value'); 361 let users = d3.select('#users_list').property('value'); 362 let filetypes = d3.select('#ft_list').property('value'); 363 364 if (area_groups.length > 0) { 365 let garray = []; 366 if (groups != "") { 367 garray = groups.split(","); 368 garray = garray.concat(area_groups); 369 } else { 370 garray = area_groups; 371 } 372 373 groups = garray.join(); 374 } 375 376 if (groups != "") { 377 url += '&groups=' + groups 378 } 379 380 if (users != "") { 381 url += '&users=' + users 382 } 383 384 if (filetypes != "") { 385 url += '&types=' + filetypes 386 } 387 388 return url 389 } 390 391 function getData(path, loadFunction) { 392 d3.select("#spinner").style("display", "inline-block") 393 394 fetch(constructAPIURL(path), { 395 headers: { 396 "Authorization": `Bearer ${cookie.get('jwt')}` 397 } 398 }).then(r => { 399 if (r.status === 401) { 400 cookie.remove("jwt", { path: "" }); 401 window.location.reload(); 402 } else if (r.status === 400) { 403 throw ("bad query") 404 } else { 405 return r.json() 406 } 407 }).then(d => { 408 d3.select("#spinner").style("display", "none"); 409 if (d.count > 0) { 410 d3.select("#error").text("") 411 allNodes.set(d.path, d); 412 filterYoungChildren(d); 413 loadFunction(d); 414 createBreadcrumbs(d.path); 415 } 416 else { 417 d3.select("#error").text("error: no results") 418 } 419 }).catch(e => { 420 d3.select("#spinner").style("display", "none"); 421 d3.select("#error").text("error: " + e) 422 }) 423 424 } 425 426 function filterYoungChildren(node) { 427 if (!node.children || age_filter < 30) { 428 return; 429 } 430 431 node.children = node.children.filter(child => atimePassesAgeFilter(child)) 432 } 433 434 var areaBasedOnSize = true 435 436 function cloneProperties(a, b) { 437 b.size = a.size; 438 b.count = a.count; 439 b.atime = a.atime; 440 b.groups = a.groups; 441 b.users = a.users; 442 b.filetypes = a.filetypes; 443 b.children = a.children; 444 b.has_children = a.has_children; 445 } 446 447 function setValues(d) { 448 if (areaBasedOnSize) { 449 d.value = d.size; 450 451 if (d.children) { 452 d.children.forEach(item => item.value = item.size); 453 } 454 } else { 455 d.value = d.count; 456 457 if (d.children) { 458 d.children.forEach(item => item.value = item.count); 459 } 460 } 461 } 462 463 function setAllValues() { 464 for (let d of allNodes.values()) { 465 setValues(d); 466 } 467 } 468 469 var BINARY_UNIT_LABELS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]; 470 471 function bytesHuman(bytes) { 472 var e = Math.floor(Math.log(bytes) / Math.log(1024)); 473 return parseFloat((bytes / Math.pow(1024, e)).toFixed(2)) + " " + BINARY_UNIT_LABELS[e]; 474 } 475 476 var NUMBER_UNIT_LABELS = ["", "K", "M", "B", "T", "Q"]; 477 478 function countHuman(count) { 479 var unit = Math.floor((count / 1.0e+1).toFixed(0).toString().length); 480 var r = unit % 3; 481 var x = Math.abs(Number(count)) / Number('1.0e+' + (unit - r)).toFixed(2); 482 return parseFloat(x.toFixed(2)) + ' ' + NUMBER_UNIT_LABELS[Math.floor(unit / 3)]; 483 } 484 485 function commaSep(list) { 486 return list.join(", "); 487 } 488 489 function dfnClasses(list) { 490 const dfns = []; 491 492 list.forEach(function (ft) { 493 let c = ft; 494 c = c.replace(/[./]/, ""); 495 dfns.push("<dfn class='ft-" + c + "'>" + ft + "</dfn>"); 496 }); 497 498 return commaSep(dfns); 499 } 500 501 function showDetails(node) { 502 d3.select('#details_path').text(node.path); 503 d3.select('#details_size').text(bytesHuman(node.size)); 504 d3.select('#details_count').text(countHuman(node.count)); 505 d3.select('#details_atime').text(node.atime); 506 d3.select('#details_groups').text(commaSep(node.groups)); 507 d3.select('#details_users').text(commaSep(node.users)); 508 d3.select('#details_filetypes').html(dfnClasses(node.filetypes)); 509 } 510 511 function showCurrentDetails() { 512 showDetails(current); 513 } 514 515 function updateDetails(node) { 516 current = node; 517 showCurrentDetails(); 518 } 519 520 var supergroups; 521 function setGroupAreas(data) { 522 supergroups = data.areas; 523 if (supergroups === null) { 524 return; 525 } 526 527 let select = d3.select('#supergrouping'); 528 529 let keys = Object.keys(supergroups); 530 keys.unshift("-none-") 531 532 select 533 .selectAll('option') 534 .remove(); 535 536 select 537 .selectAll('option') 538 .data(keys).enter() 539 .append('option') 540 .text(function (d) { return d; }); 541 542 select.on('change', function () { 543 group_area = d3.select(this).property('value'); 544 if (group_area == "-none-") { 545 area_groups = []; 546 return; 547 } 548 549 area_groups = supergroups[group_area]; 550 }); 551 552 if (group_area != "-none-") { 553 $('#supergrouping').val(group_area).prop('selected', true); 554 area_groups = supergroups[group_area]; 555 } 556 557 $("#supergroups").show(); 558 } 559 560 function setAllFilterOptions(data) { 561 setFilterOptions('#groups_list', data.groups); 562 setFilterOptions('#users_list', data.users); 563 setFilterOptions('#ft_list', data.filetypes); 564 } 565 566 function setFilterOptions(id, elements) { 567 let select = d3.select(id); 568 569 select 570 .selectAll('option') 571 .remove(); 572 573 select 574 .selectAll('option') 575 .data(elements).enter() 576 .append('option') 577 .text(function (d) { return d; }) 578 .property("selected", function (d) { return d === filters.get(id) }); 579 } 580 581 function setTimestamp(data) { 582 $('#timestamp').attr('datetime', data.timestamp) 583 $('#timestamp').timeago(); 584 } 585 586 let path = "/"; 587 let groups; 588 let users; 589 let fts; 590 591 function setPageDefaultsFromHash() { 592 path = getURLParam('path') 593 if (!path || !path.startsWith("/")) { 594 path = "/"; 595 } 596 597 groups = getURLParam('groups'); 598 if (groups) { 599 d3.select('#groups_list').property('value', groups); 600 } 601 602 users = getURLParam('users'); 603 if (users) { 604 d3.select('#users_list').property('value', users); 605 } 606 607 fts = getURLParam('fts'); 608 if (fts) { 609 d3.select('#ft_list').property('value', fts); 610 } 611 612 let area = getURLParam('area'); 613 if (area) { 614 $("#" + area).prop("checked", true); 615 areaBasedOnSize = area == "size" 616 } 617 618 age_filter = getURLParam('age'); 619 if (age_filter == null) { 620 age_filter = "0"; 621 } 622 $("#age_filter").val(age_filter); 623 624 group_area = getURLParam('supergroup'); 625 if (group_area == null) { 626 group_area = "-none-"; 627 } 628 } 629 630 setPageDefaultsFromHash(); 631 632 function createBreadcrumbs(path) { 633 $("#breadcrumbs").empty(); 634 635 if (path === "/") { 636 $("#breadcrumbs").append('<span></span><button class="dead">/</button>'); 637 return; 638 } 639 640 let dirs = path.split("/"); 641 dirs[0] = "/"; 642 let sep = ""; 643 let max = dirs.length - 1; 644 645 for (var i = 0; i <= max; i++) { 646 let dir = dirs[i]; 647 648 if (i > 1) { 649 sep = "/" 650 } 651 652 $("#breadcrumbs").append('<span>' + sep + '</span>'); 653 654 if (i < max) { 655 let this_id = 'crumb' + i; 656 $("#breadcrumbs").append('<button id="' + this_id + '">' + dir + '</button>'); 657 658 let this_path = "/" + dirs.slice(1, i + 1).join("/"); 659 $('#' + this_id).click(function () { 660 current.path = this_path; 661 setURLParams(); 662 window.location.reload(false); 663 }); 664 } else { 665 $("#breadcrumbs").append('<span>' + dir + '</span>'); 666 } 667 } 668 } 669 670 // first fetch just to get group areas 671 getData(path, function (data) { 672 setGroupAreas(data); 673 674 // now do the real fetch 675 getData(path, function (data) { 676 initialize(data); 677 setValues(data); 678 layout(data); 679 display(data); 680 updateDetails(data); 681 setAllFilterOptions(data); 682 683 if (groups) { 684 $('#groups_list').val(groups) 685 } 686 687 if (users) { 688 $('#users_list').val(users) 689 } 690 691 if (fts) { 692 $('#ft_list').val(fts) 693 } 694 695 storeFilters(data); 696 setTimestamp(data); 697 }); 698 }); 699 });