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  });