Drawing bezierCurve in d3.js - d3.js

How to draw the line using bezierCurveTo method in d3.js such that lines appears as below image
I have just referred bezier Curve but i'm not getting any idea on it.

There are a lot of ways this could be done. One could make a custom curve that achieves this.
But, we could keep it simpler too. The datum passed to the link generator, such as d3.linkHorizontal, from a d3 layout generally contains a source and target property, each of these usually contain x and y properties. Assuming this structure, we could create a function that uses these and creates and returns the appropriate path data with a bezier curve:
var linker = function(d) {
var x0 = d.source.x;
var y0 = d.source.y;
var y1 = d.target.y;
var x1 = d.target.x;
var k = 120;
var path = d3.path()
path.moveTo(y0,x0)
path.bezierCurveTo(y1-k,x0,y0,x1,y1-k,x1);
path.lineTo(y1,x1);
return path.toString();
}
The above is pretty basic, it uses d3.path but you could easily just construct the SVG path string yourself. There are lots of interactive bezier curve explorers online so you can figure out what control points work best. As the tree layout I've used is vertical, I've turned it horizontal by inverting x and y, which is why my coordinates are [y,x]. I use k above to offset the bezier curve to a small portion of the overall link on the left:
But you could easily toy with this to place the curve in the middle of the link:
Here's it in action:
var data = { "name": "Parent", "children": [
{ "name": "Child A", "children": [ { "name": "Grandchild1"}, {"name":"Grandchild2" } ] },
{ "name": "Child B", }
] };
var width = 400;
var height = 300;
margin = {left: 50, top: 10, right:30, bottom: 10}
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g").attr('transform','translate('+ margin.left +','+ margin.right +')');
var root = d3.hierarchy(data);
var tree = d3.tree()
.size([height-margin.top-margin.bottom,width-margin.left-margin.right]);
var linker = function(d) {
var x0 = d.source.x;
var y0 = d.source.y;
var y1 = d.target.y;
var x1 = d.target.x;
var k = (y1-y0)/2;
var path = d3.path()
path.moveTo(y0,x0)
path.lineTo(y0+k/2,x0)
path.bezierCurveTo(y1-k,x0,y0+k,x1,y1-k/2,x1);
path.lineTo(y1,x1);
return path.toString();
}
var link = g.selectAll(".link")
.data(tree(root).links())
.enter().append("path")
.attr("class", "link")
.attr("d", linker);
var node = g.selectAll(".node")
.data(root.descendants())
.enter().append("g")
.attr("class", function(d) { return "node" + (d.children ? " node--internal" : " node--leaf"); })
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
node.append("circle")
.attr("r", 2.5);
node.append("text")
.text(function(d) { return d.data.name; })
.attr('y',-10)
.attr('x',-10)
.attr('text-anchor','middle');
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
But, in reading the comments I notice that your question may be more about dagreD3 - which changes things considerably. Dagre D3 offers better ease of use relative to D3 at the cost of the some of D3's flexibility. If you want to provide a certain type of curve to DagreD3, then you should use a d3 curve, or some custom curve (as in the linked answer above). You can specify the curve when adding edges easily enough.
But that doesn't solve the issue of edges originating from the same point as in your image. I'll provide a d3 based solution - which probably breaks edge label placement, transitions, etc - so it should be built out a bit if you need that functionality. I'll use the bezier generator from above. The following is inspired by this:
var g = new dagreD3.graphlib.Graph()
.setGraph({rankdir: 'LR'})
.setDefaultEdgeLabel(function() { return {}; });
g.setNode(0, { label: "0"});
g.setNode(1, { label: "1"});
g.setNode(2, { label: "2"});
g.setNode(3, { label: "3"});
g.setNode(4, { label: "4"});
g.setEdge(0, 1);
g.setEdge(0, 2);
g.setEdge(1, 3);
g.setEdge(1, 4);
var render = new dagreD3.render().createEdgePaths(createEdgePaths);
var svg = d3.select("svg"),
svgGroup = svg.append("g"),
zoom = d3.zoom().on("zoom", function() {
svgGroup.attr("transform", d3.event.transform);
});
svg.call(zoom);
render(svgGroup, g);
function createEdgePaths(selection, g, arrows) {
selection.selectAll("g.edgePath")
.data(g.edges())
.enter()
.append("path")
.attr("d", function(e) {
return calcPoints(g,e);
});
}
function calcPoints(g, e) {
var source = g.node(e.v);
var target = g.node(e.w);
var x0 = source.x + source.width/2;
var x1 = target.x - target.width/2;
var y0 = source.y;
var y1 = target.y;
return linker(x0,y0,x1,y1)
}
function linker(x0,y0,x1,y1) {
var dx = x1 -x0;
var k = dx/3;
var path = d3.path()
path.moveTo(x0,y0)
path.bezierCurveTo(x1-k,y0,x0,y1,x1-k,y1);
path.lineTo(x1,y1);
return path.toString();
}
path {
stroke: #333;
stroke-width: 1.5px;
fill: none;
}
rect {
fill: none;
stroke:black;
stroke-width: 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dagre-d3#0.6.1/dist/dagre-d3.min.js"></script>
<svg width="800" height="600"></svg>

Related

D3 transition along segments of path and pause at coordinate values

I would like to be able to click on a circle (coordinate points); bring the marker to the position of the circle and pause at the position of the circle and then resume again along the path.
In addition I would like to activate a circle when marker is paused on them - they are clicked (or their Voronoi cell is clicked). My intention is to have an on click function to an href for the circle coordinates eventually.
I think I need to pass the index of the path coordinates into the translateAlong function instead of the time variables but can't work out how to do this.
I’m not sure if the Voronoi cells are necessary - I tried to add this thinking I could pause my transition and activate my circles with the Voronoi cells. In any case I can’t activate the circle with the Voronoi cell.
I was helped considerably recently on Stackoverflow d3 on click on circle pause and resume transition of marker along line
and I am hoping for assistance again
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>basic_animateBetweenCircles</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
path {
stroke: #848484;
fill: none;
}
circle {
fill: steelblue;
stroke: steelblue;
stroke-width: 3px;
}
.line {
fill: none;
stroke: #FE642E;
stroke-width: 4;
stroke-dasharray: 4px, 8px;
}
.point{
fill:#DF013A;
}
</style>
</head>
<body>
<script>
var width = 960,
height = 500;
var data = [
[480, 200],
[580, 400],
[680, 100],
[780, 300],
[180, 300],
[280, 100],
[380, 400]
];
//check index of path data
for (var i = 0; i < data.length; i++) {
var coordindex = i + " " + data[i];
console.log("Coordindex: " + coordindex);
//return coordindex;
};
var duration = 20000;
var line = d3.line()
.x(function(d) {return (d)[0];})
.y(function(d) {return (d)[1];});
var voronoi = d3.voronoi()
.extent([[0, 0], [width, height]]);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
//path to animate - marker transitions along this path
var path = svg.append("path")
.data([data])
.attr("d", line)
.attr('class', 'line')
.attr("d", function(d) {
return line(d)
});
//voronoi
var voronoiPath = svg.append("g")
.selectAll("path")
.data(voronoi.polygons(data))
.enter().append("path")
.attr("d", polygon)
.on("touchmove mousemove", function() {
d3.select(this)
.style("fill", "purple");
});
//Want to activate circles when marker paused on them / in voronoi cell - intention is to have on click to href
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("class", "point")
.attr("r", 10)
.attr("transform", function(d) { return "translate(" + d + ")"; })
.on('click', function(d, i) {
d3.select(this)
.style("fill", "green");
if (d3.active(this)) {
marker.transition();
setTimeout(function() {
pauseValues.lastTime = pauseValues.currentTime;
//console.log(pauseValues);
}, 100);
} else {
transition();
}
});
var pauseValues = {
lastTime: 0,
currentTime: 0
};
//marker to transition along path
var marker = svg.append("circle")
.attr("r", 19)
.attr("transform", "translate(" + (data[0]) + ")")
.on('click', function(d, i) {
if (d3.active(this)) {
marker.transition();
setTimeout(function() {
pauseValues.lastTime = pauseValues.currentTime;
//console.log(pauseValues);
}, 100);
} else {
transition();
}
});
function transition() {
marker.transition()
.duration(duration - (duration * pauseValues.lastTime))
.attrTween("transform", translateAlong(path.node()))
.on("end", function() {
pauseValues = {
lastTime: 0,
currentTime: 0
};
transition()
});
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
t += pauseValues.lastTime;
var p = path.getPointAtLength(t * l);
pauseValues.currentTime = t;
return "translate(" + p.x + "," + p.y + ")";
};
};
}
function polygon(d) {
return "M" + d.join("L") + "Z";
}
</script>
</body>
If you want to pause at points, I would not run one transition across the entire path. Instead, I would break it up into N transitions, moving from point to point. Before starting the circle on it's next leg, you can pause it for a time. To do this, I would just transition along each line segment with a little algebra:
// copy our data
transData = data.slice();
function transition() {
marker.transition()
.ease(d3.easeLinear)
.duration(duration)
.attrTween("transform", function(){
// get our two points
// slope between them
// and intercetp
var p0 = transData.shift(),
p1 = transData[0];
m = (p0[1] - p1[1]) / (p0[0] - p1[0]),
b = p0[1] - (m * p0[0]),
i = d3.interpolateNumber(p0[0], p1[0]);
// move the point along the line
return function(t){
var x = i(t),
y = m*x + b;
return "translate(" + x + "," + y + ")";
}
})
// one line segment is complete
.on("end", function(){
// if no more movements, stop
if (transData.length <= 1) return;
iter++;
// determine if this is a "pause"
setTimeout(transition, pausePoints.indexOf(iter) !== -1 ? pauseTime : 0);
});
Running code, click a dot to start you can pause a multiple points:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>basic_animateBetweenCircles</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
path {
stroke: #848484;
fill: none;
}
circle {
fill: steelblue;
stroke: steelblue;
stroke-width: 3px;
}
.line {
fill: none;
stroke: #FE642E;
stroke-width: 4;
stroke-dasharray: 4px, 8px;
}
.point {
fill: #DF013A;
}
</style>
</head>
<body>
<script>
var width = 960,
height = 500;
var data = [
[480, 200],
[580, 400],
[680, 100],
[780, 300],
[180, 300],
[280, 100],
[380, 400]
];
var duration = 20000/data.length,
pauseTime = 2000;
var line = d3.line()
.x(function(d) {
return (d)[0];
})
.y(function(d) {
return (d)[1];
});
var voronoi = d3.voronoi()
.extent([
[0, 0],
[width, height]
]);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
//path to animate - marker transitions along this path
var path = svg.append("path")
.data([data])
.attr("d", line)
.attr('class', 'line')
.attr("d", function(d) {
return line(d)
});
//voronoi
var voronoiPath = svg.append("g")
.selectAll("path")
.data(voronoi.polygons(data))
.enter().append("path")
.attr("d", polygon);
//Want to activate circles when marker paused on them / in voronoi cell - intention is to have on click to href
svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("class", "point")
.attr("r", 10)
.attr("transform", function(d) {
return "translate(" + d + ")";
})
.on('click', function(d, i) {
d3.select(this)
.style("fill", "green");
pausePoints.push(i);
if (pausePoints.length === 1)
transition();
});
//marker to transition along path
var marker = svg.append("circle")
.attr("r", 19)
.attr("transform", "translate(" + (data[0]) + ")");
var pausePoints = [],
iter = 0,
transData = data.slice();
function transition() {
marker.transition()
.ease(d3.easeLinear)
.duration(duration)
.attrTween("transform", function(){
var p0 = transData.shift(),
p1 = transData[0];
m = (p0[1] - p1[1]) / (p0[0] - p1[0]),
b = p0[1] - (m * p0[0]),
i = d3.interpolateNumber(p0[0], p1[0]);
return function(t){
var x = i(t),
y = m*x + b;
return "translate(" + x + "," + y + ")";
}
})
.on("end", function(){
if (transData.length <= 1) return;
iter++;
setTimeout(transition, pausePoints.indexOf(iter) !== -1 ? pauseTime : 0);
});
}
function polygon(d) {
return "M" + d.join("L") + "Z";
}
</script>
</body>

D3 Added Circles to a MultiLine Chart - Circles on top of Circles

I'm having an issue where I'm plotting circles on a multi-line chart which has a different color for each line (circles match the colors). The catch is the way I have the function I'm writing circles on top of circles - which is an issue when I try to hide specific ones.
I want to plot circles based on then name but I'm unsure how to limit the above D3 functions to one name only - currently it plots all circles for each line.
Is there a way to use d.name to limit the plotting to one name each time?
thanks
I think d3.nest is what you want:
var names = d3.nest()
.key(function(d){ return d.name; })
.entries(data);
var data = [
{"name":"1.0E-6MHz","value":"20.0","date":"2017-08-25 21:00:00"},{"name":"1.0E-6MHz","value":"93.8","date":"2017-08-25 22:00:00"},{"name":"1.0E-6MHz","value":"86.2","date":"2017-08-25 23:00:00"},{"name":"1.0E-6MHz","value":"79.2","date":"2017-08-26 00:00:00"},{"name":"1.0E-6MHz","value":"81.7","date":"2017-08-26 01:00:00"},{"name":"1.0E-6MHz","value":"76.2","date":"2017-08-26 02:00:00"},{"name":"1.0E-6MHz","value":"86.2","date":"2017-08-26 03:00:00"},{"name":"1.0E-6MHz","value":"89.2","date":"2017-08-26 04:00:00"},{"name":"1.0E-6MHz","value":"91.9","date":"2017-08-26 05:00:00"},{"name":"1.0E-6MHz","value":"78.2","date":"2017-08-26 06:00:00"},{"name":"1.0E-6MHz","value":"86.2","date":"2017-08-26 07:00:00"},{"name":"1.0E-6MHz","value":"82.2","date":"2017-08-26 08:00:00"},{"name":"1.0E-6MHz","value":"96.2","date":"2017-08-26 09:00:00"},{"name":"1.0E-6MHz","value":"88.7","date":"2017-08-26 10:00:00"},{"name":"1.0E-6MHz","value":"92.3","date":"2017-08-26 11:00:00"},{"name":"1.0E-6MHz","value":"80.2","date":"2017-08-26 12:00:00"},{"name":"1.0E-6MHz","value":"76.2","date":"2017-08-26 13:00:00"},{"name":"1.0E-6MHz","value":"93.2","date":"2017-08-26 14:00:00"},{"name":"1.0E-6MHz","value":"89.2","date":"2017-08-26 15:00:00"},{"name":"1.0E-6MHz","value":"79.2","date":"2017-08-26 16:00:00"},{"name":"1.0E-6MHz","value":"90.2","date":"2017-08-26 17:00:00"},{"name":"1.0E-6MHz","value":"85.2","date":"2017-08-26 18:00:00"},{"name":"1.0E-6MHz","value":"86.5","date":"2017-08-26 19:00:00"},{"name":"1.0E-6MHz","value":"76.2","date":"2017-08-26 20:00:00"},{"name":"1.0E-6MHz","value":"94.5","date":"2017-08-26 21:00:00"},
{"name":"2.0E-6MHz","value":"26.2","date":"2017-08-25 21:00:00"},{"name":"2.0E-6MHz","value":"33.8","date":"2017-08-25 22:00:00"},{"name":"2.0E-6MHz","value":"26.2","date":"2017-08-25 23:00:00"},{"name":"2.0E-6MHz","value":"66.2","date":"2017-08-26 00:00:00"},{"name":"2.0E-6MHz","value":"3.7","date":"2017-08-26 01:00:00"},{"name":"2.0E-6MHz","value":"76.2","date":"2017-08-26 02:00:00"},{"name":"2.0E-6MHz","value":"36.2","date":"2017-08-26 03:00:00"},{"name":"2.0E-6MHz","value":"22.2","date":"2017-08-26 04:00:00"},{"name":"2.0E-6MHz","value":"31.6","date":"2017-08-26 05:00:00"},{"name":"2.0E-6MHz","value":"44.2","date":"2017-08-26 06:00:00"},{"name":"2.0E-6MHz","value":"7.2","date":"2017-08-26 07:00:00"},{"name":"2.0E-6MHz","value":"46.2","date":"2017-08-26 08:00:00"},{"name":"2.0E-6MHz","value":"46.2","date":"2017-08-26 09:00:00"},{"name":"2.0E-6MHz","value":"21.7","date":"2017-08-26 10:00:00"},{"name":"2.0E-6MHz","value":"22.3","date":"2017-08-26 11:00:00"},{"name":"2.0E-6MHz","value":"46.2","date":"2017-08-26 12:00:00"},{"name":"2.0E-6MHz","value":"46.2","date":"2017-08-26 13:00:00"},{"name":"2.0E-6MHz","value":"46.2","date":"2017-08-26 14:00:00"},{"name":"2.0E-6MHz","value":"46.2","date":"2017-08-26 15:00:00"},{"name":"2.0E-6MHz","value":"46.2","date":"2017-08-26 16:00:00"},{"name":"2.0E-6MHz","value":"96.2","date":"2017-08-26 17:00:00"},{"name":"2.0E-6MHz","value":"46.2","date":"2017-08-26 18:00:00"},{"name":"2.0E-6MHz","value":"33.5","date":"2017-08-26 19:00:00"},{"name":"2.0E-6MHz","value":"46.2","date":"2017-08-26 20:00:00"},{"name":"2.0E-6MHz","value":"44.5","date":"2017-08-26 21:00:00"},
{"name":"3.0E-6MHz","value":"38.2","date":"2017-08-25 21:00:00"},{"name":"3.0E-6MHz","value":"43.8","date":"2017-08-25 22:00:00"},{"name":"3.0E-6MHz","value":"56.2","date":"2017-08-25 23:00:00"},{"name":"3.0E-6MHz","value":"46.2","date":"2017-08-26 00:00:00"},{"name":"3.0E-6MHz","value":"53.7","date":"2017-08-26 01:00:00"},{"name":"3.0E-6MHz","value":"3.2","date":"2017-08-26 02:00:00"},{"name":"3.0E-6MHz","value":"46.2","date":"2017-08-26 03:00:00"},{"name":"3.0E-6MHz","value":"66.2","date":"2017-08-26 04:00:00"},{"name":"3.0E-6MHz","value":"37.9","date":"2017-08-26 05:00:00"},{"name":"3.0E-6MHz","value":"42.2","date":"2017-08-26 06:00:00"},{"name":"3.0E-6MHz","value":"4.2","date":"2017-08-26 07:00:00"},{"name":"3.0E-6MHz","value":"46.2","date":"2017-08-26 08:00:00"},{"name":"3.0E-6MHz","value":"46.2","date":"2017-08-26 09:00:00"},{"name":"3.0E-6MHz","value":"21.7","date":"2017-08-26 10:00:00"},{"name":"3.0E-6MHz","value":"22.3","date":"2017-08-26 11:00:00"},{"name":"3.0E-6MHz","value":"46.2","date":"2017-08-26 12:00:00"},{"name":"3.0E-6MHz","value":"46.2","date":"2017-08-26 13:00:00"},{"name":"3.0E-6MHz","value":"46.2","date":"2017-08-26 14:00:00"},{"name":"3.0E-6MHz","value":"46.2","date":"2017-08-26 15:00:00"},{"name":"3.0E-6MHz","value":"46.2","date":"2017-08-26 16:00:00"},{"name":"3.0E-6MHz","value":"96.2","date":"2017-08-26 17:00:00"},{"name":"3.0E-6MHz","value":"46.2","date":"2017-08-26 18:00:00"},{"name":"3.0E-6MHz","value":"33.5","date":"2017-08-26 19:00:00"},{"name":"3.0E-6MHz","value":"46.2","date":"2017-08-26 20:00:00"},{"name":"3.0E-6MHz","value":"34.5","date":"2017-08-26 21:00:00"},
{"name":"4.0E-6MHz","value":"46.2","date":"2017-08-25 21:00:00"},{"name":"4.0E-6MHz","value":"53.8","date":"2017-08-25 22:00:00"},{"name":"4.0E-6MHz","value":"86.2","date":"2017-08-25 23:00:00"},{"name":"4.0E-6MHz","value":"56.2","date":"2017-08-26 00:00:00"},{"name":"4.0E-6MHz","value":"23.7","date":"2017-08-26 01:00:00"},{"name":"4.0E-6MHz","value":"16.2","date":"2017-08-26 02:00:00"},{"name":"4.0E-6MHz","value":"76.2","date":"2017-08-26 03:00:00"},{"name":"4.0E-6MHz","value":"82.2","date":"2017-08-26 04:00:00"},{"name":"4.0E-6MHz","value":"39.9","date":"2017-08-26 05:00:00"},{"name":"4.0E-6MHz","value":"6.2","date":"2017-08-26 06:00:00"},{"name":"4.0E-6MHz","value":"22.2","date":"2017-08-26 07:00:00"},{"name":"4.0E-6MHz","value":"46.2","date":"2017-08-26 08:00:00"},{"name":"4.0E-6MHz","value":"46.2","date":"2017-08-26 09:00:00"},{"name":"4.0E-6MHz","value":"21.7","date":"2017-08-26 10:00:00"},{"name":"4.0E-6MHz","value":"22.3","date":"2017-08-26 11:00:00"},{"name":"4.0E-6MHz","value":"46.2","date":"2017-08-26 12:00:00"},{"name":"4.0E-6MHz","value":"46.2","date":"2017-08-26 13:00:00"},{"name":"4.0E-6MHz","value":"46.2","date":"2017-08-26 14:00:00"},{"name":"4.0E-6MHz","value":"46.2","date":"2017-08-26 15:00:00"},{"name":"4.0E-6MHz","value":"46.2","date":"2017-08-26 16:00:00"},{"name":"4.0E-6MHz","value":"96.2","date":"2017-08-26 17:00:00"},{"name":"4.0E-6MHz","value":"46.2","date":"2017-08-26 18:00:00"},{"name":"4.0E-6MHz","value":"33.5","date":"2017-08-26 19:00:00"},{"name":"4.0E-6MHz","value":"46.2","date":"2017-08-26 20:00:00"},{"name":"4.0E-6MHz","value":"24.5","date":"2017-08-26 21:00:00"},
{"name":"5.0E-6MHz","value":"66.2","date":"2017-08-25 21:00:00"},{"name":"5.0E-6MHz","value":"63.8","date":"2017-08-25 22:00:00"},{"name":"5.0E-6MHz","value":"16.2","date":"2017-08-25 23:00:00"},{"name":"5.0E-6MHz","value":"86.2","date":"2017-08-26 00:00:00"},{"name":"5.0E-6MHz","value":"13.7","date":"2017-08-26 01:00:00"},{"name":"5.0E-6MHz","value":"36.2","date":"2017-08-26 02:00:00"},{"name":"5.0E-6MHz","value":"6.2","date":"2017-08-26 03:00:00"},{"name":"5.0E-6MHz","value":"21.2","date":"2017-08-26 04:00:00"},{"name":"5.0E-6MHz","value":"41.1","date":"2017-08-26 05:00:00"},{"name":"5.0E-6MHz","value":"86.2","date":"2017-08-26 06:00:00"},{"name":"5.0E-6MHz","value":"69.2","date":"2017-08-26 07:00:00"},{"name":"5.0E-6MHz","value":"46.2","date":"2017-08-26 08:00:00"},{"name":"5.0E-6MHz","value":"46.2","date":"2017-08-26 09:00:00"},{"name":"5.0E-6MHz","value":"21.7","date":"2017-08-26 10:00:00"},{"name":"5.0E-6MHz","value":"22.3","date":"2017-08-26 11:00:00"},{"name":"5.0E-6MHz","value":"46.2","date":"2017-08-26 12:00:00"},{"name":"5.0E-6MHz","value":"46.2","date":"2017-08-26 13:00:00"},{"name":"5.0E-6MHz","value":"46.2","date":"2017-08-26 14:00:00"},{"name":"5.0E-6MHz","value":"46.2","date":"2017-08-26 15:00:00"},{"name":"5.0E-6MHz","value":"46.2","date":"2017-08-26 16:00:00"},{"name":"5.0E-6MHz","value":"96.2","date":"2017-08-26 17:00:00"},{"name":"5.0E-6MHz","value":"46.2","date":"2017-08-26 18:00:00"},{"name":"5.0E-6MHz","value":"33.5","date":"2017-08-26 19:00:00"},{"name":"5.0E-6MHz","value":"46.2","date":"2017-08-26 20:00:00"},{"name":"5.0E-6MHz","value":"4.5","date":"2017-08-26 21:00:00"}
];
// parsing
data.forEach(function(d){
d.value = +d.value;
d.date = new Date(d.date);
})
// after this you will have 5 name keys of its values(in your case)
var names = d3.nest()
.key(function(d){ return d.name; })
.entries(data);
// console.log(names)
var chart = d3.select("#chart");
d3.select("#names")
.selectAll("button")
.data(names.map(function(d){ return d.key; }))
.enter()
.append("button")
.text(function(d){ return d; })
.on("click", redraw);
var svgWidth = 500,
svgHeight = 400,
radius = 5,
margin = { top: 30, right: 30, bottom: 30, left: 30 },
width = svgWidth - margin.right - margin.left,
height = svgHeight - margin.top - margin.bottom;
var xScale = d3.time.scale().range([0, width]),
yScale = d3.scale.linear().range([0, height]),
xAxis = d3.svg.axis().orient("bottom").scale(xScale),
yAxis = d3.svg.axis().orient("left");
var svg = chart.append("svg").attr({ width: svgWidth, height: svgHeight });
var gMain = svg.append("g").attr({
class: "gMain",
transform: "translate(" + [margin.left, margin.top] + ")"
}),
gYAxis = gMain.append("g").attr("class", "axis yaxis"),
gXAxis = gMain.append("g").attr({
class: "axis xaxis",
transform: "translate(0," + height + ")"
})
gPlot = gMain.append("g").attr({
class: "plot",
transform: "translate(0," + height + ")"
});
redraw("1.0E-6MHz", 0);
function redraw(name, index){
var points = names[index].values;
xScale.domain(d3.extent(points, function(d){ return d.date; }));
yScale.domain(d3.extent(points, function(d){ return d.value; }));
gXAxis.transition().call(xAxis);
gYAxis.transition().call(yAxis.scale(yScale.copy().range([height, 0])));
var update = gPlot.selectAll("circle").data(points),
enter = update.enter()
.append("circle")
.attr({
class: "circle",
r: radius,
fill: "steelblue",
cx: function(d){ return xScale(d.date); },
cy: function(d){ return -yScale(d.value); }
});
update.exit().remove();
update.transition()
.duration(700)
.attr({
cx: function(d){ return xScale(d.date); },
cy: function(d){ return -yScale(d.value); }
});
}
.axis path{
fill: none;
stroke: black;
stroke-width: 1;
}
.axis line{
stroke: black;
stroke-width: 1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="names"></div>
<div id="chart"></div>

Removing the D3 brush on button click

I'm wrestling with a problem of a brush not being removed correctly on a bar chart. You can see the Bl.ock here and see what's not working correctly.
In short, the brush highlights the bars that have been selected by the brush, as well as snaps to the edge of the rect to make selecting spans of time easier (there's a secondary bug here where the brush snapping isn't quite mapping correctly to the dates -- you'll see this if you try to draw the brush up to the edge of the barchart). Somewhere along the way (maybe with the rect snapping?) the background click-to-remove-brush feature stopped working (it now selects a single year span, although doesn't highlight the rect correctly). To make it easier for users, I wanted to add a button that a user can click to remove the brush when they're done (the resetBrush() function below).
My understanding was the brush selection can be cleared with brush.extent(), but when you clear the extent you then have to redraw the brush. I thought I was doing that correctly, but alas, I'm running into some problem somewhere that I can't seem to track down. Any pointers on where I'm tripping up would be greatly appreciated!
Code:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font-family: sans-serif;
color: #000;
text-rendering: optimizeLegibility;
}
.barchart {
z-index: 30;
display: block;
visibility: visible;
position: relative;
padding-top: 15px;
margin-top: 15px;
}
.axis {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
.resize path {
fill: #666;
fill-opacity: .8;
stroke: #000;
stroke-width: 1.5px;
}
.brush .extent {
stroke: #fff;
stroke-opacity: .6;
stroke-width: 2px;
fill-opacity: .1;
shape-rendering: crispEdges;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/d3.geo.projection.v0.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
brushYearStart = 1848;
brushYearEnd = 1905;
// Scales
var x = d3.scale.ordinal().rangeRoundBands([0, width - 60], .1);
var y = d3.scale.linear().range([height, 0]);
// Prepare the barchart canvas
var barchart = d3.select("body").append("svg")
.attr("class", "barchart")
.attr("width", "100%")
.attr("height", height + margin.top + margin.bottom)
.attr("y", height - height - 100)
.append("g");
var z = d3.scale.ordinal().range(["steelblue", "indianred"]);
var brushYears = barchart.append("g")
brushYears.append("text")
.attr("id", "brushYears")
.classed("yearText", true)
.text(brushYearStart + " - " + brushYearEnd)
.attr("x", 35)
.attr("y", 12);
d3.csv("years_count.csv", function (error, post) {
// Coercion since CSV is untyped
post.forEach(function (d) {
d["frequency"] = +d["frequency"];
d["frequency_discontinued"] = +d["frequency_discontinued"];
d["year"] = d3.time.format("%Y").parse(d["year"]).getFullYear();
});
var freqs = d3.layout.stack()(["frequency", "frequency_discontinued"].map(function (type) {
return post.map(function (d) {
return {
x: d["year"],
y: +d[type]
};
});
}));
x.domain(freqs[0].map(function (d) {
return d.x;
}));
y.domain([0, d3.max(freqs[freqs.length - 1], function (d) {
return d.y0 + d.y;
})]);
// Axis variables for the bar chart
x_axis = d3.svg.axis().scale(x).tickValues([1850, 1855, 1860, 1865, 1870, 1875, 1880, 1885, 1890, 1895, 1900]).orient("bottom");
y_axis = d3.svg.axis().scale(y).orient("right");
// x axis
barchart.append("g")
.attr("class", "x axis")
.style("fill", "#000")
.attr("transform", "translate(0," + height + ")")
.call(x_axis);
// y axis
barchart.append("g")
.attr("class", "y axis")
.style("fill", "#000")
.attr("transform", "translate(" + (width - 85) + ",0)")
.call(y_axis);
// Add a group for each cause.
var freq = barchart.selectAll("g.freq")
.data(freqs)
.enter().append("g")
.attr("class", "freq")
.style("fill", function (d, i) {
return z(i);
})
.style("stroke", "#CCE5E5");
// Add a rect for each date.
rect = freq.selectAll("rect")
.data(Object)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function (d) {
return x(d.x);
})
.attr("y", function (d) {
return y(d.y0) + y(d.y) - height;
})
.attr("height", function (d) {
return height - y(d.y);
})
.attr("width", x.rangeBand())
.attr("id", function (d) {
return d["year"];
});
// Draw the brush
brush = d3.svg.brush()
.x(x)
.on("brush", brushmove)
.on("brushend", brushend);
var arc = d3.svg.arc()
.outerRadius(height / 15)
.startAngle(0)
.endAngle(function(d, i) { return i ? -Math.PI : Math.PI; });
brushg = barchart.append("g")
.attr("class", "brush")
.call(brush);
brushg.selectAll(".resize").append("path")
.attr("transform", "translate(0," + height / 2 + ")")
.attr("d", arc);
brushg.selectAll("rect")
.attr("height", height);
});
// ****************************************
// Brush functions
// ****************************************
function brushmove() {
y.domain(x.range()).range(x.domain()).clamp(true);
b = brush.extent();
brushYearStart = Math.ceil(y(b[0]));
brushYearEnd = Math.ceil(y(b[1]));
// Snap to rect edge
d3.select("g.brush").call(brush.extent([y.invert(brushYearStart), y.invert(brushYearEnd)]));
// Fade all years in the histogram not within the brush
d3.selectAll("rect.bar").style("opacity", function (d, i) {
return d.x >= brushYearStart && d.x < brushYearEnd ? "1" : ".4"
});
}
function brushend() {
// Additional calculations happen here...
// filterPoints();
// colorPoints();
// styleOpacity();
// Update start and end years in upper right-hand corner of the map
d3.select("#brushYears").text(brushYearStart + " - " + brushYearEnd);
}
function resetBrush() {
d3.selectAll(".brush").remove();
d3.selectAll("brushg.resize").remove();
brush.clear();
brushg.call(brush);
}
</script>
<div id="resetMap">
<button
id="returnBrush"
class="btn btn-default"
onclick="resetBrush()"/>Remove Brush
</div>
</body>
</html>
If you execute d3.selectAll(".brush").remove(); you remove <g class="brush"></g> and his childs.
This d3.selectAll("brushg.resize").remove(); is a bug. Must to be brushg.selectAll(".resize").remove(); but is the same case that d3.selectAll(".brush").remove();.
You have to do this:
For reset the brush.extent() and fire the brush event.
function resetBrush() {
brush
.clear()
.event(d3.select(".brush"));
}
For reset #brushYears to the initial state
function brushend() {
var localBrushYearStart = (brush.empty()) ? brushYearStart : Math.ceil(y(b[0])),
localBrushYearEnd = (brush.empty()) ? brushYearEnd : Math.ceil(y(b[1]));
// Update start and end years in upper right-hand corner of the map
d3.select("#brushYears").text(localBrushYearStart + " - " + localBrushYearEnd);
}
For reset to initial values on brush event
function brushmove() {
y.domain(x.range()).range(x.domain()).clamp(true);
b = brush.extent();
3.1. To set the localBrushYearStart and localBrushYearEnd variables to initial state on brush.empty() or set to Math.ceil(brush.extent()))
var localBrushYearStart = (brush.empty()) ? brushYearStart : Math.ceil(y(b[0])),
localBrushYearEnd = (brush.empty()) ? brushYearEnd : Math.ceil(y(b[1]));
3.2. To execute brush.extent() on selection, or brush.clear() on brush.empty()
// Snap to rect edge
d3.select("g.brush").call((brush.empty()) ? brush.clear() : brush.extent([y.invert(localBrushYearStart), y.invert(localBrushYearEnd)]));
3.3. To set opacity=1 years on brush.empty() or selection, and opacity=.4 on not selected years
// Fade all years in the histogram not within the brush
d3.selectAll("rect.bar").style("opacity", function(d, i) {
return d.x >= localBrushYearStart && d.x < localBrushYearEnd || brush.empty() ? "1" : ".4";
});
}
Check the corrections on my BL.OCKS
Just do this
function resetBrush() {
d3.select("g.brush").call(brush.extent([0, 0]))
d3.selectAll("rect.bar").style("opacity", "0.4");
//reset year labels at top
}

D3 Voronoi Edge Condition

I'm working with a D3 example file for a force directed voronoi graph... however, I mainly just needed a simplified version with only three vertices... so I simplified the file and have included a JSFiddle example of where my file stands currently. My issue is how the edge condition is handled. Right now, the voronoi edges extend out to the edge of the container div. However, I'd like to clip each voronoi cell by a circle boundary. I've included two images to help explain my problem. The first image shows the script as it exists now, whereas the second is made with photoshop - showing the circular clipping boundary. It seems like D3's polygon.clip method would be the best option, but I don't really know how to implement the clipping method into my script. Any suggestions would be greatly appreciated.
<!DOCTYPE html>
<html>
<head>
<title>Voronoi Diagram with Force Directed Nodes and Delaunay Links</title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<script src="http://d3js.org/d3.v3.min.js"></script>
<style type="text/css">
path
{
stroke: #EFEDF5;
stroke-width: 4px;
}
</style>
</head>
<body>
<div id="chart">
</div>
<script type="text/javascript">
var w = window.innerWidth > 960 ? 960 : (window.innerWidth || 960),
h = window.innerHeight > 500 ? 500 : (window.innerHeight || 500),
radius = 5.25,
links = [],
simulate = true,
zoomToAdd = true,
cc = ["#FFA94A","#F58A3A","#F85A19"]
var numVertices = (w*h) / 200000;
var vertices = d3.range(numVertices).map(function(i) {
angle = radius * (i+10);
return {x: angle*Math.cos(angle)+(w/2), y: angle*Math.sin(angle)+(h/2)};
});
var d3_geom_voronoi = d3.geom.voronoi().x(function(d) { return d.x;}).y(function(d) { return d.y; })
var prevEventScale = 1;
var zoom = d3.behavior.zoom().on("zoom", function(d,i) {
if (zoomToAdd){
if (d3.event.scale > prevEventScale) {
angle = radius * vertices.length;
}
force.nodes(vertices).start()
} else {
if (d3.event.scale > prevEventScale) {
radius+= .01
} else {
radius -= .01
}
vertices.forEach(function(d, i) {
angle = radius * (i+10);
vertices[i] = {x: angle*Math.cos(angle)+(w/2), y: angle*Math.sin(angle)+(h/2)};
});
force.nodes(vertices).start()
}
prevEventScale = d3.event.scale;
});
d3.select(window)
.on("keydown", function() {
// shift
if(d3.event.keyCode == 16) {
zoomToAdd = false
}
})
.on("keyup", function() {
zoomToAdd = true
})
var svg = d3.select("#chart")
.append("svg")
.attr("width", w)
.attr("height", h)
.call(zoom)
var force = d3.layout.force()
.charge(-300)
.size([w, h])
.on("tick", update);
force.nodes(vertices).start();
var path = svg.selectAll("path");
function update(e) {
path = path.data(d3_geom_voronoi(vertices))
path.enter().append("path")
// drag node by dragging cell
.call(d3.behavior.drag()
.on("drag", function(d, i) {
vertices[i] = {x: vertices[i].x + d3.event.dx, y: vertices[i].y + d3.event.dy}
})
)
.style("fill", function(d, i) { return cc[0] })
path.attr("d", function(d) { return "M" + d.join("L") + "Z"; })
.transition().duration(150)
.style("fill", function(d, i) { return cc[i] })
path.exit().remove();
if(!simulate) force.stop()
}

How to draw a line / link between two points on a D3 map based on latitude / longitude?

I am attempting to create a map of the 10 major NASA facilities in D3. I have successfully generated the base United States map and appended NASA logos at each one of the center locations based on a .csv with latitude and longitude. However, I cannot figure out any elegant way to draw lines / links / arcs / connections between the points on the map.
In the code below, I have drawn a line between GSFC and KSC (using the 'var = places', 'var = route', and 'svg.append("path")') but it is on an SVG layer, so it is on top of the logos (which looks awful) and does not scale (or go away would be fine, too) when clicking to zoom in on a state. I would like to be able to draw links between the centers based on the latitude and longitude data from the .csv.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.background {
fill: none;
pointer-events: all;
}
#states {
fill: #aaaaaa;
}
#states .active {
fill: #ff0000;
fill-opacity: .5;
}
#state-borders {
fill: none;
stroke: #ffffff;
stroke-width: 1.5px;
stroke-linejoin: round;
stroke-linecap: round;
pointer-events: none;
}
path.link {
fill: none;
stroke: #666666;
stroke-width: 1.5px;
}
.stroke {
fill: none;
stroke: #000;
stroke-width: 3px;
}
.fill {
fill: #fff;
}
.graticule {
fill: none;
stroke: #777;
stroke-width: .5px;
stroke-opacity: .5;
}
.route {
fill: none;
stroke: blue;
stroke-width: 3px;
}
</style>
<body>
<h2>
<span>NASA Centers</span>
</h2>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/d3.geo.projection.v0.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script>
var width = 1000,
height = 600,
centered;
var projection = d3.geo.albersUsa()
.scale(1070)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var graticule = d3.geo.graticule();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var g = svg.append("g");
var places = {
GSFC: [-76.852587, 38.991621],
KSC: [-80.650813, 28.524963]
};
var route = {
type: "LineString",
coordinates: [
places.GSFC,
places.KSC
]
};
var point = svg.append("g")
.attr("class", "points")
.selectAll("g")
.data(d3.entries(places))
.enter().append("g")
.attr("transform", function(d) { return "translate(" + projection(d.value) + ")"; });
point.append("text")
.attr("y", 5)
.attr("dx", "1em")
.text(function(d) { return d.key; });
d3.json("us.json", function(error, us) {
g.append("g")
.attr("id", "states")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path)
.on("click", clicked);
g.append("path")
.datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))
.attr("id", "state-borders")
.attr("d", path);
d3.csv("nasacenters.csv", function(error, data) {
g.selectAll("image").data([0])
.data(data)
.enter()
.append("image")
.attr("xlink:href", "nasalogo.png")
.attr("width", "30")
.attr("height", "30")
.attr("x", function(d) {
return projection([d.lon, d.lat])[0]-15;
})
.attr("y", function(d) {
return projection([d.lon, d.lat])[1]-15;
})
svg.append("path")
.datum(route)
.attr("class", "route")
.attr("d", path)
.style("opacity", 0.5);
});
});
function clicked(d) {
var x, y, k;
if (d && centered !== d) {
var centroid = path.centroid(d);
x = centroid[0];
y = centroid[1];
k = 4;
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
g.selectAll("path")
.classed("active", centered && function(d) { return d === centered; });
g.transition()
.duration(750)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
.style("stroke-width", 1.5 / k + "px");
}
</script>
</body>
</html>
The .csv file is in the following format:
code,center,lat,lon
GSFC,Goddard Space Flight Center,38.991621,-76.852587
KSC,Kennedy Space Center,28.524963,-80.650813
JPL,Jet Propulsion Laboratory,34.200463,-118.176008
DFRC,Dryden Flight Research Center,34.613714,-118.076790
GRC,Glenn Research Center,41.415891,-81.861774
MSFC,Marshall Space Flight Center,34.646554,-86.674368
ARC,Ames Research Center,37.409574,-122.064292
LaRC,Langley Research Center,37.092123,-76.376230
JSC,Johnson Space Center,29.551508,-95.092256
SSC,Stennis Space Center,30.363692,-89.600036
I modified your example slightly based on the problems you described: http://bl.ocks.org/erikhazzard/6201948
It looks like there are three issues:
Paths draw on top of icon. To fix this, you can change the order of when you add items to the group, or add sub groups to your main g group, ensuring the order that you add the groups matches the order you want things to appear.
The paths between points doesn't zoom when you zoom the map. To fix this, make sure to add everything to the group that you're modifying the clicked() function. In this case, your g group is being zoomed on, so if you add the paths to the g group instead of the svg directly the paths will zoom as well. In the example provided, text does also not zoom in - that's because it's added directly to the SVG and not the g group that is being transformed.
Paths aren't created automatically from the data. To fix this, you can generate an array containing LineString objects from the data. For example,
for(var i=0, len=data.length-1; i<len; i++){
// (note: loop until length - 1 since we're getting the next
// item with i+1)
links.push({
type: "LineString",
coordinates: [
[ data[i].lon, data[i].lat ],
[ data[i+1].lon, data[i+1].lat ]
]
});
}
Then, do the standard data join pattern and pass in the links list to the data. When you pass in path as the d attribute, it will generate a great arc based on the coordinates for each item:
// Standard enter / update
var pathArcs = arcGroup.selectAll(".arc")
.data(links);
//enter
pathArcs.enter()
.append("path").attr({
'class': 'arc'
}).style({
fill: 'none',
});
//update
pathArcs.attr({
//d is the points attribute for this path, we'll draw
// an arc between the points using the arc function
d: path
})
.style({
stroke: '#0000ff',
'stroke-width': '2px'
})
In my example ( http://bl.ocks.org/enoex/6201948 ) I added a transition on the great arc paths to illustrate how the path is drawn based on the order of coordinate pairs passed into the links object.
Hope that helps!

Resources