D3: Transition Treemap and retain original group sizing

Viewed 209

I have a simple D3 treemap.

When a user clicks the treemap, I'd like to show a subset of the data. My goal is to have each of the cells in the update resize, so that all the blue cells fill the original blue cell area, the red cells fill the original red cell area, and so forth.

I thought d3.treemapResquarify would magically take care of this, but I'm clearly missing something!

var data;

var width = 600,
    height = 500;

var svg = d3.select('svg')
    .attr('width', width)
    .attr('height', height)

var fader = color => d3.interpolateRgb(color, '#fff')(0.2),
    color = d3.scaleOrdinal(d3.schemeCategory20.map(fader)),
    format = d3.format(',d');

var treemap = d3.treemap()
    .tile(d3.treemapResquarify)
    .size([width, height])
    .round(true)
    .paddingInner(1);

var url = 'https://raw.githubusercontent.com/d3/d3-hierarchy/master/test/data/flare.json';
d3.json(url, function(error, json) {
  data = json;
  var root = getTreeRoot(data);
  treemap(root);
  draw(root);
});

function draw(root) {
  var leaves = root.leaves()

  var cell = svg.selectAll('g').data(leaves, d => d.id)
  
  cell.exit().remove()
  
  var g = cell.enter()
    .append('g')
      .attr('transform', d => 'translate(' + d.x0 + ',' + d.y0 + ')')
  
  g.append('rect')
      .attr('width', d => d.x1 - d.x0)
      .attr('height', d => d.y1 - d.y0)
      .attr('fill', d => color(d.parent.data.id));
      
  g.append('text')
      .attr('clip-path', d => 'url(#clip-' + d.data.id + ')')
    .selectAll('tspan')
      .data(d => d.data.name.split(/(?=[A-Z][^A-Z])/g))
    .enter().append('tspan')
      .attr('x', 4)
      .attr('y', (d, i) => 13 + i * 10)
      .text(d => d)
}

function getTreeRoot(data) {
  return d3.hierarchy(data)
    .eachBefore(d => {d.data.id = (d.parent ? d.parent.data.id + '.' : '') + d.data.name })
    .sum(d => d.value)
    .sort((a, b) => b.height - a.height || b.value - a.value)
}

/**
* Transitions
**/

window.addEventListener('click', () => {
  var newData = JSON.parse(JSON.stringify(data));
  mutateChildren(newData);
  var root = getTreeRoot(newData);  
  treemap(root);
  draw(root);
})

function mutateChildren(data) {
  data.children = data.children.filter(c => {
    if (c.children && c.children.length) mutateChildren(c);
    return Math.random() > 0.5;
  });
}
text { font-size: 9px; font-family: 'courier' }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg>

When updating the display, I suppose I could find the parent nodes of each group, find their sizes and offsets, then build new treemaps for each of those groups to make the children fill that parent container. But this can't possibly be the right way forward...

Does anyone know how I can resize the cells so as to fill their parent group? Any pointers others can offer on this question would be super appreciated!

1 Answers

I ended up doing almost what you said: I place down an area for every group of data, and then fill the cells in the group on every iteration. It does not work for every node, because sometimes, the levels are not equal, and then it's difficult to do. Maybe you can find an improvement that contains that. This should at least get you started.

var data;

var width = 800,
  height = 400;

var svg = d3.select('svg')
  .attr('width', width)
  .attr('height', height)

var fader = color => d3.interpolateRgb(color, '#fff')(0.2),
  color = d3.scaleOrdinal(d3.schemeCategory20.map(fader)),
  format = d3.format(',d');

var treemap = d3.treemap()
  .size([width, height])
  .round(true)
  .paddingInner(1);

var url = 'https://raw.githubusercontent.com/d3/d3-hierarchy/master/test/data/flare.json';
d3.json(url, function(error, json) {
  data = json;
  var root = getTreeRoot(data);
  rollupLeaves(root)
  treemap(root);
  initialDraw(root);
});

function initialDraw(root) {
  var groupData = root.leaves();

  var group = svg.selectAll('g')
    .data(groupData)
    .enter()
    .append('g')
    .classed('group', true)
    .each(function(parent) {
      parent.children = parent._children;
      
      let leaves, fill
      if(parent.children === undefined) {
        leaves = [parent];
        fill = color(parent.parent.data.id);
      } else {
        d3.select(this).attr('transform', d => 'translate(' + d.x0 + ',' + d.y0 + ')');
        resetHeightDepth(parent, 0);
        const width = parent.x1 - parent.x0;
        const height = parent.y1 - parent.y0;
        leaves = treemap.size([width, height])(parent).leaves();
        fill = color(parent.data.id);
      }

      var cell = d3.select(this)
        .selectAll('g')
        .data(leaves)
        .enter()
        .append('g')
        .attr('transform', d => 'translate(' + d.x0 + ',' + d.y0 + ')')

      cell.append('rect')
        .attr('width', d => d.x1 - d.x0)
        .attr('height', d => d.y1 - d.y0)
        .attr('fill', d => fill);

      cell.append('text')
        .attr('clip-path', d => 'url(#clip-' + d.data.id + ')')
        .selectAll('tspan')
        .data(d => {
          return d.data.name.split(/(?=[A-Z][^A-Z])/g)
        })
        .enter().append('tspan')
        .attr('x', 4)
        .attr('y', (d, i) => 13 + i * 10)
        .text(d => d);
    });
}

function redraw() {
  d3.selectAll('.group')
    .each(function(parent) {
      parent.children = parent._children;

      let leaves;
      if(parent.children === undefined) {
        leaves = [parent];
      } else {
        hideSomeChildren(parent);
        const width = parent.x1 - parent.x0;
        const height = parent.y1 - parent.y0;
        leaves = treemap.size([width, height])(parent).leaves();
      }

      const cell = d3.select(this)
        .selectAll('g')
        .data(leaves, d => d.data.id)
        .attr('transform', d => 'translate(' + d.x0 + ',' + d.y0 + ')');

      cell
        .select('rect')
        .attr('width', d => d.x1 - d.x0)
        .attr('height', d => d.y1 - d.y0);

      cell
        .select('text')
        .attr('opacity', d => d.value > 0 ? 1 : 0);
    });
}

function getTreeRoot(data) {
  return d3.hierarchy(data)
    .eachBefore(d => {
      d.data.id = (d.parent ? d.parent.data.id + '.' : '') + d.data.name;
    })
    .sum(d => d.value)
    .sort((a, b) => b.height - a.height || b.value - a.value)
}

/**
 * Transitions
 **/

window.addEventListener('click', () => {
  redraw();
})

function hideSomeChildren(node) {
  if (!node.children) return;
  node.children.forEach(c => {
    if (c._value === undefined) c._value = c.value;
    c.value = Math.random() > 0.5 ? c._value : 0;
  });

  if (node.children.every(c => c.value === 0))
    node.children[0].value = node.children[0]._value;

  node.value = d3.sum(node.children, c => c.value);
}

function rollupLeaves(node) {
  if (!node.children) {
    return;
  }

  if (
    node.children.length &&
    node.children.every(n => !n.children)
  ) {
    // All children are leaf nodes
    node._children = node.children;
    node.children = undefined;
    node.value = d3.sum(node._children, n => n.value);
  } else {
    node.children.forEach(n => {
      rollupLeaves(n);
    });
  }
  return node;
}

function resetHeightDepth(node, depth) {
  var height = 0;
  node.depth = depth;

  if (node.children) {
    height = d3.max(node.children.map(c => resetHeightDepth(c, depth + 1))) + 1;
  }

  return height;
}
text {
  font-size: 9px;
  font-family: 'courier'
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
<svg></svg>

Related