2

Am trying to create a custom layout for multipanel-graphs in D3. The layout should be 3 columns wide, with the first 2 columns split to two rows. The end result should be 4 equally sized rectangles on the left with a single top-to bottom column on the right (right-side bar). I have tried doing so with the following code:

<!DOCTYPE html>

<script type="text/javascript" src="d3.min.js"></script>

<script type="text/javascript">

    var dataset;    
    var quadheight = "289"; 
    var quadwidth = "450";

    var maincontainer = d3.select("body")
        .append("div")
        .attr("class","main-container")
        .attr("id","main-cont");

    var ltq = maincontainer
        .append("svg")
        .attr("id", "left-top-quadrant")
        .attr("width", quadwidth)
        .attr("height", quadheight)
        .attr("class","quadrant");

    var rtq = maincontainer
        .append("svg")
        .attr("id", "right-top-quadrant")
        .attr("width", quadwidth)
        .attr("height", quadheight)
        .attr("class","quadrant");

    var menu = d3.select("body")
        .append("div")
        .attr("id","menu-div");

    var lbq = maincontainer
        .append("svg")
        .attr("id", "left-bottom-quadrant")
        .attr("width", quadwidth)
        .attr("height", quadheight)
        .attr("class","quadrant");

    var rbq = maincontainer
        .append("svg")
        .attr("id", "right-bottom-quadrant")
        .attr("width", quadwidth)
        .attr("height", quadheight)
        .attr("class","quadrant");

</script>

However I cannot get my head around how to place the side-bar (final column) on the right hand side. Any help would be greatly appreciated. As am inexperienced in front-end development I realize this may not be the best solution of achieving the same result, so any alternative better solutions would also be great, including plain CSS/HTML solutions (without dynamically constructing the layout in D3.js).

Thanks in advance, Tumaini

Tumaini Kilimba
  • 195
  • 2
  • 15

1 Answers1

4

With your current code you are constructing the quadrant elements correctly, but you aren't positioning them anywhere. You could take your code and manually place each quadrant where you wanted; and then add an extra a column and manually calculate where that should go as well... or you could use the power of D3 layouts to help you out:

set up the layout

This is the set up, here I'm forcing the layout to fill the entire body element's width with a fixed height of 600px. A D3 layout — in simple terms — is just a set of pre-built behaviors that act on the data you provide in a specific way, I've chosen to use the partition layout because this approximately matches what you want to achieve.

var body = d3.select('body'),
    w = body[0][0].offsetWidth,
    h = 600,
    x = d3.scale.linear().range([0, w]),
    y = d3.scale.linear().range([0, h]);

The set up above is pretty straight-froward, the only oddity here is that I'm using d3.scale.linear() to give me two functions x and y. You could quite easily calculate the mathematical expressions to use here, but the point of using a library such as D3 is to cut down on your own effort. Basically the resulting function x() when called later on, will convert any passed in value between 0 and 1, to a value along the scale 0 to w (where w is the width of the body at the point when the body[0][0].offsetWidth was evaluated). The same is true for y except the range is between 0 and h. So if I were to execute x(0.5) the resulting value returned would be half of w. These functions will work beyond 0 and 1 too (hence the name scale, as in number scale), so if I were to call y(2) the returned value would be equal to 2*h i.e. 1200.

The rest of this section just appends and styles a few wrapping elements, save for the .value(function(d){ return d.size; }) part. This bit would be used if you were actually using the partition layout to display visual differences in values, you aren't, so it really has no affect on this code. The only reason it is there is because the partition layout expects a value defined for each of its rectangles/items, and won't display correctly without it.

var partition = d3.layout.partition()
      .value(function(d) { return d.size; }),
    visual = body
      .append("div")
        .attr("class", "chart")
        .style("width", w + "px")
        .style("height", h + "px")
      .append("svg:svg")
        .attr("width", w)
        .attr("height", h)
      .append("g")
        .classed("container", true);

why partition, what is it?

Basically I'm slightly misusing a partition layout to achieve what you've requested. A partition layout fills the available space with even width cells in a hierarchical order; the height of those cells is affected by each cells "value" and the number of sibling cells owned by a parent. It is explained more eloquently here:

The partition layout produces adjacency diagrams: a space-filling variant of a node-link tree diagram. Rather than drawing a link between parent and child in the hierarchy, nodes are drawn as solid areas (either arcs or rectangles), and their placement relative to other nodes reveals their position in the hierarchy. The size of the nodes encodes a quantitative dimension that would be difficult to show in a node-link diagram.

https://github.com/mbostock/d3/wiki/Partition-Layout

Going into more depth about the partition layout is beyond the scope of this question, and really the best way to learn is to fire up an example or tutorial yourself — the D3 website has several examples for each of the different available layouts.

All you need to take away from the above is that the partition layout is responsible for displaying children at a smaller vertical size to that of it's parent (and usually in a horizontal order).

------> working direction is left to right by default.

+--------++--------++--------+
|        || Child  |+--------+  <-- even smaller children
|        ||  One   |+--------+
| parent |+--------++--------+
|        || Child  |+--------+
|        ||  Two   |+--------+
+--------++--------++--------+

d3 and data

After the set up above, you will need to populate the display with data in order for it to display anything. In your question's code you are defining each of your items manually, but D3 is designed to generate visual elements directly from a list of objects (so you don't have to worry about creating each item yourself). The following is a simplistic example but many D3 layouts rely on being passed a list of data, and each separate visual element that is generated, is tied directly to one item from that list.

So, as an example, you could have a list of four colours and feed this into a D3 layout. The layout would then automatically create four SVG circles each filled with one of the colours.

Because I've chosen to use the partition layout the actual data I'm using to seed the visualisation is not one column and four quadrants (as your question might assume), but instead one parent that houses two halves, which in turn contain a child each. The data also contains a "size" attribute, this is required for the layout to work correctly — and ties into the .value(function(d) { return d.size; }) already explain above. The value does not need to represent any actual value, as long as they are the same for all cells you wish to size equally.

var root = {
  "name": "full column",
  "class": "full-col",
  "children": [
    {
      "name": "top right quad",
      "class": "quad",
      "size": 1,
      "children": [{
        "name": "top left quad",
        "size": 1,
        "class": "quad"
      }]
    },
    {
      "name": "bottom right quad",
      "class": "quad",
      "size": 1,
      "children": [{
        "name": "top left quad",
        "size": 1,
        "class": "quad"
      }]
    }
  ]
};

The above could be visualised as such:

+--------++--------++--------+
|        || Child  || Child  |
|        ||   A    || of A   |
| parent |+--------++--------+
|        || Child  || Child  |
|        ||   B    || of B   |
+--------++--------++--------+

Once you have your data structure, it needs to be fed into D3. The visual object below is a reference to an outer wrapping SVG element that all our other elements will be added to.

In D3, just like the .attr and .style methods, you can bind .data() to a selected group of SVG elements. If .data() is given an array/list of items, it applies each array item to each found element in the current selection, in a linear order. For example, d3.selectAll('circle').data([{color: 'red'}, {color: 'blue'}]); would set the first object {color: 'red'} as the "data" for the first found circle, and the second object {color: 'blue'} for the second. Calling .data() without passing a parameter will return the bound data for the first item in the current selection.

Hopefully the above makes sense, because that is all that is happening below — albeit our data items are being first processed by partition.nodes() — each one of our data items is being tied to a g element. Passing our items to partition.nodes() just preps our data with features that the partition layout will make use of — for example dx is not in our data-set and is added by partition.nodes — and at the same time it makes the partition layout aware of our data nodes/items.

kx and ky are just short hand variables that are used as part of the cell positioning calculation later on. At the root level, root.dx contains the number of cell divisions along the x function, so kx will contain the width of a column in our layout.

/// must run this first to prep the root object with .dx and .dy
var g = visual.selectAll("g").data(partition.nodes(root)),
    kx = w / root.dx,
    ky = h / 1;

I met a man that wasn't there

Now that the data is in place and has been parsed by D3's partition code you can go about building the actual visual display.

If you've got the idea of data being bound to elements, and the number of data items correlating to the same number of chosen visual elements, then that's great. The one part that may confuse in the above however is visual.selectAll("g") because no g elements exist in our layout yet — so how can you select them? For this part to make sense you need to know about enter and exit.

enter and exit

D3 documentation defines these as special kinds of sub-selection. For any selection, once you have executed .enter() or .exit(), any chained methods from there on in are operating on the elements that have yet to get created (or be removed). Visual items will only get created or destroyed if there is a disparity in the number of data items (bound to the current selection) to that of visual representations of those data items (selected by the current selection). If there are more data items than visual, visual items are created (as per our planned instructions). If there are more visual items than data, then visual items are destroyed. Any already existing visual items are ignored by the enter and exit sub-selections.

To keep things simple, and not end up with very long inline chains, I've broken the following up into parts. First I define some instructions to create a wrapping g element whenever a new item of data "enters" the scene and requires visual exposure. I keep a reference to this "on enter" sub-selection in ge (which also references the yet to be created g element). Next I plan for a rectangle element to be added as a child of the theoretical g element, followed by a sibling of some text. So, whenever a new item of data is added to the layout, three new elements should be created.

/// for each data item, append a g element
var ge = g.enter()
    .append("svg:g")
        .attr("transform", function(d) {
            /// calculate the cells position in our layout
            return "translate(" + (-x(d.y)) + "," + y(d.x) + ")";
        });

/// for each item append a rectangle
ge.append("svg:rect")
    .attr("width", root.dy * kx)
    .attr("height", function(d) { return d.dx * ky; })
    .attr("class", function(d) { return d["class"]; });

/// for each item append some text
ge.append("svg:text")
    .attr("transform", function(d) {
      return "translate(8," + d.dx * ky / 2 + ")";
    })
    .attr("dy", ".35em")
    .text(function(d) { return d.name; });

Because our selection of visual.selectAll("g") does not find any elements, it means that the "on enter plan" is triggered for every data item bound to visual.selectAll("g") (which is our root data items). Instantly creating our five g elements, each with their internal rectangle and text elements.

The reason D3 layouts are designed this way is so that you can easily modify your data-set, re-run your visual creation code, and have your visual representation update correctly and efficiently. For your example, this is probably overkill, but if you wish to understand D3 it is best to get these ideas locked down.

data functions

Another key understanding to fathom in terms of D3 is the way that you can set attributes and styles to fixed values, or you can set them by way of a return value from a function.

In the above code you will see a number of lines similar to .attr("height", function(d){ return d.dx * ky; }). At first his may seem odd, but once you understand that the d parameter is being passed the specific data item that is bound to the currently selected/being-created/being-destroyed element, it should start to make sense.

It is basically a way of generalising your element creation, as we have done, to describe many elements. But then allowing you to be specific about their certain attributes, based on the data you've passed in or that other code has defined.

The height example above is rather complex as it relies on values generated by the partition layout, the only way to truly understand this is to get into the code and console.log values until you understand what the layout is doing. I would also suggest you do this to understand some of the position and size calculations. A simpler example — just to understand the premise — would be .attr("class", function(d) { return d["class"]; }). All this code does is set the class attribute of the currently selected element, to that of the class property defined in our data item.

changing direction

All that remains is to explain this bit of code:

  /// force the layout to work from the right side to left
  visual.attr("transform", "translate("+(w - root.dy * kx)+",0)")

All the above does is shift the visual container off to the right of the SVG canvas, minus the width of the first column. The reason for this is to keep things simple in calculating the position of each cell. When calculating the x position of each cell — which is this line "translate(" + (-x(d.y)) + "," + y(d.x) + ")" — it is simple to cause the layout to run right to left by just making the x values negative. The reason for subtracting one column width can be explained more easily visually:

First if we didn't minus one column width, the first cell would be rendered at 0,0 — which would appear off screen, because the visual element's position 0,0 would be on exactly the far right of the viewport.

+----------------------+
| svg                  +----------------------+
|                      | visual container     |
|       +------+------+|+------+              |
|       |Cell  |Cell  |||Cell  |              |
|       |      |      |||      |              |
|       +------+------+||      |              |
|       |Cell  |Cell  |||      |              |
|       |      |      |||      |              |
|       +------+------+|+------+              |
|                      +----------------------+
+----------------------+

If we minus the width of one column from the right of the svg viewport, the the visual container's 0,0 is at the right location for our first cell which will be positioned at 0,0.

+----------------------+
| svg           +----------------------+
|               | visual container     |
|+------+------+|+------+              |
||Cell  |Cell  |||Cell  |              |
||      |      |||      |              |
|+------+------+||      |              |
||Cell  |Cell  |||      |              |
||      |      |||      |              |
|+------+------+|+------+              |
|               +----------------------+
+----------------------+

There are probably a number of ways of approaching the above, you could probably change the scale.linear() x function, or make the cell translation code more complicated. But for me this was the more straight-forward approach.

putting it all together

Below you should find a working example. Whether or not this is the best way for you to go about what you are trying to achieve, really depends on exactly what you are aiming to do with the layout itself. If you plan to visualise data then D3 is the best route, if you are planning to create a more complex HTML interface, then perhaps a different approach would be best, perhaps just a table layout using CSS, or a flex-box approach.

var body = d3.select('body'),
    w = body[0][0].offsetWidth,
    h = 600,
    x = d3.scale.linear().range([0, w]),
    y = d3.scale.linear().range([0, h]);

var partition = d3.layout.partition()
      .value(function(d) { return d.size; }),
    visual = body
      .append("div")
        .attr("class", "chart")
        .style("width", w + "px")
        .style("height", h + "px")
      .append("svg:svg")
        .attr("width", w)
        .attr("height", h)
      .append("g")
        .classed("container", true);

(function(){
    
  var root = {
    "name": "full column",
    "class": "full-col",
    "children": [
      {
        "name": "top right quad",
        "class": "quad",
        "size": 1,
        "children": [{
          "name": "top left quad",
          "size": 1,
          "class": "quad"
        }]
      },
      {
        "name": "bottom right quad",
        "class": "quad",
        "size": 1,
        "children": [{
          "name": "top left quad",
          "size": 1,
          "class": "quad"
        }]
      }
    ]
  };
  
  /// we must run this first to prep the root object with .dx and .dy
  var g = visual.selectAll("g").data(partition.nodes(root)),
      kx = w / root.dx,
      ky = h / 1;
  
  /// force the layout to work from the right side to left
  visual.attr("transform", "translate("+(w - root.dy * kx)+",0)")
  /// for each data item, append a g element
  var ge = g.enter()
      .append("svg:g")
          .attr("transform", function(d) {
              return "translate(" + (-x(d.y)) + "," + y(d.x) + ")";
          });
  
  /// for each item append a rectangle
  ge.append("svg:rect")
      .attr("width", root.dy * kx)
      .attr("height", function(d) { return d.dx * ky; })
      .attr("class", function(d) { return d["class"]; });

  /// for each item append some text
  ge.append("svg:text")
      .attr("transform", function(d) {
        return "translate(8," + d.dx * ky / 2 + ")";
      })
      .attr("dy", ".35em")
      .text(function(d) { return d.name; });
  
})();
.chart {
  display: block;
  margin: auto;
  margin-top: 30px;
  font-size: 12px;
}

rect {
  stroke: #fff;
  fill: darkred;
  fill-opacity: .8;
}

rect.full-col {
  fill: orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Some may think that it is a lot of effort to go to, just to get a few rectangles arranged. But the beauty of using something like D3 to manage your layouts is that you can quickly alter things, just by changing the data.

http://jsfiddle.net/9gem72vk/

Community
  • 1
  • 1
Pebbl
  • 34,937
  • 6
  • 62
  • 64
  • Hello, Thanks for the well worked detailed response, I see you have gone to a lot of effort to explain and the outcome IS what I was looking for, but am just not understanding the code and what/how it is working :-(. Am relatively new to D3.js as well, so forgive my naivety, I thought I knew the basics but clearly not just yet... Acknowledging the effort you have already put in so far to help, would you be able to elaborate what each step is doing please? – Tumaini Kilimba Mar 12 '15 at 12:14
  • @TumainiKilimba ~ Things are hectic here at the moment, not likely to get much more free time before the weekend I'm afraid. I shall endeavor to explain what will keep within SO's scope in more detail then. In the meantime I suggest you work through a few tutorials [here](https://github.com/mbostock/d3/wiki/Tutorials) as D3 is not an easy perspective to explain, far better to learn from trial and error. Put very simply D3 is designed to tie data objects to visual SVG components, and that is all the above code really has done. Key reading search terms are `d3 enter and exit` and `d3 layouts` – Pebbl Mar 13 '15 at 00:08
  • @TumainiKilimba ~ I've updated my explanation. From what you describe about your client-side knowledge, and the fact you don't seem to require D3 absolutely, the above approach may be over the top. However, I urge you to stick with it and learn. Because once you understand D3, you can do a lot of powerful things in terms of displays. – Pebbl Mar 17 '15 at 12:25
  • Hey wow, thanks for the explanation, let me start going through it :-) Will update you how I get on, thanks again! – Tumaini Kilimba Mar 24 '15 at 12:22