Using the D3 Sankey plugin, I'm updating a Sankey diagram with new values (on changing the data, passing new values for the nodes and links -- keeping all of them consistent). Is there functionality like d3.treemap's sticky to maintain node and link orders on the page? If not, is there an approach to building this?
var sankey = d3.sankey()
.nodeWidth(15)
.nodePadding(10)
.size([width, height]);
.sticky(true)
I'm following the pattern here: http://bost.ocks.org/mike/sankey/
No, there isn't. If you want to dive into the layout, here's where you want to look:
function computeNodeDepths(iterations) {
var nodesByBreadth = d3.nest()
.key(function(d) { return d.x; })
.sortKeys(d3.ascending)
.entries(nodes)
.map(function(d) { return d.values; });
initializeNodeDepth();
...
Notice that sortKeys points at d3.ascending. You'd want this to point to some kind of hard-wired value, which you'd need to compute either in the first iteration or in your data preparation. It will still get adjusted when the collision detection function is run, so you might see your nodes pushed out of position but this will give you the best chance to maintain some control.
Related
I'm following Mike Bostock's tutorial here to create a bubble chart... except that I'm using my own dataset and I'm using d3 v4. I'm quite new to d3 and I understand a lot has changed in v4 from v3. I'm having trouble converting the sample code to v4.
For instance, I've converted this code in d3 v3:
var bubble = d3.layout.pack()
.sort(null)
.size([diameter, diameter])
.padding(1.5);
to:
var bubble = d3.pack(dataset)
.size([diameter, diameter])
.padding(1.5);
Is the above correct? I'm not sure since I'm not having any errors till this point.
But I get an error in the following piece of code:
var node = svg.selectAll(".node")
.data(
bubble.nodes(root)
.filter(function(d) {
return !d.children;
})
)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
with a bubble.nodes is not a function. What is the equivalent in d3 v4?
Fiddle: https://jsfiddle.net/r24e8xd7
Here is your updated fiddle: https://jsfiddle.net/r24e8xd7/9/.
Root node should be constructed using d3.hierarchy:
var nodes = d3.hierarchy(dataset)
.sum(function(d) { return d.responseCount; });
Then pack layout can be called:
var node = svg.selectAll(".node")
.data(bubble(nodes).descendants())
Comparing the different docs, v3
# pack(root)
# pack.nodes(root)
Runs the pack layout, returning the array of nodes associated with the
specified root node. The cluster layout is part of D3's family of
hierarchical layouts. These layouts follow the same basic structure:
the input argument to the layout is the root node of the hierarchy,
and the output return value is an array representing the computed
positions of all nodes. Several attributes are populated on each node:
parent - the parent node, or null for the root.
children the array of child nodes, or null for leaf nodes.
value - the node value, as returned by the value accessor.
depth - the depth of the node, starting at 0 for the root.
x - the computed x-coordinate of the node position.
y - the computed y-coordinate of the node position.
r - the computed node radius.
to the newer v4
# pack(root) <>
Lays out the specified root hierarchy, assigning the following
properties on root and its descendants:
node.x - the x-coordinate of the circle’s center
node.y - the y-coordinate of the circle’s center
node.r - the radius of the circle
You must call root.sum before passing the hierarchy to the pack
layout. You probably also want to call root.sort to order the
hierarchy before computing the layout.
it looks like pack() is what you are looking for, but it looks like you might need a change or two before you do.
update
Quick look into different things and there are a few things going on that its not just a simple fix.
your data is entirely different to the example and is flat, which
effects the diagram.
why not use v3? Most of the examples out there are in v3 and like you
said you are new to d3. why make things difficult.
Finally start small. I would suggest trying to find a small bubble
chart first and then make your way up, or substitute your data into
the example code and get that working and then incrementally change
it instead of trying to change multiple things at once.
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've been attempting to learn better visualization with node.js and the mapbox library.
Using this example here: Running Map Example
I'd like to add a graph of speed, and allow a user to click on a node, and see data about that position in a little popup - For today, I just want to get speed working.
It seems to be a recursive algorithm, so I need to implement variables to store the previous position and time, but I've ran into three problems:
I don't know how to use this date format: "2015-01-19T21:24:20Z" or Chroniton's parsing of it to generate a subtractable number to get the difference in time.
I don't know how to get the distance between two points using the code given, I could simply do sqrt((.x(point1) - .x(point2)) + (.y(point1) + .y(point2)), but I'm not sure how coordinates are stored or parsed in this example.
I don't know where to calculate the speed. It seems like the coordinates are only defined after the graphs are displayed, since the coordinates aren't used in the graphics. I am probably wrong, but I need some direction.
Here is what I have now:
Using the elevation display as my template, I think I have made it able to display the line by adding in the following three snippets:
Setting the scale:
var speed = d3.scale.linear()
.range([height, 0])
.domain([0, d3.max(dataRet, function(d) {
return d[1][2];
})]);
Adding in the line, with data:
var SpeedLine = d3.svg.line()
.x(function (d) { return x(d[0]); })
.y(function (d) { return speed(d[2])})
Displaying the line:
svg.append('path')
.datum(dataRet)
.attr('class', 'speed-line')
.attr('d', speedLine);
I know I have to add in a speed function similar to this psudocode:
var dt = chroniton.domain(Time1, Time2)
var speed[i] = LongLat(previousPoint).distanceto(currentPoint)/dt
And on the popup box:
dt.format(something to do with time formatting)
Note 1:, I changed the name of the function datePlaceHeart to dataRet since I'll be adding new things to do it, and datePlaceHeartSpeedStuffAndThings was getting a bit long ;)
Note 2: I haven't been able to start the pop-up because I haven't figured out how to calculate speed using the given data, and well, it seems kinda silly to do the easy one first. (With my luck, its actually not easy)
Please help? Here is my edited code in full (Edited index.js):
Code
I'm trying out a way to get paths to display next to each other, such that they'll push each other around (factoring in widths and neighbouring points) and not overlap.
This is my fiddle, mostly pieced together from examples
https://jsfiddle.net/crimsonbinome22/k2xqn24x/
var LineGroup = svg.append("g")
.attr("class","line");
var line = d3.svg.line()
.interpolate("linear")
.x(function(d) { return (d.x); })
.y(function(d) { return (d.y); })
;
LineGroup.selectAll(".line")
.data(series)
.enter().append("path")
.attr("class", "line")
.attr("d", function(d){ return line(d.p); })
.attr("stroke", function(d){ return d.c; })
.attr("stroke-width", function(d){ return d.w; })
.attr("fill", "none");
And this is what I'm hoping to achieve in this image here, basically:
For all lines landing on the same point, push them left or right of that point so together they center around it.
Factor in line width so they don't overlap, or leave whitespace between.
Be able to handle paths with different numbers of points (max in example is 3 but I want to deal with up to 10)
Note though points that overlap will always have the same index (they won't loop around, but just go outwards like a tree)
Be able to handle different numbers of lines landing on the same point.
Some issues I'm having:
I'm new to d3 and I find functions a bit baffling. Not sure how to even start to apply logic that will move the lines around.
My data structure has some redundant info in it, such as r for the rank (to decide whether to push left or right) and w for the width both of which will always be the same for a particular line.
I have a lot of data so the data structure used here won't work with the csv data I have. Can maybe skip this one for now and I'll open up a new question for that one later.
I've had a search around but can't find any examples of how to do this. In a way it's almost like a chord diagram but a little different, and I can't find much relevant code to reuse. Any help on how to achieve this (either with the approach I've started, or something totally different if I've missed it) would be appreciated.
I would go with the following steps:
compute an array of node objects, i.e. one object for each point visited by a line
compute the tree on this node (that is, for every node, add links to its parent and children)
make sure that children of any node are ordered according to the angle they make with this node
at this point, each line now only depends on its final node
for each node compute an ordered list of lines going through
visit all nodes bottom-up (i.e. starting from the leaves)
the "go-through" list is the concatenation of the lists of the children + all lines that end at the current node
for each node, compute an array of offsets (by summing the successive width
of the lines going through)
finally, for every line and every node in the line, check the array of offsets to know how much the line must be shifted
Edit: running example
https://jsfiddle.net/toh7d9tq/1/
I have used a slightly different approach for the last two steps (computing the offset): I actually create a new p array for each series with a list of pairs {node, offset}. This way it is much easier to access all relevant data in the drawing function.
I needed to add an artificial root to have a nice starting line (and to make it easier for recursion and angles and everything), you can skip it in the drawing phase if you want.
function key(p) {
return p.time+"_"+p.value
}
// a node has fields:
// - time/value (coordinates)
// - series (set of series going through)
// - parent/children (tree structure)
// - direction: angle of the arc coming from the parent
//artificial root
var root={time:200, value:height, series:[], direction:-Math.PI/2};
//set of nodes
var nodes = d3.map([root], key);
//create nodes, link each series to the corresponding leaf
series.forEach(function(s){
s.pWithOffset=[]; //this will be filled later on
var parent=root;
s.p.forEach(function(d) {
var n=nodes.get(key(d));
if (!n) {
//create node at given coordinates if does not exist
n={time:d.time,
value:d.value,
parent:parent,
series:[],
direction:Math.atan2(d.value-parent.value, d.time-parent.time)};
nodes.set(key(n),n);
//add node to the parent's children
if (!parent.children) parent.children=[];
parent.children.push(n);
}
//this node is the parent of the next one
parent=n;
})
//last node is the leaf of this series
s.leafNode=parent;
parent.series.push(s);
})
//sort children by direction
nodes.values().forEach(function(n){
if (n.children)
n.children.sort(function (a,b){
if (a.direction>n.direction)
return a.direction-b.direction;
});
});
//recursively list all series through each node (bottom-up)
function listSeries(n) {
if (!n.children) return;
n.children.forEach(listSeries);
n.series=d3.merge(n.children.map(function(c){return c.series}));
}
listSeries(root);
//compute offsets for each series in each node, and add them as a list to the corresponding series
//in a first time, this is not centered
function listOffsets(n) {
var offset=0;
n.series.forEach(function(s){
s.pWithOffset.push( {node:n, offset:offset+s.w/2})
offset+=s.w;
})
n.totalOffset=offset;
if (n.children)
n.children.forEach(listOffsets);
}
listOffsets(root);
And then in the drawing section:
var line = d3.svg.line()
.interpolate("linear")
.x(function(d) { return (d.node.time-Math.sin(d.node.direction)*(d.offset-d.node.totalOffset/2)); })
.y(function(d) { return (d.node.value+Math.cos(d.node.direction)*(d.offset-d.node.totalOffset/2)); })
;
LineGroup.selectAll(".line")
.data(series)
.enter().append("path")
.attr("class", "line")
.attr("d", function(d){ return line(d.pWithOffset); })
.attr("stroke", function(d){ return d.c; })
.attr("stroke-width", function(d){ return d.w; })
.attr("fill", "none");
I am working on a sunburst viz based off of Mike Bostock's Zoomable Sunburst example.
I want to be able to change the underlying data using a whole new JSON (which has the same structure but different 'size' values), and have the sunburst animate a transition to reflect the updated data.
If I change the data of the path elements using .data(), and then attempt to update in the following fashion:
path.data(partition.nodes(transformed_json))
.transition()
.duration(750)
.attrTween("d", arcTween(transformed_json));
(..which is pretty much the exact same code as the click fn)
function click(d) {
path.transition()
.duration(750)
.attrTween("d", arcTween(d));
}
..I find that the sunburst does correctly change to reflect the new data, but it snaps into place rather than smoothly transitioning, like it does when you zoom in.
http://jsfiddle.net/jTV2y/ <-- Here is a jsfiddle with the issue isolated (the transition happens one second after you click 'Run')
I'm guessing that I need to create a different arcTween() fn, but my d3 understanding is not there yet. Many thanks!
Your example is quite similar to the sunburst partition example, which also updates data with a transition. The difference is that in this example it's the same underlying data with different value accessors. This means that you can't save the previous value in the data (as that will be different), but need to put it somewhere else (e.g. the DOM element).
The updated tween function looks like this:
function arcTweenUpdate(a) {
var i = d3.interpolate({x: this.x0, dx: this.dx0}, a);
return function(t) {
var b = i(t);
this.x0 = b.x;
this.dx0 = b.dx;
return arc(b);
};
}
This requires, as in the original example, to save the original x and dx values:
.enter().append("path")
.each(function(d) {
this.x0 = d.x;
this.dx0 = d.dx;
});
Complete example here. This one has a kind of weird transition which is cause by the different order of the data in the layout. You can disable that by calling .sort(null), see here.