D3 circle packing - swithc color for each branch - d3.js

I have a d3 circle packing, and I would like to find an elegant way to color each branch differently. I have a depth property, so it is easy to apply a gradient based on it.
To color each branch differently I think it requires to know what color has been attributed to the precedent sibling but I am not sure how to find it.
Any help is appreciated, I am not sure how to get started.
thanks
the coloring is produced by the following that will return a color based on the depth in the tree:
var color = d3.scale
.linear()
.domain([-1, 5])
.range(["hsl(152,80%,80%)", "hsl(228,30%,40%)"])
.interpolate(d3.interpolateHcl);
function color_for_node(node) {
var out;
//some other operation removed here for simplification
out = node.children ? color(node.depth) : null;
return out;
}
It is then used by the following (simplified) :
circle = svg
.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.someOtherStuff()
.style("fill", color_for_node) //<--used here
(I am working on a solution, I will edit this again once it is done. The key in this solution is not really depending on D3, but on the data tree. Luckily I found out that each node references its parent, so I think it is possible to find out of a node belongs to a specific branch which what I am doing now )

Here is a generic solution. Since d3 collection are properly ordered, every time a root branch node is met, it is followed by its childs, until it reaches another root branch node. Therefore we can effectively switch the color from one branch to another without havin gto check the parents of each node, which would be costly computation.
const ranges = [
["hsl(120,0%,90%)", "hsl(120,0%,50%)"], //grey
["hsl(228,80%,80%)", "hsl(300,30%,40%)"], //light blue
["hsl(300,80%,80%)", "hsl(360,30%,40%)"], //light pink to brick
["hsl(152,80%,80%)", "hsl(228,30%,40%)"], //light green-blue
["hsl(80,80%,80%)", "hsl(120,30%,40%)"], //light yellow-green
["hsl(120,80%,80%)", "hsl(150,30%,40%)"], //light green
["hsl(0,80%,80%)", "hsl(40,30%,40%)"], //light pink to brown
["hsl(40,80%,80%)", "hsl(80,30%,40%)"] //sand
];
let rangeIndex = 0;
const color = node => {
let range = ranges[rangeIndex];
if (node.depth === 1) {
rangeIndex = rangeIndex < ranges.length - 1 ? rangeIndex + 1 : 0;
range = ranges[rangeIndex];
}
return d3.scale
.linear()
.domain([-1, 5])
.range(range)
.interpolate(d3.interpolateHcl)(node.depth);
};

Related

How to filter views with an opacity range in d3/dc.js?

I don't know if this is possible in dc.js and crossfilter.js, but I decided to ask anyways.
I combined a scatterplot and a barChart example from dc to make an interactive dashboard:
var chart1 = dc.scatterPlot("#test1");
var chart2 = dc.scatterPlot("#test2");
d3.csv("output.csv", function(error, data) {
data.forEach(function (x) {
x.x = +x.x;
x.y = +x.y;
x.z = +x.z;
});
var ndx = crossfilter(data),
dim1 = ndx.dimension(function (d) {
return [d.x, d.y];
}),
dim2 = ndx.dimension(function (d) {
return Math.floor(parseFloat(d.z) * 10) / 10;
}),
group1 = dim1.group(),
group2 = dim2.group(),
chart1.width(300)
.height(300)
.x(d3.scale.linear().domain([-2, 2]))
.y(d3.scale.linear().domain([-2, 2]))
.yAxisLabel("y")
.xAxisLabel("x")
.clipPadding(10)
.dimension(dim1)
//.excludedOpacity(0.5)
.excludedColor('#ddd')
.group(group1)
.symbolSize([2.5]);
chart2
.width(600)
.dimension(dim2)
.group(group2)
.x(d3.scale.linear().domain([0,3]))
.elasticY(true)
.controlsUseVisibility(false)
.barPadding([0.1])
.outerPadding([0.05]);
chart2.xAxis().tickFormat(function(d) {return d}); // convert back to base unit
chart2.yAxis().ticks(10);
dc.renderAll();
});
Result when brushing the bar chart:
I want to change the filtering so that when I brush the bar chart, brushed points in the scatterplot will have an opacity value, which is 1 in the middle of the brush, and decreases towards end of the range of brush.
The other points (outside the brush) should just be grey, instead of invisible as in the current script. Illustration:
Is this possible to do with the dc.js and crossfilter.js?
PS: The attached scatterplot isn't the desired outcome. It is not filtered based on opacity. I just attached it to show how the other points(grey) should look like after brushing the bar chart.
I couldn't get this working with animated transitions, because there is something I am missing about how to interrupt transitions, and the original dc.scatterPlot is already applying opacity transitions.
So, to start off, let's turn transitions on the original scatter plot:
chart1
.transitionDuration(0)
We also need to add Z to the input data for the scatter plot. Although it would make more sense to add it to the value, it's easy to add it to the key (and the scatter plot will ignore extra elements in the key):
dim1 = ndx.dimension(function (d) {
return [d.x, d.y, d.z];
}),
Then we can add a handler to to the scatter plot to apply opacity to the dots, based on the range of the filter in the bar chart:
chart1.on('pretransition', function(chart) {
var range = chart2.filter(); // 1
console.assert(!range || range.filterType==='RangedFilter'); // 2
var mid, div; // 3
if(range) {
mid = (range[0] + range[1])/2;
div = (range[1] - range[0])/2;
}
chart1.selectAll('path.symbol') // 4
.attr('opacity', function(d) {
if(range) { // 5
if(d.key[2] < range[0] || range[1] < d.key[2])
op = 0; // 6
else
op = 1 - Math.abs(d.key[2] - mid)/div; // 7
//console.log(mid, div, d.key[2], op);
return op;
}
else return 1;
})
});
Get the current brush/filter from the bar chart
It should either be null or it should be a RangedFilter
Find the midpoint and the distance from the midpoint to the edges of the brush
Now apply opacity to all symbols in the scatter plot
If there is an active brush, apply opacity (otherwise 1)
If the symbol is outside the brush, opacity is 0
Otherwise the opacity is linear based on the distance from the midpoint
You could probably use d3.ease to map the distance [0,1] to opacity [0,1] using a curve instead of linearly. This might be nice so that it emphasizes the points closer to the midpoint
This demo is not all that cool because the data is purely random, but it shows the idea:
https://jsfiddle.net/gordonwoodhull/qq31xcoj/64/
EDIT: alright, it's a total abuse of dc.js, but if you really want to use it without filtering, and displaying the excluded points in grey, you can do that too.
This will disable filtering on the bar chart:
chart2.filterHandler(function(_, filters) { return filters; });
Then apply opacity and color to the scatter plot like this instead:
chart1.selectAll('path.symbol')
.attr('opacity', function(d) {
if(range && range.isFiltered(d.key[2]))
return 1 - Math.abs(d.key[2] - mid)/div;
else return 1;
})
.attr('fill', function(d) {
if(!range || range.isFiltered(d.key[2]))
return chart1.getColor(d);
else return '#ccc';
})
With this data it's tricky to see the difference between the light blue dots and the grey dots. Maybe it will work better with non-random data, maybe not. Maybe another color will help.
Again, you might as well use straight D3, since this disables most of what dc.js and crossfilter do. But you'd have to start from scratch to ask that question.
Updated fiddle.
EDIT 2: sort the dots by filteredness like this:
.sort(function(d) {
return range && range.isFiltered(d.key[2]) ? 1 : 0;
})
Fiddle 3

How to tune horizontal node position in d3 sankey.js?

I am trying to plot some flow diagrams using d3's sankey.js.
I am stuck at arranging nodes x positions in the diagrams.
t2_a should be in same column as t2_b as they represent quantity from same time period. However by default this is placed at the end which gives wrong interpretation.
I can arrange manually for small number of nodes but its really difficult when number of nodes increase. Any help or suggestion would be highly appreciated.
In sankey.js comment the moveSinksRight call in computeNodeBreadths:
function computeNodeBreadths() {
var remainingNodes = nodes,
nextNodes,
x = 0;
while (remainingNodes.length) {
nextNodes = [];
remainingNodes.forEach(function(node) {
node.x = x;
node.dx = nodeWidth;
node.sourceLinks.forEach(function(link) {
nextNodes.push(link.target);
});
});
remainingNodes = nextNodes;
++x;
}
//
// moveSinksRight(x); <-- comment this
scaleNodeBreadths((width - nodeWidth) / (x - 1));
}

Avoid overlapping of nodes in tree layout in d3.js

I have created a collapsible tree to represent some biological data.
In this tree the size of the node represents the importance of the node. As I have a huge data and also the sizes of the nodes vary,they overlap over each other. I need to specify the distance between the sibling nodes.
I tried tree.separation() method but it didn't work.
Code is as follows :
tree.separation(seperator);
function seperator(a, b)
{
if(a.parent == b.parent)
{
if((a.abundance == (maxAbd)) || (b.abundance == (maxAbd)))
{
return 2;
}
else
{
return 1;
}
}
}
This is giving me error saying:
Unexpected value translate(433.33333333333337,NaN) parsing transform attribute.
I understand that that after adding the separation method it is unable to calculate the x coordinate for the nodes. Can anyone please help me with how to do this?
I also tried modifying the source code as suggested in https://groups.google.com/forum/#!topic/d3-js/7Js0dGrnyek but that did not work either.
Please suggest some solution.
I had the same problem. This is how I solved it. I have width assigned to each node, height for now is the same for all nodes (basically nodes with smaller height than nodeHeight, get centered vertically):
var tree = d3.layout.tree().nodeSize([1, nodeHeight])
.separation(function(a, b) {
var width = a.width + b.width,
distance = width / 2 + 16; // horizontal distance between nodes = 16
return distance;
}),
nodes = tree.nodes(data),
links = tree.links(nodes);
Hope this helps.
SiegS's answer just works fine!
My situation is that: My node is actually some text, which may have various width, which I don't know in advance. So I need to calculate the width of each nodes first.
I have a JSON object json_data as my data.
var tree = d3.layout.tree()
.sort(null)
.size([500,500])
.children( some_function_identify_children );
var nodes = tree.nodes(json_data); //which doesn't consider the node's size;
var links = tree.links(nodes);
// append a svg;
var layoutRoot = d3.select("body")
.append("svg:svg").attr("width","600").attr("height","600")
.append("svg:g")
.attr("class","container");
var nodeGroup = layoutRoot.selectAll("g.node")
.data(nodes)
.enter().append("text").text(function(d){return d.text;});
// now we knows the text of each node.
//calculate each nodes's width by getBBox();
nodeGroup.each(function(d,i){d["width"] = this.getBBox().width;})
//set up a new tree layout which consider the node width.
var newtree = d3.layout.tree()
.size([500,500])
.children(function(d) {return d.children;})
.separation(function(a,b){
return (a.width+b.width)/2+2;
});
//recalcuate the node's x and y by newtree
newtree.nodes(nodes[0]); //nodes[0] is the root
links = newtree.links(nodes);
//redraw the svg using new nodes and links.
...
Hope this will help.

Getting Screen Positions of D3 Nodes After Transform

I'm trying to get the screen position of a node after the layout has been transformed by d3.behavior.zoom() but I'm not having much luck. How might I go about getting a node's actual position in the window after translating and scaling the layout?
mouseOver = function(node) {
screenX = magic(node.x); // Need a magic function to transform node
screenY = magic(node.y); // positions into screen coordinates.
};
Any guidance would be appreciated.
EDIT: 'node' above is a force layout node, so it's x and y properties are set by the simulation and remain constant after the simulation comes to rest, regardless of what type of transform is applied.
EDIT: The strategy I'm using to transform the SVG comes from d3's zoom behavior, which is outlined here: SVG Geometric Zooming.
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.call(d3.behavior.zoom().scaleExtent([1, 8]).on("zoom", zoom))
.append("g");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height);
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 2.5)
.attr("transform", function(d) { return "translate(" + d + ")"; });
function zoom() {
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
It's pretty straightforward. d3's zoom behavior delivers pan and zoom events to a handler, which applies the transforms to the container element by way of the transform attribute.
EDIT: I'm working around the issue by using mouse coordinates instead of node coordinates, since I'm interested in the node position when the node is hovered over with the mouse pointer. It's not exactly the behavior I'm after, but it works for the most part, and is better than nothing.
EDIT: The solution was to get the current transformation matrix of the svg element with element.getCTM() and then use it to offset the x and y coordinates to a screen-relative state. See below.
It appears the solution to my original question looks something like this:
(Updated to support rotation transforms.)
// The magic function.
function getScreenCoords(x, y, ctm) {
var xn = ctm.e + x*ctm.a + y*ctm.c;
var yn = ctm.f + x*ctm.b + y*ctm.d;
return { x: xn, y: yn };
}
var circle = document.getElementById('svgCircle'),
cx = +circle.getAttribute('cx'),
cy = +circle.getAttribute('cy'),
ctm = circle.getCTM(),
coords = getScreenCoords(cx, cy, ctm);
console.log(coords.x, coords.y); // shows coords relative to my svg container
Alternately, this can also be done using the translate and scale properties from d3.event (if rotation transforms are not needed):
// This function is called by d3's zoom event.
function zoom() {
// The magic function - converts node positions into positions on screen.
function getScreenCoords(x, y, translate, scale) {
var xn = translate[0] + x*scale;
var yn = translate[1] + y*scale;
return { x: xn, y: yn };
}
// Get element coordinates and transform them to screen coordinates.
var circle = document.getElementById('svgCircle');
cx = +circle.getAttribute('cx'),
cy = +circle.getAttribute('cy'),
coords = getScreenCoords(cx, cy, d3.event.translate, d3.event.scale);
console.log(coords.x, coords.y); // shows coords relative to my svg container
// ...
}
EDIT: I found the below form of the function to be the most useful and generic, and it seems to stand up where getBoundingClientRect falls down. More specifically, when I was trying to get accurate SVG node positions in a D3 force layout project, getBoundingClientRect produced inaccurate results while the below method returned the circle element's exact center coordinates across multiple browsers.
(Updated to support rotation transforms.)
// Pass in the element and its pre-transform coords
function getElementCoords(element, coords) {
var ctm = element.getCTM(),
x = ctm.e + coords.x*ctm.a + coords.y*ctm.c,
y = ctm.f + coords.x*ctm.b + coords.y*ctm.d;
return {x: x, y: y};
};
// Get post-transform coords from the element.
var circle = document.getElementById('svgCircle'),
x = +circle.getAttribute('cx'),
y = +circle.getAttribute('cy'),
coords = getElementCoords(circle, {x:x, y:y});
// Get post-transform coords using a 'node' object.
// Any object with x,y properties will do.
var node = ..., // some D3 node or object with x,y properties.
circle = document.getElementById('svgCircle'),
coords = getElementCoords(circle, node);
The function works by getting the transform matrix of the DOM element, and then using the matrix rotation, scale, and translate information to return the post-transform coordinates of the given node object.
You can try node.getBBox() to get the pixel positions of a tight bounding box around the node shapes after any transform has been applied. See here for more: link.
EDIT:
getBBox doesn't work quite the way I thought. Since the rectangle is defined in terms of the transformed coordinate space it is always relative to the parent <g> and will therefore always be the same for contained shapes.
There is another function called element.getBoundingClientRect that appears to be quite widely supported and it returns its rectangle in pixel position relative to the top left of the browser view port. That might get you closer to what you want without needing to mess with the transform matrix directly.

In d3 is it possible to dynamically change where a path position starts for tweening?

to: clarify. Picture a circle. We start drawing the circle from a particular coordinate. Now lets draw the circle starting from another coordinate.
I am playing with path data derived from SVG glyphs and then using d3js tween to animate the change between the paths.
For this example, counting from 1 -> 9,0 and then repeating.
http://jsfiddle.net/chrisloughnane/HL2ET/
As you can see some of the transitions are not as nice as others. They draw a line that closes the path for the next path. (I'm guessing that) this happens when the start and end of the path are very far apart when the calculation for the new shape is made. When it works it's very nice.
Could anybody suggest a possible solution to the ugly lines?
CODE without path data
svg.append("path")
.attr("transform", "translate(150,300)scale(.2,-.2)")
.style("stroke", "red")
.style("fill", "gray")
.style("stroke-width", "9")
.attr("d", d0)
.call(transition, digits[0], digits[position]);
function transition(path, d0, d1) {
position++;
if(position==10)
{
position=0;
}
path.transition()
.duration(2000)
.attrTween("d", pathTween(d1, 4))
.each("end", function() { d3.select(this).call(transition, d1, digits[position]); });
}
function pathTween(d1, precision) {
return function() {
var path0 = this,
path1 = path0.cloneNode(),
n0 = path0.getTotalLength(),
n1 = (path1.setAttribute("d", d1), path1).getTotalLength();
// Uniform sampling of distance based on specified precision.
var distances = [0], i = 0, dt = precision / Math.max(n0, n1);
while ((i += dt) < 1) distances.push(i);
distances.push(1);
// Compute point-interpolators at each distance.
var points = distances.map(function(t) {
var p0 = path0.getPointAtLength(t * n0),
p1 = path1.getPointAtLength(t * n1);
return d3.interpolate([p0.x, p0.y], [p1.x, p1.y]);
});
return function(t) {
return t < 1 ? "M" + points.map(function(p) { return p(t); }).join("L") : d1;
};
};
}
Unfortunately it fails on chrome mobile too where as http://bl.ocks.org/mbostock/3081153 works fine.
The next step is to apply this effect to sentences.
The difference between your example and that in Bostock's is that in his example there is a single continuous path that he tweens into another single continuous path.
Whereas, in your example, digits like 1, 2, 3, 5, 6, 7 can be drawn using single continuous path. But, in order to draw digits like 4, 6, 9 and 0 you need 2 paths- one on top of the other. And, for digit 8, you need to have 2 paths on top of an outer path.
So, my suggestion would be to keep 2 paths at all times atop the outer path that you are using at present & give them appropriate dimensions whenever any peculiar digit is to be shown.
Refer image for more details:

Resources