Xavier_Ex
Xavier_Ex

Reputation: 8810

D3 - How to bind the same data to multiple object?

I am trying to draw multiple shapes using the same data in a selection, so let's say I have:

myData = [
    { 
        shape: 'rect',
        attr: { width: 100, height: 100, x: 100, y:100 }
    } , {
        shape: 'circle',
        attr: {cx, cy, etc...}
    }
]

node.selectAll('*').data([myData]);

myData.obj.forEach(function(obj) {
    // Append different shapes based on data
    var shape = node.enter().append(obj.shape);
    Object.keys(obj.attrs).forEach(function(attr) {
        // Bind attrs to shapes based on data
        shape.attr(attr, obj.attrs[attr]);
    });
});

Here node is a 'g' element, myData is a single data object. My goal is to modify the child shapes inside the g based on myData, so later if I bind another myData and call this function again, they can be updated. But I believe somehow myData is only bound to the first appended shape. Is there a way to easily bind the same data to multiple shapes?

Upvotes: 4

Views: 4237

Answers (3)

Stphane
Stphane

Reputation: 3456

TLDR;

Use Selection.data "key" argument function.

A key function may be specified to control which datum is assigned to which element «This function is evaluated for each selected element, in order, […] The key function is then also evaluated for each new datum in data, […] the returned string is the datum’s key.

(Search «If a key function is not specified…» paragraph)

More details …

D3 binds data to elements. By default is uses the «join-by-index» method (first found element maps to datum at index 0 and so forth…). If join-by-index is not enough to appropriately identify elements, Selection.data "key" should be used in order for D3 to appropriately synchronise input dataset during rendering phases: "update" (result of data() method call), "creation" enter selection, "removal" (exit selection).

Let's say we need to draw some legend, given the following dataset …

const dataSet = [
  [ 15, "#440154" ]
  [ 238.58, "#414487" ]
  // ...
]

we first decide to draw the coloured square using <rect> element.

const updateSelection = d3.select('rect').data(dataSet);
// join-by-index strategy is enough here so there is no need to supply the key fn arg
// Key function could be `d => d[0]` (given data source cannot produce two entries
// with value at index 0 being the same, uniqness is garanteed)

updateSelection.enter()
    .append('rect')
    .attr('width', 15).attr('height', 15)
    .merge(updateSelection)
    .style('fill', d => d[1])
;

// Dispose outdated elements
updateSelection.exit().remove();

We now need a <text> element to be drawn for each given datum to visually expose numeric values. Keeping join-by-index (default) strategy would cause D3 to first draw both rect and text elements but since there can only be a single element bound to a specific datum key, only the first found element will be considered upon update phase and any other element having same datum key will end up in the exit selection and removed from the DOM as soon as the Selection remove method is called.

… If multiple elements have the same key, the duplicate elements are put into the exit selection …

A solution is to concatenate the numeric value with the element nodeName.

// Notice how the selector has changed
const updateSelection = d3.select('rect, text').data(dataSet, function(d) { 
      // Concatenate the numeric value with the element nodeName
      return `${d[0]}:${this.nodeName}`;
  })
  , enterSelection = updateSelection.enter()
;

// Add rect element
enterSelection
    .append('rect').attr('width', 15).attr('height', 15)
    .merge(updateSelection)
    .style('fill', d => d[1])
;

// Add text label
enterSelection
    .append('text')
    .merge(updateSelection)
    .text(d => d[0])
;

// Dispose outdated elements
updateSelection.exit().remove();

Upvotes: 0

Daniel Korenblum
Daniel Korenblum

Reputation: 108

Perhaps, what you want here is the .datum() function in d3. One of it's specific use-cases is to bind the same datum to multiple DOM elments (i.e. bind one datum to an entire d3 selection).

For example, d3.selectAll('div').datum({foo: 'bar'}) would bind the same object {foo: 'bar'} to all of the <div>...</div> elements currently existing on the document.

Quoting directly from https://github.com/mbostock/d3/wiki/Selections#datum

selection.datum([value])...If value is specified, sets the element's bound data to the specified value on all selected elements. If value is a constant, all elements are given the same data [sic]

(Somewhat ironically, he refers to the constant datum as "data" in the explanation of the .datum() function!)

However, this is a literal answer to your question; there may be a more design-oriented answer to your question which might explain the "conventional" d3-way of handling your overall objective... In which case, you should probably consult some tutorials like

http://mbostock.github.io/d3/tutorial/circle.html

Upvotes: 2

Adam Pearce
Adam Pearce

Reputation: 9293

I would create a g element for entry in myData:

groups = d3.select('body').append('svg')
           .selectAll('g').data(myData).enter()
           .append('g');

and append shapes to those group elements individually:

groups.append('rect')
groups.append('circle')

Upvotes: 2

Related Questions