D3: update data with multiple elements in a group - d3.js

I have a combined bar / line chart. For each row in the input file, I create a group that contains multiple elements (lines, rectangles, texts):
var myGroups = svg.selectAll('g').data(myData)
myGroups.enter().append('g')
...
myGroups.append('line')
...
myGroups.append('polygon')
...
myGroups.append('text')
...
I currently just
svg.selectAll('*').remove()
and create everything from scratch every time the data are updated. However, I'd like to have a smooth transition for all elements.
I've gone through this tutorial several times, but I still don't understand how I can do it in my case.

The key is to handle all the selections, not just the enter selection:
var myGroups = svg.selectAll('g').data(myData);
// enter selection
var myGroupsEnter = myGroups.enter().append("g");
myGroupsEnter.append("line");
myGroupsEnter.append("polygon");
// ...
// update selection -- this will also contain the newly appended elements
myGroups.select("line").attr(...);
// ...
// exit selection
myGroups.exit().remove();
There are two things here that warrant further explanation. First, elements in the enter selection that have had new elements appended merge into the update selection. That is, there is no need to set attributes on the elements in the enter selection if the same thing happens in the update selection. This allows you to append new elements and update existing ones without duplicating code.
The second thing becomes important on subsequent calls with updated data. As the elements you're binding the data to are not the ones that are actually drawn, the new data needs to be propagated to them. This is what .select() does. That is, by doing myGroups.select("line"), you're propagating the new data bound to the g elements to their children line elements. So the code to set attributes is the same as for the enter case.
Now you can simply add transitions where desired before setting the new attributes.

Related

What is the difference between .append and .join in D3.js

This may be a silly question, but I just can't figure out when to use .join and when to use .append in D3.
Like in this block, I don't know why to use join rather than append here.
elements.selectAll('circle')
.data(d=>d.values)
//.append('circle')
.join('circle')
.attr("class","dots")
.attr('r',2)
.attr('fill',d=>colorScale(d['track']))
.attr("cx", d=>dateScale(d['edate']))
.attr("cy", d=>valueScale(d['record_time']));
Can anybody help me to understand that?
TL;DR
selection.append by itself merely appends a single child element to every element in the selection it is called on (inheriting its parent's datum). selection.join() is data dependent: it conducts an enter/update/exit cycle so that the number of matching elements in the DOM matches the number of data array items.
The code you have suggests that you want to use .enter().append("circle") as opposed to merely .append("circle"): this completes the enter() portion of the enter/update/exit cycle that is also completed by using .join().
You can use join or individual enter/exit/update selections to achieve the same results, join is just a convenience, as stated in the docs:
This method is a convenient alternative to the explicit general update
pattern, replacing selection.enter, selection.exit, selection.append,
selection.remove, and selection.order. (docs)
Enter/Update/Exit
When you see selectAll() followed by .data() we are selecting all matching elements, for each one that exists, we bind an item from the data array to it. The use of .data() returns what is called the update selection: it contains existing elements (if any) with the newly supplied data bound to those existing items.
However, if the number of selected elements does not match the number of items¹, then .data() creates an enter selection or an exit selection. If we have excess data items, then we have an enter selection with one element for every item we need to add in order to have an equal number of DOM elements and data array items. Conversely, if we have excess DOM elements, then we have an exit selection.
Calling .enter() on the update selection returns the enter selection. This selection contains placeholders ("Conceptually, the enter selection’s placeholders are pointers to the parent element docs"), which we can use .append("tagname") with to add the elements we need.
Conversely, calling .exit() on the update selection returns the exit selection, which often is simply removed with .exit().remove();
This pattern generally looks something like this:
let circle = svg.selectAll("circle")
.data([1,2,3,4])
circle.exit().remove();
circle.enter()
.append("circle")
.attr...
circle.attr(...
First we select the all the circles, let's say there are 2 circles to select.
Second we remove excess elements using selection.exit() : however, since we have four data items and only two matching DOM elements there is nothing to remove, so the selection is empty and nothing is removed.
Third we add elements as required to ensure that the number of matching DOM elements is the same as the number of data array items. As we have four data items and only two matching DOM elements the enter selection contains two placeholders (pointers to the parent). We append circles to them and style them as we want.
Lastly we use the update selection containing the two pre-existing circles and style them as we want based on the new data.
Often we want to style new elements and existing elements the same, so we could use the merge method to combine the enter and update selections:
let circle = svg.selectAll("circle")
.data([1,2,3,4])
circle.exit().remove();
circle.enter()
.append("circle")
.merge(circle)
.attr(...
This simplifies our code a bit as we don't need to duplicate styling for both enter and update separately.
Join
So where does .join() come in? Its for convenience. In its simplest form: .join("elementTagName") .join replaces the above code with:
let circle = svg.selectAll("circle")
.data([1,2,3,4])
.join("circle")
.attr(...
Here the join method removes the exit selection and returns a merged update selection and enter selection (containing new circles), which we can now style as needed. It is essentially a short hand method that allows you to write more concise code, but is functionally the same² as the 2nd code block.
Your Code
In your code, you have a selection of one or more elements (a/some parent element/s). The bound datum contains a child array, which you wish to use to create child elements. In order to do so you provide to .data() that child data array:
parentElements.selectAll("circle")
.data(d=>d.values)
You can follow that up with .join(): this will do the enter/update/exit cycle for every parent element so that they each have the proper amount of circles and returns a selection of all these circles.
You cannot use just .append() because that would append one circle to every parent element, returning a selection of these circles. This is very unlikely to be the desired result.
Instead, as noted at the top of this answer, you can use .enter().append("circle") so that you are using the pattern correctly.
You only need the enter selection if you create the elements once and never update the data, otherwise, you'll need to use enter/update/exit methods to handle excess elements, excess data items, and updated elements.
Ultimately, the difference between join and enter/update/exit is a matter of code preference, style, succinctness, but otherwise, there is nothing that you can't do with one that you can't do with the other.
¹ Assuming the provision of only one parameter to .data() - the 2nd, optional, parameter is a keying function which matches DOM element and data item based on a key. DOM elements without a matching datum are placed in the exit selection, data array items without a matching DOM element are placed in the enter selection, the remainder are placed in the update selection.
² Assuming that .join() is not provided its second or third parameters, which allow more granular control of the enter/exit/update cycle.

How would you display multiple fields on a node in d3?

I am new to d3 and I have objects that are connected to each other. Each of these objects has multiple fields that I want to display. I was wondering how can you do this in d3.
I’m not sure I understand the question, but what I think you’re going for is something like this:
Make your selection and bind the data.
let update = d3.selectAll(".object").data(objects);
Remove old elements (if necessary).
update.exit().remove();
Create an enter selection and append a child for each field.
let enter = update.enter()
.append("g").attr("class", "object");
enter.append("g").attr("class", "field1");
enter.append("g").attr("class", "field2");
Merge your enter selection back into update, then fill in the fields.
update = update.merge(enter);
update.select(".field1").text(d => d.field1);
update.select(".field2").text(d => d.field2);

D3.js: concatenate selections?

Is there a way in D3 to concatenate selections?
Use case: I'd like to add a mouseover event to both the update and enter selections of a particular selection.
I can do this as follows:
var s = d3.selectAll('.yellow').data(myData);
s.on('mouseover'...
s.enter().append('path').attr('class','yellow').on('mouseover'...
But I'd prefer to do it with one line of code.
In this particular case you don't need to concatenate -- the enter selection merges into the update selection after it's been called, so all you need to do is handle .enter() before the update selection.
In general, you can't really concatenate selections as such, but in practice this isn't really necessary. You can either modify the selection condition to select all the elements you need, or, if this is not possible, use .call() to run a function on all selected elements. This way you don't need to repeat the code to set attributes etc.

Infragistics UltraGrid with Expandable Rows

I'm showing to my users, a list of items within a UltraGrid, that come from database.
Now, i'm with one need and I don't find anything useful or any documentation that is clear nor none tutorial is available where I can learn.
I need extend the functionality of this grid, to, set a (+) Expand function, that, when the user press that button (+), the row expands and shows the items that are in the History.
It is anyone in the world that can help me to solve this issue or indicate appropriate docs where I can Learn?
Very thanks.
The trick is simple. You just need to bind the grid to a DataSet that contains two (or more) tables and the correct DataRelation object that glues together the tables.
You need also to be sure that the property grid.DisplayLayout.ViewStyle is set to MultiBand (it is the default so it should be already set).
So, for example, in this pseudocode I load two tables and add them to a dataset, then I define a supposed relation between the columns involved and at last I bind the dataset to the grid.
DataSet ds = new DataSet();
DataTable dtItems = YourLoadDataTableMethodHere("Items");
ds.Tables.Add(dtItems);
DataTable dtHistory = YourLoadDataTableMethodHere("History");
ds.Tables.Add(dtHistory);
DataRelation rel = new DataRelation("Items_History_Relation",
dtItems.Columns["IDItem"],
dtHistory.Columns["IDItem"]);
ds.Relations.Add(rel);
grid.DataSource = ds;
This will automatically force the UltraGrid to create two Bands (grid.DisplayLayout.Bands[]), in the first Band (Band[0]) you will find the rows of the Items datatable, each row has its [+] button to click and expand the second Band (Band[1]) where you will see the History rows related to the row in the first Band

Adding new row in correct position to a user sorted wingrid with bindingsource

I have a Infragistics UltraGrid using a bindingSource.
If I add a new object to my binding list, it adds a row to the bottom of the grid which is fine if there's no user defined sort.
Question is if the user clicks on a column header to sort the grid, is there a way for new rows to appear in the proper sorted order instead of always on the bottom?
Re-sorting all rows on every insert is too expensive.
Seems kind of ghetto. Infragistics support also indicated that the RefreshSortPosition() method is the only choice.
// Add to binding list which will trigger a row to be added to the bound ultragrid.
this.bindingList.Add(new Person("Smith", "John"));
// Get length since we know this will always be added to the end
int length = this.ultraGrid.Rows.All.Length;
// Get it to sort
this.ultraGrid.Rows[length - 1].RefreshSortPosition();
To be a bit more efficient, you can always be clever by disable redrawing, etc and then call refresh on a bunch of rows after a batch of orders, etc...
Hope that helps. I had very little luck Googling this problem.

Resources