Reputation: 8810
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
Reputation: 3456
Use Selection.data "key" argument function.
(Search «If a key function is not specified…» paragraph)
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
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])
...Ifvalue
is specified, sets the element's bound data to the specified value on all selected elements. Ifvalue
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
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