In referencing this chord diagram example: https://bl.ocks.org/mbostock/4062006
The following block sets up the ribbon:
var ribbon = d3.ribbon()
.radius(innerRadius);
But then later in the code ribbon is then used in a way I can't understand:
g.append("g")
.attr("class", "ribbons")
.selectAll("path")
.data(function(chords) { return chords; })
.enter().append("path")
.attr("d", ribbon)
.style("fill", function(d) { return color(d.target.index); })
.style("stroke", function(d) { return d3.rgb(color(d.target.index)).darker(); });
How is it that .attr("d", ribbon) knows how to pass in the chord data to ribbon? Is this just part of D3 magic that I have to just remember?
In your example, d3.chord() computes the layout of the data. It's important to note that, in d3, "layout" has a different meaning of layout in data visualisation: here, layout has to do with preparing the data to create a given chart (not the visual "layout" of the chart).
Basically, d3.chord() takes your data matrix and creates another data matrix with a bunch of starting angles and ending angles, associating sources to targets.
After d3.chord() modifies the data, they are passed to d3.ribbon(). d3.ribbon is the real path generator, creating the actual path that will be painted in the SVG. How does it knows the data? You have previously bounded the modified data to your group:
.datum(chord(matrix));
That's the data passed to d3.ribbon().
According to the API, d3.ribbon()...
...generates a ribbon for the given arguments. The arguments are arbitrary; they are simply propagated to the ribbon generator’s accessor functions along with the this object [...] with the default settings, a chord object expected.
Related
I'm building a "burnup" d3 line chart attempting to forecast trends based on scope and historic data.
A dropdown box selects the data to be displayed, ideally transitioning between lines, but I'm having troubles clearing the previous data displayed, and instead the new lines are written over the existing lines.
Link to the jsfiddle: https://jsfiddle.net/dgf1vts8/
Currently doing it this way (line 329):
svg.append("path")
.datum(selectedData)
.attr("class", "line")
.attr("d", line);
other approach I've tried and did not work (line 318)
var lines = svg.selectAll(".line").data(selectedData).attr("class","line");
lines.transition().duration(500).attr("d",line);
lines.enter()
.append("path")
.attr("class","line")
.attr("d",line);
lines.exit()
.remove();
Any guidance with this would be much appreciated
Since you only append new object in your update function, every time that function is invoked a new line is added to the chart.
The easiest workaround would be to just remove all path objects before you add the new ones.
svg.selectAll("path").remove();
However, the enter-update-exit logic for this would be
var lines = svg.selectAll(".line").data(selectedGroup);
lines.enter() // enter any new data
.append("path")
.attr("class", "line")
.merge(lines)
.datum(selectedData)
.attr("d", line);
lines.exit() // exit
.remove();
The logic for lines is different as for e. g. points or bars, where you need to create an object for every data point of the time series. For lines you only have to create one path object. This is why the data binding to the selection
svg.selectAll(".line")
should be the label of the line. If you work with multiple lines, it is often best to nest the array.
If you start a new project you should probably use the current version d3v7 and not d3v3, since there have been major changes which break compatibility. I reduced your fiddle and changed all the parts to make it work with d3v7: https://jsfiddle.net/esd4kofr/1/
I'm trying to make a chart of sleeping sessions that looks something like this:
https://i.stack.imgur.com/Wsnha.png
I currently have only been able to get it to draw one rectangle per data point. I'm wondering what the syntax would be to have it draw multiple rectangles associated with a single row/data point since each data point contains an array of sleeping sessions from that day.
Here is what my code currently looks like:
var start;
var end;
var rectGrp = svg
.append("g")
.attr('transform', 'translate(' +padding.left+','+padding.top+')')
.selectAll("rect")
.data(sleepArr)
.enter()
//I want to create one of these rectangles for all the sleeping sessions in that day
.append("rect")
.attr("x", (d) => {
start = d.sessions[0].startTime;
end = d.sessions[0].endTime;
return xScale(start);
})
.attr("width", (d) => {
start = d.sessions[0].startTime;
end = d.sessions[0].endTime;
var width = end-start;
return xScale(width);
})
.attr("y", (d) => yScale(d.date))
.attr("height", yScale.bandwidth())
.attr("fill", (d) => colors[getDayOfWeek(d.date)])
.append("title")
.text((d) => d.date);
I realize that I probably don't have a good understanding of how to use D3 yet and any help would be greatly appreciated.
With the enter/update/exit cycle one item in the data array should be bound to one element in the DOM. The enter selection is used to ensure we create one element for every item for which there is already not one corresponding element in the DOM. In your case the number of rectangles you want to draw is not the same as the number of people in your data array. The approach you have is great for a flat data structure, but we need to make nested selectAll statements in order to properly represent your data.
First, each item in sleepArr represents one person, so lets create one parent g for every person:
var sleepers = svg
.append("g")
.attr('transform', 'translate(' +padding.left+','+padding.top+')')
.selectAll("rect")
.data(sleepArr)
.enter()
.append("g")
.attr("transform", (d,i)=>...
We probably want to do the vertical positioning here as all children of each parent g elements are the same.
Second, we want to create child rectangles representing sleep sessions. We want one rectangle per session. The data array for these rectangles is the sessions property of the datum bound to each parent g, so we pass that to the data method:
sleepers.selectAll("rect")
.data(function(d) return d.sessions; }))
.enter()
.append("rect")
.attr("x", ...
...
For each parent g, representing a single person, we select all child rectangles of each g, bind data, and enter the required rectangles in the appropriate g.
This way we keep a one to one relationship between data and elements representing them: one g per person, one rect per session. The items in sleepArr are bound to the parent g elements, and the items in the child data array are bound to the rects.
I am experimenting with a stacked bar chart in d3js and ran into enter exit selection difficulties. I used the d3.stack to get two arrays organized by keys, then I tried to follow the general update pattern. The problem I'm having now is the data is not getting updated when I click a different state in the dropdown menu. Here's the problem code and a link to the full project: http://plnkr.co/edit/8N8b2yUYRF9zqRkjkIiO?p=preview
var series = g.append("g")
var seriesready =
series.selectAll("g")
.data(stack(data))
.enter().append("g")
.attr("fill",function(d){console.log(d); return z(d.key)}) //not logging
when I update the bar chart
var rectangles =
seriesready.selectAll("rect")
.data(function(d){return d})
rectangles.exit().remove()
rectangles.enter().append("rect")
.attr("width", x.bandwidth())
.transition()
.duration(1500)
.attr("transform", function(d) {console.log(d); return "translate(" + x(d.data.Date) + ",0)"; })
.attr("height", function(d) {
return height - y(d[1]-d[0]);
})
.attr("y", function(d) {
return y(d[1]-d[0]);
});
I also think I'm getting confused as to what selections should be removed or added. Would really appreciate any pointers. Data viz is fun to work with, but I still haven't fully grasped data binding yet.
I have not made the switch to version 4 yet, but the data binding methodology is the same i think.
You need to define a key function as the second parameter to the .data() function.
A key function may be specified to control which datum is assigned to
which element, replacing the default join-by-index.
https://github.com/d3/d3-selection/blob/master/README.md#selection_data
Your updated code
http://plnkr.co/edit/wwdjJEflZtyACr6w9LiS?p=preview
The changed code:
var seriesUpdate = series.selectAll("g")
.data(stack(data),d=>d)
var seriesready = seriesUpdate.enter().append("g")
.attr("fill",function(d){return z(d.key)})
seriesUpdate.exit().remove()
When binding data to elements, D3 calculates what data is new/existing/removed in relation to the selection. By default it does this by data index - the size of the input array. Since the computed stack data for michigan and ohio both return 2 sets of data (injured and killed), D3 views this as "same" data, thus it's an update.
If you define a key function, D3 recognizes the computed stack data for michigan and ohio as being "different" data, thus it's an enter.
With a key function, when you select Ohio first, the enter selection is size 2 with Ohio. If you then select Michigan, the enter selection is size 2 with Michigan, and the exit selection is size 2 with Ohio.
I am currently using the d3js Bilevel partition because our data is too large to show all at once. I first had the sunburst partition with the last layers hidden with css, but then the chart wouldn't have the same size every time, which annoyed me.
A user can adjust values with a range slider, this should update the graph in real-time. This worked using this in the sunburst partition
path.data(partition.nodes)
.transition()
.duration(1000)
.attrTween("d", arcTweenData);
Is it also possible to do something similar in the bilevel partition?
So basicly this sunburst partition but with the bilevel partition or only 2 rows showing each time(like the bilevel partition).
FOUND THE SOLUTION
I've finally found the solution, the bilevel partition uses the sum by default so doesn't update with the changes made to the value. Specifying the value again before updating did it for me.
path = path.data(partition.value(function (d) { return d.value}).nodes(current).slice(1));
path.transition().duration(750)
.attrTween("d", function (d) { return arcTween.call(this, updateArc(d)); });
Bilevel is different from sunburst in the sense that although it reads and stores the data/nodes into a partition, it only displays two layers by using the "children" element. Sunburst displays them all (default children).
In your case, what you really want is to update/refresh the path data using transitions. This can be easily be done by calling the transition on the path whenever you want to update:
path.transition()
.style("fill-opacity", 1)
.attrTween("d", function(d) { return arcTween.call(this, updateArc(d)); });
You could make it update on an interval or place it within a function that is called whenever is button is clicked. If you're not updating the d values, but updating an external data variable instead; you could simply pass that new data value using a global variable.
function updateVisual(d,newData){
path.transition()
.style("fill-opacity", 1)
.attrTween("d", function(d) { return arcTween.call(this, updateArc(newData));});
} // If d is updated, remove newData and use updateArc(d)
Note: This is assuming you're using Bilevel Partition
I'm learning D3.js and trying to get my head around data keys used with streamgraphs. I would like to adapt the official streamgraph example:
...so that each path has an explicit data key, and so that the mouseover logs the data key.
The official example adds paths as follows:
var area = d3.svg.area()
.x(function(d) { console.log('x', d.data); return d.x * w / mx; })
.y0(function(d) { return h - d.y0 * h / my; })
.y1(function(d) { return h - (d.y + d.y0) * h / my; });
vis.selectAll("path")
.data(data0)
.enter().append("path")
.style("fill", function() { return color(Math.random()); })
.attr("d", area);
I tried adapting the code as follows, but I'm not sure how to change the structure of data0 (currently an array of arrays) to achieve what I want:
vis.selectAll("path")
.data(data0, function(d) { return d.name }) // Add key function
.enter().append("path")
.style("fill", function() { return color(Math.random()); })
.attr("d", area)
.on("mouseover", function (d,i) {
console.log("mouseover", d.name); // Log name property on mouseover
});
As it stands, without my having made any changes to the structure of data0, it unsurprisingly does not work. How can I add a name property to data0 without also messing up the area and .data() functions?
UPDATE: To be a bit clearer: the D3 docs say that the area function is expecting a two-dimensional array. So if I change data0 from a two-dimensional array, to an array of objects, each with a name key and a data key, how can I also change what I pass to area?
The data in the example doesn't have a "name" property, so you would need to add that to the data to use it. The data keys you refer to are used when merging/updating data, i.e. you have drawn some paths already and then update (some of them). The .data() function will try to figure out what data is updated and what data is new. If that doesn't work for you, you can use the data key to help it, i.e. in your case tell it that things with the same name are the same data.
If what you mean by data keys are "data legends", then you might want to take a look at the following examples where I've completely separated the placement of magnitudes, legend bullets and legend text in different areas of the charts.
Multiple D3 Pie Charts Mixed In With HTML Layout Constructs
Multiple D3 Horizontal Bar Charts Mixed In With HTML Layout Constructs
In each of the examples, you'll clearly see how the data is labeled, structured, passed in, and used.
I also tied them together through mouseover and mouseout events so that mousing over or out of any element causes all elements in a set to change color simultaneously.
I hope this helps.
Frank