Update textPath after tween transition in D3 - javascript

I'm trying to put together an animated sunburst diagram that can zoom and change between data representations, based some examples I've found: Vasco, Visual Cinnamon, David Richard etc.
I'm not able to get the animation part to properly work. The arcs are updating, but the text labels aren't. Any advice? Also, I'm pretty new to this so I'd appreciate any other tips or advice on how to structure my code as well.
// Variables
const width = window.innerWidth;
const height = window.innerHeight;
const radius = (Math.min(width, height) / 2) - 5;
const color = d3.scaleOrdinal(d3.schemeCategory20b);
const x = d3.scaleLinear().range([0, 2 * Math.PI]).clamp(true);
const y = d3.scaleLinear().range([0, radius]);
// Create our sunburst data structure and size it.
const partition = d3.partition();
// Size our <svg> element
const svg = d3.select('#chart').append('svg')
.style('width', '100vw')
.style('height', '100vh')
.attr('viewBox', `${-width / 2} ${-height / 2} ${width} ${height}`)
.attr("id", "container");
arc = d3.arc()
.startAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x0)));
})
.endAngle(function(d) {
return Math.max(0, Math.min(2 * Math.PI, x(d.x1)));
})
.innerRadius(function(d) {
return Math.max(0, y(d.y0));
})
.outerRadius(function(d) {
return Math.max(0, y(d.y1));
});
// JSON data
var nodeData = {
"name": "TOPICS", "children": [{
"name": "Topic A",
"children": [{"name": "Sub A1", "size": 4}, {"name": "Sub A2", "size": 4}]
}, {
"name": "Topic B",
"children": [{"name": "Sub B1", "size": 3}, {"name": "Sub B2", "size": 3}, {
"name": "Sub B3", "size": 3}]
}, {
"name": "Topic C",
"children": [{"name": "Sub A1", "size": 4}, {"name": "Sub A2", "size": 4}]
}]
};
createVisualization()
// Main function to draw and set up the visualization
function createVisualization() {
// Find the root node, calculate the node.value, and sort our nodes by node.value
root = d3.hierarchy(nodeData)
.sum(function(d) {
return d.size;
})
.sort(function(a, b) {
return b.value - a.value;
});
original = root;
partition(root);
// Add a <g> element for each node in thd data, then append <path> elements and draw lines based on the arc
// variable calculations. Last, color the lines and the slices.
slices = svg.selectAll('path')
.data(root.descendants())
.enter().append('g').attr("class", "node")
.on("click", focusOn);
slices.append('path')
.attr('class', 'main-arc')
.attr("id", function(d, i) {
return "arc_" + i;
})
.attr("d", arc)
.style('stroke', '#fff')
.style("fill", function(d) {
return color((d.children ? d : d.parent).data.name);
})
.each(function(d, i) {
const halfPi = Math.PI / 2;
const angles = [Math.max(0, Math.min(2 * Math.PI, x(d.x0))) - halfPi, Math.max(0, Math.min(2 * Math.PI, x(d.x1))) - halfPi];
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
const middleAngle = (angles[1] + angles[0]) / 2;
const invertDirection = middleAngle > 0 && middleAngle < Math.PI; // On lower quadrants write text ccw
if (invertDirection) {
angles.reverse();
}
const path = d3.path();
path.arc(0, 0, r, angles[0], angles[1], invertDirection);
//Create a new invisible arc that the text can flow along
d3.select(this).append("path")
.attr("class", "hiddenArcs")
.attr("id", "hiddenArc_" + i)
.attr("d", path.toString())
.style("fill", "none");
});
text = slices.append("text")
.attr("class", "arcText");
text.append("textPath")
.attr("xlink:href", function(d, i) {
return "#hiddenArc_" + i;
})
.attr('startOffset', '50%')
.style('fill', 'none')
.style('stroke', '#fff')
.style('stroke-width', 5)
.style('stroke-linejoin', 'round');
text.append("textPath")
.attr("xlink:href", function(d, i) {
return "#hiddenArc_" + i;
})
.attr('startOffset', '50%');
slices.selectAll("textPath")
.text(function(d) {
return d.parent ? d.data.name : ""
});
// Redraw the Sunburst Based on User Input
d3.selectAll(".sizeSelect").on("click", build);
}
function build() {
// Determine how to size the slices.
if (this.value === "size") {
root.sum(function(d) {
return d.size;
});
} else {
root.count();
}
// Calculate the sizes of each arc that we'll draw later.
partition(root);
slices.selectAll("path.main-arc").transition().duration(750).attrTween("d", arcTweenData);
slices.selectAll("path.hiddenArcs").transition().duration(750).attrTween("d", hiddenArcTweenData);
slices.selectAll("textPath")
.text(function(d) {
return d.parent ? d.data.name : ""
});
}
// Respond to slice click.
function focusOn(d) {
original = d;
svg.selectAll("path").transition().duration(1000).attrTween("d", arcTweenZoom(d))
}
// When zooming: interpolate the scales.
function arcTweenZoom(d) {
var xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
yd = d3.interpolate(y.domain(), [d.y0, 1]), // [d.y0, 1]
yr = d3.interpolate(y.range(), [d.y0 ? 40 : 0, radius]);
return function(d, i) {
return i ? function(t) {
return arc(d);
} : function(t) {
x.domain(xd(t));
y.domain(yd(t)).range(yr(t));
return arc(d);
};
};
}
// When switching data: interpolate the arcs in data space.
function arcTweenData(a, i) {
// (a.x0s ? a.x0s : 0) -- grab the prev saved x0 or set to 0 (for 1st time through)
// avoids the stash() and allows the sunburst to grow into being
// var oi = d3.interpolate({ x0: (a.x0s ? a.x0s : 0), x1: (a.x1s ? a.x1s : 0) }, a);
var oi = d3.interpolate({
x0: (a.x0s ? a.x0s : 0),
x1: (a.x1s ? a.x1s : 0)
}, a);
function tween(t) {
var b = oi(t);
a.x0s = b.x0;
a.x1s = b.x1;
return arc(b);
}
if (i == 0) {
// If we are on the first arc, adjust the x domain to match the root node
// at the current zoom level. (We only need to do this once.)
var xd = d3.interpolate(x.domain(), [original.x0, original.x1]);
return function(t) {
x.domain(xd(t));
return tween(t);
};
} else {
return tween;
}
}
function hiddenArcTweenData(a, i) {
// (a.x0s ? a.x0s : 0) -- grab the prev saved x0 or set to 0 (for 1st time through)
var oi = d3.interpolate({
x0: (a.x0s ? a.x0s : 0),
x1: (a.x1s ? a.x1s : 0)
}, a);
function tween(t) {
var b = oi(t);
a.x0s = b.x0;
a.x1s = b.x1;
return middleArc(b);
}
if (i == 0) {
// If we are on the first arc, adjust the x domain to match the root node
// at the current zoom level. (We only need to do this once.)
var xd = d3.interpolate(x.domain(), [original.x0, original.x1]);
return function(t) {
x.domain(xd(t));
return tween(t);
};
} else {
return tween;
}
}
function middleArc(d) {
const halfPi = Math.PI / 2;
const angles = [Math.max(0, Math.min(2 * Math.PI, x(d.x0))) - halfPi, Math.max(0, Math.min(2 * Math.PI, x(d.x1))) - halfPi];
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
const middleAngle = (angles[1] + angles[0]) / 2;
const invertDirection = middleAngle > 0 && middleAngle < Math.PI; // On lower quadrants write text ccw
if (invertDirection) {
angles.reverse();
}
const path = d3.path();
path.arc(0, 0, r, angles[0], angles[1], invertDirection);
return path;
}
body {
#import "https://fonts.googleapis.com/css?family=Fakt:400,600";
font-family: 'Fakt', fakt;
font-size: 12px;
font-weight: 400;
background-color: #fff;
width: 960px;
height: 700px;
margin-top: 10px;
}
.node {
cursor: pointer;
}
.node .main-arc {
stroke: #fff;
stroke-width: 1px;
}
.node .hidden-arc {
stroke-width: 1px;
stroke: #000;
}
.node text {
pointer-events: none;
dominant-baseline: middle;
text-anchor: middle;
fill: #000
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Sunburst</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<link rel="stylesheet" type="text/css" href="sunburst.css" />
</head>
<body>
<div id="main">
<label>
<input class="sizeSelect" type="radio" name="mode" value="count" checked/> Count </label>
<label>
<input class="sizeSelect" type="radio" name="mode" value="size" /> Size </label>
<div id="chart">
<script type="text/javascript" src="sunburst.js"></script>
</div>
</div>
</body>
</html>

Your JS code on line 97 says:
d3.select(this).append("path")
This will add the .hiddenArcs path as a child of the .main-arc <path>, rather than the parent <g>. This makes it invalid SVG, but it also makes the selector slices.selectAll("path.hiddenArcs") return an empty set. So, the hidden arcs never tween, and so the text stays where it is.
Changing line 97 so the hidden arc is added to the path's parent node, rather than the path, fixes this:
d3.select(this.parentNode).append("path")

Related

Creating sunburst to accept csv data

I've maybe tried to run before I can walk but I've used the following two references:
http://codepen.io/anon/pen/fcBEe
https://bl.ocks.org/mbostock/4063423
From the first I've tried to take the idea of being able to feed a csv file into the sunburst with the function buildHierarchy(csv). The rest of the code is from Mike Bostock's example.
I've narrowed the data down to something very simple as follows:
var text =
"N-CB,50\n\
N-TrP-F,800\n";
So I thought this would produce three concentric rings - which is does - but I was hoping that the inner ring would be split in the ratio of 800:50 as in the data. Why am I getting the rings that I'm getting?
<!DOCTYPE html>
<meta charset="utf-8">
<style>
#sunBurst {
position: absolute;
top: 60px;
left: 20px;
width: 250px;
height: 300px;
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
position: relative;
width: 250px;
}
form {
position: absolute;
right: 20px;
top: 30px;
}
</style>
<form>
<label>
<input type="radio" name="mode" value="size" checked> Size</label>
<label>
<input type="radio" name="mode" value="count"> Count</label>
</form>
<script src="//d3js.org/d3.v3.min.js"></script>
<div id="sunBurst"></div>
<script>
var text =
"N-CB,50\n\
N-TrP-F,800\n";
var csv = d3.csv.parseRows(text);
var json = buildHierarchy(csv);
var width = 300,
height = 250,
radius = Math.min(width, height) / 2,
color = d3.scale.category20c();
//this bit is easy to understand:
var svg = d3.select("#sunBurst").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr({
'transform': "translate(" + width / 2 + "," + height * .52 + ")",
id: "sunGroup"
});
// it seems d3.layout.partition() can be either squares or arcs
var partition = d3.layout.partition()
.sort(null)
.size([2 * Math.PI, radius * radius])
.value(function(d) {
return 1;
});
var arc = d3.svg.arc()
.startAngle(function(d) {
return d.x;
})
.endAngle(function(d) {
return d.x + d.dx;
})
.innerRadius(function(d) {
return Math.sqrt(d.y);
})
.outerRadius(function(d) {
return Math.sqrt(d.y + d.dy);
});
var path = svg.data([json]).selectAll("path")
.data(partition.nodes)
.enter()
.append("path")
.attr("display", function(d) {
return d.depth ? null : "none";
})
.attr("d", arc)
.style("stroke", "#fff")
.style("fill", function(d) {
return color((d.children ? d : d.parent).name);
})
.attr("fill-rule", "evenodd")
.style("opacity", 1)
.each(stash);
d3.selectAll("input").on("change", function change() {
var value = this.value === "size" ? function() {
return 1;
} : function(d) {
return d.size;
};
path
.data(partition.value(value).nodes)
.transition()
.duration(2500)
.attrTween("d", arcTween);
});
//});
// Stash the old values for transition.
function stash(d) {
d.x0 = d.x;
d.dx0 = d.dx;
}
// Interpolate the arcs in data space.
function arcTween(a) {
var i = d3.interpolate({
x: a.x0,
dx: a.dx0
}, a);
return function(t) {
var b = i(t);
a.x0 = b.x;
a.dx0 = b.dx;
return arc(b);
};
}
d3.select(self.frameElement).style("height", height + "px");
// Take a 2-column CSV and transform it into a hierarchical structure suitable
// for a partition layout. The first column is a sequence of step names, from
// root to leaf, separated by hyphens. The second column is a count of how
// often that sequence occurred.
function buildHierarchy(csv) {
var root = {
"name": "root",
"children": []
};
for (var i = 0; i < csv.length; i++) {
var sequence = csv[i][0];
var size = +csv[i][1];
if (isNaN(size)) { // e.g. if this is a header row
continue;
}
var parts = sequence.split("-");
var currentNode = root;
for (var j = 0; j < parts.length; j++) {
var children = currentNode["children"];
var nodeName = parts[j];
var childNode;
if (j + 1 < parts.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k]["name"] == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {
"name": nodeName,
"children": []
};
children.push(childNode);
}
currentNode = childNode;
} else {
// Reached the end of the sequence; create a leaf node.
childNode = {
"name": nodeName,
"size": size
};
children.push(childNode);
}
}
}
return root;
};
</script>
There is a further live example of here on plunker : https://plnkr.co/edit/vqUqDtPCRiSDUIwfCbnY?p=preview
Your buildHierarchy function takes whitespace into account. So, when you write
var text =
"N-CB,50\n\
N-TrP-F,800\n";
it is actually two root nodes:
'N' and ' N'
You have two options:
Use non-whitespace text like
var text =
"N-CB,50\n" +
"N-TrP-F,800\n";
Fix buildHierarchy function to trim whitespace.

Migrating from D3.js v3 to D3.js v4 not working - a select issue?

I'm trying to migrate this JSFiddle which is in D3 v3 to D3 v4, but it is not working.
I know D3 drag behavior is now simply d3.drag() which I've changed, but when trying to run it, it is giving an error on line 53:
rect = d3.select(self.rectangleElement[0][0]);
with Chrome saying:
Uncaught TypeError: Cannot read property '0' of undefined
How do I go about changing this JSFiddle so it runs in D3 v4?
As of D3 v4 a selection no longer is an array of arrays but an object. The changelog has it:
Selections no longer subclass Array using prototype chain injection; they are now plain objects, improving performance.
When doing self.rectangleElement[0][0] in v3 you were accessing the first node in a selection. To get this node in v4 you need to call selection.node() on self.rectangleElement. With this your code becomes:
rect = d3.select(self.rectangleElement.node());
Have a look at the updated JSFiddle for a working version.
First, why are you re-selecting those things? They are already the selection you want. For example, self.rectangleElement is the selection of the rect. Second, passing an object to .attr is no longer supported in version 4. Third, the drag behavior has changed and the circle are eating your second mouse down. Here's a version where I've fixed up these things:
d3.select('#rectangle').on('click', function(){ new Rectangle(); });
var w = 600, h = 500;
var svg = d3.select('body').append('svg').attr("width", w).attr("height", h);
function Rectangle() {
var self = this, rect, rectData = [], isDown = false, m1, m2, isDrag = false;
svg.on('mousedown', function() {
console.log(isDown);
m1 = d3.mouse(this);
if (!isDown && !isDrag) {
self.rectData = [ { x: m1[0], y: m1[1] }, { x: m1[0], y: m1[1] } ];
self.rectangleElement = d3.select('svg').append('rect').attr('class', 'rectangle').call(dragR);
self.pointElement1 = d3.select('svg').append('circle').attr('class', 'pointC').call(dragC1);
self.pointElement2 = d3.select('svg').append('circle').attr('class', 'pointC').call(dragC2);
self.pointElement3 = svg.append('circle').attr('class', 'pointC').call(dragC3);
self.pointElement4 = svg.append('circle').attr('class', 'pointC').call(dragC4);
updateRect();
isDrag = false;
} else {
isDrag = true;
}
isDown = !isDown;
})
.on('mousemove', function() {
m2 = d3.mouse(this);
if(isDown && !isDrag) {
self.rectData[1] = { x: m2[0] - 5, y: m2[1] - 5};
updateRect();
}
});
function updateRect() {
self.rectangleElement
.attr("x", self.rectData[1].x - self.rectData[0].x > 0 ? self.rectData[0].x : self.rectData[1].x)
.attr("y", self.rectData[1].y - self.rectData[0].y > 0 ? self.rectData[0].y : self.rectData[1].y)
.attr("width", Math.abs(self.rectData[1].x - self.rectData[0].x))
.attr("height", Math.abs(self.rectData[1].y - self.rectData[0].y));
var point1 = self.pointElement1.data(self.rectData);
point1.attr('r', 5)
.attr('cx', self.rectData[0].x)
.attr('cy', self.rectData[0].y);
var point2 = self.pointElement2.data(self.rectData);
point2.attr('r', 5)
.attr('cx', self.rectData[1].x)
.attr('cy', self.rectData[1].y);
var point3 = self.pointElement3.data(self.rectData);
point3.attr('r', 5)
.attr('cx', self.rectData[1].x)
.attr('cy', self.rectData[0].y);
var point3 = self.pointElement4.data(self.rectData);
point3.attr('r', 5)
.attr('cx', self.rectData[0].x)
.attr('cy', self.rectData[1].y);
}
var dragR = d3.drag().on('drag', dragRect);
function dragRect() {
var e = d3.event;
for(var i = 0; i < self.rectData.length; i++){
self.rectangleElement
.attr('x', self.rectData[i].x += e.dx )
.attr('y', self.rectData[i].y += e.dy );
}
self.rectangleElement.style('cursor', 'move');
updateRect();
}
var dragC1 = d3.drag().on('drag', dragPoint1);
var dragC2 = d3.drag().on('drag', dragPoint2);
var dragC3 = d3.drag().on('drag', dragPoint3);
var dragC4 = d3.drag().on('drag', dragPoint4);
function dragPoint1() {
var e = d3.event;
self.pointElement1
.attr('cx', function(d) { return d.x += e.dx })
.attr('cy', function(d) { return d.y += e.dy });
updateRect();
}
function dragPoint2() {
var e = d3.event;
self.pointElement2
.attr('cx', self.rectData[1].x += e.dx )
.attr('cy', self.rectData[1].y += e.dy );
updateRect();
}
function dragPoint3() {
var e = d3.event;
self.pointElement3
.attr('cx', self.rectData[1].x += e.dx )
.attr('cy', self.rectData[0].y += e.dy );
updateRect();
}
function dragPoint4() {
var e = d3.event;
self.pointElement4
.attr('cx', self.rectData[0].x += e.dx )
.attr('cy', self.rectData[1].y += e.dy );
updateRect();
}
}//end Rectangle
svg {
border: solid 1px red;
}
rect {
fill: lightblue;
stroke: blue;
stroke-width: 2px;
}
<button id='rectangle'>Rectangle</button>
<script src="https://d3js.org/d3.v4.js" charset="utf-8"></script>

How do I draw directed arrows between rectangles of different dimensions in d3?

I would like to draw directed arcs between rectangles (nodes that are represented by rectangles) in such a way that the arrow-tip always hits the edge in a graceful way. I have seen plenty of SO posts on how to do this for circles (nodes represented by circles). Quite interestingly, most d3 examples deal with circles and squares (though squares to a lesser extent).
I have an example code here. Right now my best attempt can only draw from center-point to center-point. I can shift the end point (where the arrow should be), but upon experimenting with dragging the rectangles around, the arcs don't behave as intended.
Here's what I've got.
But I need something like this.
Any ideas on how I can easily do this in d3? Is there some built-in library/function that can help with this type of thing (like with the dragging capabilities)?
A simple algorithm to solve your problem is
when a node is dragged do the following for each of its incoming/outgoing edges
let a be the node dragged and b the node reached through the outgoing/incoming edge
let lineSegment be a line segment between the centers of a and b
compute the intersection point of a and lineSegment, this is done by iterating the 4 segments that make the box and checking the intersection of each of them with lineSegment, let ia be the intersection point of one of the segments of a and lineSegment, find ib in a similar fashion
Corner cases that I have considered but haven't solved
when a box's center is inside the other box there won't be 2 segment intersections
when both intersections points are the same! (solved this one in an edit)
when your graph is a multigraph edges would render on top of each other
plunkr demo
EDIT: added the check ia === ib to avoid creating an edge from the top left corner, you can see this on the plunkr demo
$(document).ready(function() {
var graph = {
nodes: [
{ id: 'n1', x: 10, y: 10, width: 200, height: 200 },
{ id: 'n2', x: 10, y: 270, width: 200, height: 250 },
{ id: 'n3', x: 400, y: 270, width: 200, height: 300 }
],
edges: [
{ start: 'n1', stop: 'n2' },
{ start: 'n2', stop: 'n3' }
],
node: function(id) {
if(!this.nmap) {
this.nmap = { };
for(var i=0; i < this.nodes.length; i++) {
var node = this.nodes[i];
this.nmap[node.id] = node;
}
}
return this.nmap[id];
},
mid: function(id) {
var node = this.node(id);
var x = node.width / 2.0 + node.x,
y = node.height / 2.0 + node.y;
return { x: x, y: y };
}
};
var arcs = d3.select('#mysvg')
.selectAll('line')
.data(graph.edges)
.enter()
.append('line')
.attr({
'data-start': function(d) { return d.start; },
'data-stop': function(d) { return d.stop; },
x1: function(d) { return graph.mid(d.start).x; },
y1: function(d) { return graph.mid(d.start).y; },
x2: function(d) { return graph.mid(d.stop).x; },
y2: function(d) { return graph.mid(d.stop).y },
style: 'stroke:rgb(255,0,0);stroke-width:2',
'marker-end': 'url(#arrow)'
});
var g = d3.select('#mysvg')
.selectAll('g')
.data(graph.nodes)
.enter()
.append('g')
.attr({
id: function(d) { return d.id; },
transform: function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
}
});
g.append('rect')
.attr({
id: function(d) { return d.id; },
x: 0,
y: 0,
style: 'stroke:#000000; fill:none;',
width: function(d) { return d.width; },
height: function(d) { return d.height; },
'pointer-events': 'visible'
});
function Point(x, y) {
if (!(this instanceof Point)) {
return new Point(x, y)
}
this.x = x
this.y = y
}
Point.add = function (a, b) {
return Point(a.x + b.x, a.y + b.y)
}
Point.sub = function (a, b) {
return Point(a.x - b.x, a.y - b.y)
}
Point.cross = function (a, b) {
return a.x * b.y - a.y * b.x;
}
Point.scale = function (a, k) {
return Point(a.x * k, a.y * k)
}
Point.unit = function (a) {
return Point.scale(a, 1 / Point.norm(a))
}
Point.norm = function (a) {
return Math.sqrt(a.x * a.x + a.y * a.y)
}
Point.neg = function (a) {
return Point(-a.x, -a.y)
}
function pointInSegment(s, p) {
var a = s[0]
var b = s[1]
return Math.abs(Point.cross(Point.sub(p, a), Point.sub(b, a))) < 1e-6 &&
Math.min(a.x, b.x) <= p.x && p.x <= Math.max(a.x, b.x) &&
Math.min(a.y, b.y) <= p.y && p.y <= Math.max(a.y, b.y)
}
function lineLineIntersection(s1, s2) {
var a = s1[0]
var b = s1[1]
var c = s2[0]
var d = s2[1]
var v1 = Point.sub(b, a)
var v2 = Point.sub(d, c)
//if (Math.abs(Point.cross(v1, v2)) < 1e-6) {
// // collinear
// return null
//}
var kNum = Point.cross(
Point.sub(c, a),
Point.sub(d, c)
)
var kDen = Point.cross(
Point.sub(b, a),
Point.sub(d, c)
)
var ip = Point.add(
a,
Point.scale(
Point.sub(b, a),
Math.abs(kNum / kDen)
)
)
return ip
}
function segmentSegmentIntersection(s1, s2) {
var ip = lineLineIntersection(s1, s2)
if (ip && pointInSegment(s1, ip) && pointInSegment(s2, ip)) {
return ip
}
}
function boxSegmentIntersection(box, lineSegment) {
var data = box.data()[0]
var topLeft = Point(data.x, data.y)
var topRight = Point(data.x + data.width, data.y)
var botLeft = Point(data.x, data.y + data.height)
var botRight = Point(data.x + data.width, data.y + data.height)
var boxSegments = [
// top
[topLeft, topRight],
// bot
[botLeft, botRight],
// left
[topLeft, botLeft],
// right
[topRight, botRight]
]
var ip
for (var i = 0; !ip && i < 4; i += 1) {
ip = segmentSegmentIntersection(boxSegments[i], lineSegment)
}
return ip
}
function boxCenter(a) {
var data = a.data()[0]
return Point(
data.x + data.width / 2,
data.y + data.height / 2
)
}
function buildSegmentThroughCenters(a, b) {
return [boxCenter(a), boxCenter(b)]
}
// should return {x1, y1, x2, y2}
function getIntersection(a, b) {
var segment = buildSegmentThroughCenters(a, b)
console.log(segment[0], segment[1])
var ia = boxSegmentIntersection(a, segment)
var ib = boxSegmentIntersection(b, segment)
if (ia && ib) {
// problem: the arrows are drawn after the intersection with the box
// solution: move the arrow toward the other end
var unitV = Point.unit(Point.sub(ib, ia))
// k = the width of the marker
var k = 18
ib = Point.sub(ib, Point.scale(unitV, k))
return {
x1: ia.x,
y1: ia.y,
x2: ib.x,
y2: ib.y
}
}
}
var drag = d3.behavior.drag()
.origin(function(d) {
return d;
})
.on('dragstart', function(e) {
d3.event.sourceEvent.stopPropagation();
})
.on('drag', function(e) {
e.x = d3.event.x;
e.y = d3.event.y;
var id = 'g#' + e.id
var target = d3.select(id)
target.data().x = e.x
target.data().y = e.y
target.attr({
transform: 'translate(' + e.x + ',' + e.y + ')'
});
d3.selectAll('line[data-start=' + e.id + ']')
.each(function (d) {
var line = d3.select(this)
var other = d3.select('g#' + line.attr('data-stop'))
var intersection = getIntersection(target, other)
intersection && line.attr(intersection)
})
d3.selectAll('line[data-stop=' + e.id + ']')
.each(function (d) {
var line = d3.select(this)
var other = d3.select('g#' + line.attr('data-start'))
var intersection = getIntersection(other, target)
intersection && line.attr(intersection)
})
})
.on('dragend', function(e) {
});
g.call(drag);
})
svg#mysvg { border: 1px solid black;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg id="mysvg" width="800" height="800">
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refx="0" refy="3" orient="auto" markerUnits="strokeWidth">
<path d="M0,0 L0,6 L9,3 z" fill="#f00" />
</marker>
</defs>
</svg>
Here's the result: https://jsfiddle.net/he0f4u23/2/
For source arrows I just filled rectangles with white to paint the arrow.
For target it is a little bit more trickier than you think. You have to calculate source and target rectangles positions and draw your arrow accordingly.
I've made a tarmid function with addition to you mid function. Your mid function calculates the arrows source point which is fine. But for target point I used the tarmid function which is:
tarmid: function(d) {
var startnode = this.node(d.start);
var endnode = this.node(d.stop);
if(startnode.x == endnode.x && startnode.y <= endnode.y){
var x = endnode.width / 2.0 + endnode.x,
y = endnode.y -17;
}else if(startnode.x < endnode.x && startnode.y <= endnode.y){
var x = endnode.x-17,
y = endnode.y + startnode.height / 2.0;
}
return { x: x, y: y };
}
see how I calculated the target point according to the rectangle placement.
Also notice that these two are not the only cases for all rectangle placement and you must update your function accordingly so I'm leaving the rest to you.

Set position of polygons and paths in d3

I have several external SVG files I want to read in and position. Some of the SVG files are polygons, others are paths. For most of them, I specific a random amount to translate the files when I read them in. But I want to specifically assign a position for a couple of the shapes. (JSFiddle).
var width = 300, height = 300;
var sampleSVG = d3.select("body")
.append("svg")
.attr( {width: width, height: height} );
var shapes = [
{url:"https://dl.dropboxusercontent.com/u/2467665/shapes/shapes-01.svg", number: 1, color: 'red'},
{url:"https://dl.dropboxusercontent.com/u/2467665/shapes/shapes-02.svg", number: 2, color: 'yellow'},
{url:"https://dl.dropboxusercontent.com/u/2467665/shapes/shapes-03.svg", number: 3, color: 'orange'},
{url:"https://dl.dropboxusercontent.com/u/2467665/shapes/shapes-04.svg", number: 4, color: 'green'},
{url:"https://dl.dropboxusercontent.com/u/2467665/shapes/shapes-05.svg", number: 5, color: 'blue'},
{url:"https://dl.dropboxusercontent.com/u/2467665/shapes/shapes-06.svg", number: 6, color: 'purple'},
{url:"https://dl.dropboxusercontent.com/u/2467665/shapes/shapes-07.svg", number: 7, color: 'red'},
{url:"https://dl.dropboxusercontent.com/u/2467665/shapes/shapes-08.svg", number: 8, color: 'orange'}];
var q = queue();
shapes.forEach(function(shape) {
q.defer(d3.xml, shape.url, "image/svg+xml");
});
q.awaitAll(function (error, results) {
sampleSVG.selectAll('g.shape').data(shapes)
// g tag is created for positioning the shape at random location
.enter().append('g').attr('class', 'shape')
.attr('transform', function () {
return 'translate(' + Math.random() * (w - 50) + ',' + Math.random() * (h - 50) + ')'
})
.each(function (d, i) {
// the loaded svg file is then appended inside the g tag
this.appendChild(results[i].documentElement);
d3.select(this).select('svg').select("*")
.attr("stroke", function () {return d.color;})
.attr('transform', function () {
if (d.number > 6) {
var newX = 200,
newY = 200;
//This is where I want to set the position
return 'scale(0.3) translate(0,0)'}
else {return 'scale(0.1)'}
;})
})
});
Basically, I want the shapes to have different random positions except for two shapes (those with d.number > 6) which I want to appear at (200,200).
Is there a way to either reset translate and then run it again with new amounts, or specifically set the position of both paths and polygons?
Still unclear, but I think this is what you're looking for:
q.awaitAll(function (error, results) {
sampleSVG.selectAll('g.shape')
.data(shapes)
.enter()
.append('g')
.attr('class', 'shape')
.each(function (d, i) {
// the loaded svg file is then appended inside the g tag
this.appendChild(results[i].documentElement);
d3.select(this).select('svg').select("*")
.attr("stroke", d.color);
})
.attr('transform', function (d, i) {
// In general, in here we're setting the transform of a <g>, which contains an svg. So:
if (d.number > 6) {
var newX = 200,
newY = 200;
// What you return here will scale and translate the <g> (and the svg inside it)
return 'scale(0.3) translate(0,0)';
}
else {
return 'translate(' + Math.random() * (w - 50) + ',' + Math.random() * (h - 50) + ') scale(0.1)';
}
});
});

d3: Nodes + Links to make a multi-generation family tree; how to parse data to draw lines?

I'm working on making a three or four generation family tree in d3.js. You can see the early version here:
http://jsfiddle.net/Asparagirl/uenh4j92/8/
Code:
// People
var nodes = [
{ id: 1, name: "Aaron", x: 50, y: 100, gender: "male", dob: "1900", hasParent: false, hasSpouse: true, spouse1_id: 2 },
{ id: 2, name: "Brina" , x: 400, y: 100, gender: "female", dob: "1900", hasParent: false, hasSpouse: true, spouse1_id: 1 },
{ id: 3, name: "Caden", x: 100, y: 260, gender: "female", dob: "1925", hasParent: true, parent1_id: 1, parent2_id: 2, hasSpouse: false },
{ id: 4, name: "David", x: 200, y: 260, gender: "male", dob: "1930", hasParent: true, parent1_id: 1, parent2_id: 2, hasSpouse: false },
{ id: 5, name: "Ewa", x: 320, y: 260, gender: "female", dob: "1935", hasParent: true, parent1_id: 1, parent2_id: 2, hasSpouse: true, spouse_id: 6 },
{ id: 6, name: "Feivel", x: 450, y: 260, gender: "male", dob: "1935", hasParent: false, hasSpouse: true, spouse_id: 5 },
{ id: 7, name: "Gershon", x: 390, y: 370, gender: "male", dob: "1955", hasParent: true, parent1_id: 5, parent2_id: 6, hasSpouse: false }
];
var links = [
{ source: 0, target: 1 }
];
// Make the viewport automatically adjust to max X and Y values for nodes
var max_x = 0;
var max_y = 0;
for (var i=0; i<nodes.length; i++) {
var temp_x, temp_y;
var temp_x = nodes[i].x + 200;
var temp_y = nodes[i].y + 40;
if ( temp_x >= max_x ) { max_x = temp_x; }
if ( temp_y >= max_y ) { max_y = temp_y; }
}
// Variables
var width = max_x,
height = max_y,
margin = {top: 10, right: 10, bottom: 10, left: 10},
circleRadius = 20,
circleStrokeWidth = 3;
// Basic setup
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("id", "visualization")
.attr("xmlns", "http://www.w3.org/2000/svg");
var elem = svg.selectAll("g")
.data(nodes)
var elemEnter = elem.enter()
.append("g")
.attr("data-name", function(d){ return d.name })
.attr("data-gender", function(d){ return d.gender })
.attr("data-dob", function(d){ return d.dob })
// Draw one circle per node
var circle = elemEnter.append("circle")
.attr("cx", function(d){ return d.x })
.attr("cy", function(d){ return d.y })
.attr("r", circleRadius)
.attr("stroke-width", circleStrokeWidth)
.attr("class", function(d) {
var returnGender;
if (d.gender === "female") { returnGender = "circle female"; }
else if (d.gender === "male") { returnGender = "circle male"; }
else { returnGender = "circle"; }
return returnGender;
});
// Add text to the nodes
elemEnter.append("text")
.attr("dx", function(d){ return (d.x + 28) })
.attr("dy", function(d){ return d.y - 5 })
.text(function(d){return d.name})
.attr("class", "text");
// Add text to the nodes
elemEnter.append("text")
.attr("dx", function(d){ return (d.x + 28) })
.attr("dy", function(d){ return d.y + 16 })
.text(function(d){return "b. " + d.dob})
.attr("class", "text");
// Add links between nodes
var linksEls = svg.selectAll(".link")
.data(links)
.enter()
// Draw the first line (between the primary couple, nodes 0 and 1)
.append("line")
.attr("x1",function(d){ return nodes[d.source].x + circleRadius + circleStrokeWidth; })
.attr("y1",function(d){ return nodes[d.source].y; })
.attr("x2",function(d){ return nodes[d.target].x - circleRadius - circleStrokeWidth; })
.attr("y2",function(d){ return nodes[d.target].y; })
.attr("class","line");
// Draw subsequent lines (from each of the children to the couple line's midpoint)
function drawLines(d){
var x1 = nodes[d.source].x;
var y1 = nodes[d.source].y;
var x2 = nodes[d.target].x;
var y2 = nodes[d.target].y;
var childNodes = nodes.filter(function(d){ return ( (d.hasParent===true) && (d.id!=7) ) });
childNodes.forEach(function(childNode){
svg.append("line")
// This draws from the node *up* to the couple line's midpoint
.attr("x1",function(d){ return childNode.x; })
.attr("y1",function(d){ return childNode.y - circleRadius - circleStrokeWidth + 1; })
.attr("x2",function(d){ return (x1+x2)/2; })
.attr("y2",function(d){ return (y1+y2)/2; })
.attr("class","line2");
})
}
linksEls.each(drawLines);
So, this works okay, kinda, for one generation. The problem is that when it comes time for the next generation (Ewa married to Feivel, child is Gershom) we have to figure out how to replicate a structure with a straight-line between partners and line to the child coming down from the mid-point of the parents' couple line. A related problem is that right now, the first couple is recognized as a couple (different line type) only by virtue of them being the first two pieces of data in my nodes list, as opposed to truly being recognized as such by reading the data (i.e. hasSpouse, spouse1_id, etc.).
Thoughts and ideas to make this work better are much appreciated!
Let all the persons having hasSpouse property value true to have a spouse_id(Instead of spouse1_id or spouse_id) and generate links array from the node array as shown below. couple object is used for preventing redundancy of links like links from 0->1 and 1->0.
var couple = {},
links = [];
nodes.forEach(function(d, i) {
if (d.hasSpouse) {
var link = {};
link["source"] = i;
var targetIdx;
nodes.forEach(function(s, sIdx) {
if (s.id == d.spouse_id) targetIdx = sIdx;
});
link["target"] = targetIdx;
if (!couple[i + "->" + targetIdx] && !couple[targetIdx + "->" + i]) {
couple[i + "->" + targetIdx] = true;
links.push(link);
}
}
});
Now, you will need to make a small change in the code for finding child nodes in your drawLines method. Find the subnodes by matching it's parent ids.
function drawLines(d) {
var src = nodes[d.source];
var tgt = nodes[d.target];
var x1 = src.x, y1 = src.y, x2 = tgt.x, y2 = tgt.y;
var childNodes = nodes.filter(function(d) {
//Code change
return ((d.hasParent === true) && (d.parent1_id == src.id && d.parent2_id == tgt.id))
});
......................
}
Here is the updated fiddle

Categories

Resources