d3j snake curved path animation - javascript

I am interested in trying to create a controlled curved path. Is there a way to plot specific coordinates and styling to mimic something like this design. I imagine it as a kind of 2D Donnie Darko time tunnel or slinkey/snake.
update 1 - journey path 1
http://jsfiddle.net/0ht35rpb/241/
update 2 - journey 2
** I've given it a softer look with stroke-linecap: round -- http://jsfiddle.net/0ht35rpb/243/
update 3 - journey 3
http://jsfiddle.net/0ht35rpb/245/
^ I've started to create multiple path lines - be good to organise this so its easier to make/control
-- essentially the journey will need to consist of the key gates to pass and corners -- and maybe have different colors/speeds to take on.
update 4- journey 4 - 18/10/2017
I've upgraded this to v4 - and made a getCoord function - so the journeys can be made and ran from a series of ids
http://jsfiddle.net/0ht35rpb/257/
I've adapted some path animation code - but I am not sure how to control or modify the path to hit specific coordinates.
//animation curved path.
http://jsfiddle.net/0ht35rpb/217/
//static curved path
http://jsfiddle.net/0ht35rpb/215/
//dot plots
http://jsfiddle.net/0ht35rpb/222/
How would I draw a line from do1 to dot3 -- or animate a curved path following multiple dot points?
var width = 600;
var height = 400;
var bezierLine = d3.svg.line()
.x(function(d) { return d[0]; })
.y(function(d) { return d[1]; })
.interpolate("basis");
var svg = d3.select("#bezier-demo")
.append("svg")
.attr("width", width)
.attr("height", height);
svg.append('path')
.attr("d", bezierLine([[0, 40], [25, 70], [50, 100], [100, 50], [150, 20], [200, 130], [300, 120]]))
.attr("stroke", "red")
.attr("stroke-width", 1)
.attr("fill", "none")
.transition()
.duration(2000)
.attrTween("stroke-dasharray", function() {
var len = this.getTotalLength();
return function(t) { return (d3.interpolateString("0," + len, len + ",0"))(t) };
});

I made a simple js fiddle where i have 3 point with a curve. When you click it add a point to the curve and transition to it:
https://jsfiddle.net/cs00L0ok/
here is the onclick that add the new point
svg.on("click", function (d) {
// add a nex anchor point
circle_data.push({
x: d3.event.x,
y: d3.event.y
});
d3.select("path")
.transition()
.duration(2000)
.attr("d", bezierLine(circle_data))
})
I let you look into the jsfidddle to see the transition to the new point.
You can see how i control my path. I hope that help you. come back to me if you have answer / want more info

Related

Draw an arc between two points

I'm working on a proof of concept for an application that I think D3 might be a good fit for. Since I'm new to D3 I thought I would start off simple and build my way up to the application requirements. However, I seem to be hitting a snag on what I believe should be a very easy task with this library. I want to place two small circles on an SVG and then draw an arc or curve between them. Based on the documentation, I believe arcTo would be the best fit for this since I know the start and end points. For the life of me I cannot get it to draw the arc. The circles are drawn perfectly every time though.
var joints = [{x : 100, y : 200, r : 5},
{x : 150, y : 150, r : 5}];
var svg = d3.select("svg");
svg.selectAll("circle")
.data(joints)
.enter().append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", function(d) { return d.r; });
svg.selectAll("path").append("path").arcTo(100,200,150,150,50)
.attr("class", "link");
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="960" height="800" />
I'm either going about this the wrong way or I don't fully understand how to append a path to an SVG. Can someone point me in the right direction? I haven't been able to find many examples of arcTo. Thank you!
You misunderstood what d3.path() is. According to the API:
The d3-path module lets you take [a HTML Canvas] code and additionally render to SVG.
And for d3.path():
d3.path(): Constructs a new path serializer that implements CanvasPathMethods.
As you can see, the d3-path module has only a bunch of methods that allow you to take a HTML canvas code and use it to draw SVG elements.
That being said, you cannot use arcTo straight away in the SVG, as you are doing right now. It should be:
var path = d3.path();
path.moveTo(100, 200);
path.arcTo(100,200,150,150,50)
... and then:
svg.append("path")
.attr("d", path.toString())
However, as an additional problem, arcTo is more complicated than that: the first two values are not the x and y of the starting point, but the coordinates of the first tangent.
Here is a demo, using different values for arcTo, which I think is what you want:
var joints = [{
x: 100,
y: 200,
r: 5
}, {
x: 150,
y: 150,
r: 5
}];
var svg = d3.select("svg");
svg.selectAll("circle")
.data(joints)
.enter().append("circle")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.r;
});
var path = d3.path();
path.moveTo(100, 200);
path.arcTo(100, 150, 150, 150, 50);
svg.append("path")
.attr("d", path.toString())
.attr("stroke", "firebrick")
.attr("stroke-width", 2)
.attr("fill", "none");
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="400" height="250" />
An easy alternative is simply dropping the d3.path() and doing all this using just SVG code. There are plenty of examples showing how to draw an SVG arc from point A to point B with a given radius.

How do make my plot points smooth in d3.js using projection?

I'm able to plot some weather data onto a map using the following code. However the points are rectangles and i'd like to get them smoother.
,
I'd like to plot them smoother like something similar to
I believe I need to look into interpolating, spatial analysis, and/or Choropleth maps. I think they are different algorithms in doing this. I feel like i need to fill in more points in between the existing ones? And with that is it possible to make gradient like points? Is this doable in D3? Or should i consider using three.js or WebGL stuff?
var width = 960,
height = 960;
var map = {};
var projection = d3.geo.mercator()
.scale((width + 1) / 2 / Math.PI)
.translate([width / 2, height / 2])
.precision(.1);
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);
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
d3.json("world-50m.json", function(error, world) {
svg.insert("path", ".graticule")
.datum(topojson.feature(world, world.objects.land))
.attr("class", "land")
.attr("d", path);
svg.insert("path", ".graticule")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; }))
.attr("class", "boundary")
.attr("d", path);
});
map.plot_points = [];
map.max = 30;
map.min = -1;
var opacity = d3.scale.linear()
.domain([map.min, map.max])
.range([0,1]);
var rainbow = ["#CE0C82", "#800CCE", "#1F0CCE", "#0C5BCE", "#0C99CE", "#2ECE0C", "#BAE806", "#FEFF00", "#FFCD00", "#FF9A00", "#FF6000", "#FF0000"];
zs.forEach(function(zv,zi){
zv.forEach(function(zzv, zzi){
if(zzv != 999)
{
map.plot_points.push({lat: ys[zi], long:xs[zzi],value:zzv});
}
})
});
console.log(map);
var points = svg.selectAll("rects.points")
.data(map.plot_points)
.enter()
.append("rect")
.attr("class", "points")
.style("fill", function(d) {
var scale = d3.scale.linear().domain([map.min, map.max]).range([1, rainbow.length]);
return rainbow[Math.round(scale(d.value))];
}).attr("width", 8)
.attr("height", 8)
.style("fill-opacity", 1)
.attr("transform", function(d) {
return "translate(" + projection([d.long, d.lat]) + ")";
})
It sounds like the problem in your case is the data. What you would need to do is take the original data and interpolate it to a smoother form. For this, you can use a GIS program such as QGIS. How exactly to do that depends on what format your original data is in.
Once you have the smoother data, you can plot it again in D3. My guess is that the end result would be somewhat similar to what I've done here, where contour lines are drawn to much the same effect as what you're aiming for.
Maybe you could take a look into heatmap js.
http://www.patrick-wied.at/static/heatmapjs/
Although is point based it may give you a hint.
It uses canvas instead of svg.
Jason Davies wrote an implementation of the conrec algorithm that does exactly what you need:
https://github.com/jasondavies/conrec.js
It's got a working example inside
I believe that the white stripes are happening because of the projection you are using;
In fact the height of each rectangle should adjust accordingly going north and south from the Equator, because the Mercator projection alters the distance going north and south.
To have a fixed height of the rectangles you could try with this projection instead:
http://bl.ocks.org/mbostock/3757119
which preserves the dimension going north and south

Manipulating D3 axis labels

I'm working with D3 for the first time and I'm trying to make a parallel coordinate graph. I basically am using this demo. The only real changes I've had is changing the data and changing the far right axis so it has strings instead of numbers as the labels. I do this by using the following:
if(d === "Dog Breed") {
y[d] = d3.scale.ordinal()
.domain(dogData.map(function(p) { return p[d]; }))
.rangePoints([h, 0]); // quantitative color scale
}
Unfortunately, if the dog's breed is too long, the text gets cut off, making it hard to read the label (one has to move the axis in its entirety to read it, but when they let go of it, it goes right back to where it was initially).
My other change were the following:
var m = [30, 10, 10, 10],
w = screen.width - 150, // Make it 150px less than the screen's width.
h = 500 - m[0] - m[2];
The axis label code remains the same at:
// Add an axis and title.
g.append("svg:g")
.attr("class", "axis")
.each(function(d) { d3.select(this).call(axis.scale(y[d])); })
.append("svg:text")
.attr("text-anchor", "middle")
.attr("y", -9)
.text(String);
Is there any way to avoid the name-being-clipped-thing? Even shifting the graph itself over in its block about 20px would help, but I don't know where the code for that would be...
The fix was to manipulate var m to have more on the lefthand side in m[3].

D3 - Pie Chart & Force Directed Labels

I'm looking to create a pie chart with floating labels using D3. I'm new to D3 and I'm not even sure this is possible? Can you use the labels of one graph in another somehow? If you can, can you point me to an example?
Shorter Explanation:
I want labels from:
http://bl.ocks.org/1691430
...to be on a pie chart.
Here's the code I was running below:
Or in a JSBIN: http://jsbin.com/awilak/1/edit
If I understand his code correctly, this is the section that adds the labels. I don't understand what the labelForce.update does. From there, I don't care about transition, so that line isn't needed. Then the rest is just drawing the circles and adds a link / line? If someone could integrate that would be amazing but if you can help me understand what's going on and what I'm missing I'd be more than grateful.
// Now for the labels
// This is the only function call needed, the rest is just drawing the labels
anchors.call(labelForce.update)
labels = svg.selectAll(".labels")
.data(data, function(d,i) {return i;})
labels.exit()
.attr("class","exit")
.transition()
.delay(0)
.duration(500)
.style("opacity",0)
.remove();
// Draw the labelbox, caption and the link
newLabels = labels.enter().append("g").attr("class","labels")
newLabelBox = newLabels.append("g").attr("class","labelbox")
newLabelBox.append("circle").attr("r",11)
newLabelBox.append("text").attr("class","labeltext").attr("y",6)
newLabels.append("line").attr("class","link")
labelBox = svg.selectAll(".labels").selectAll(".labelbox")
links = svg.selectAll(".link")
labelBox.selectAll("text").text(function(d) { return d.num})
}
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>Testing Pie Chart</title>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.js?2.1.3"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.geom.js?2.1.3"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.layout.js?2.1.3"></script>
<style type="text/css">
.slice text {
font-size: 16pt;
font-family: Arial;
}
</style>
</head>
<body>
<button id="button"> Test </button>
<br>
<form id="controls">
<div>
<h2>Y axis</h2>
<ul id="y-axis">
<li><label><input checked="checked" type="radio" name="y-axis" value="Component">Component</label></li>
<li><label><input type="radio" name="y-axis" value="Browser">Browser</label></li>
<li><label><input type="radio" name="y-axis" value="Version">Version</label></li>
</ul>
</div>
</form>
<script type="text/javascript">
// return a list of types which are currently selected
function plottableTypes () {
var types = [].map.call (document.querySelectorAll ("#coaster-types input:checked"), function (checkbox) { return checkbox.value;} );
return types;
}
var w = 600, //width
h = 600, //height
r = 100,
r2 = 200, //radius
axis = getAxis (), //axes
color = d3.scale.category20c(); //builtin range of colors
data = [
{"Browser":"Internet Explorer ","Version":"8.0","Toatl":2000,"Component":"6077447412293130422"},
{"Browser":"Internet Explorer ","Version":"9.0 ","Toatl":1852,"Component":"6077447412293130422"},
{"Browser":"Internet Explorer ","Version":"6.0 ","Toatl":1754,"Component":"6077447412293130422"},
{"Browser":"Firefox ","Version":"16.0 ","Toatl":1020,"Component":"6077447412293130422"},
{"Browser":"Chrome ","Version":"23.0 ","Toatl":972,"Component":"6077447412293130422"},
{"Browser":"Internet Explorer ","Version":"7.0 ","Toatl":700,"Component":"6077447412293130422"},
{"Browser":"Mobile Safari ","Version":"6.0 ","Toatl":632,"Component":"6077447412293130422"},
{"Browser":"BOT ","Version":"BOT ","Toatl":356,"Component":"6077447412293130422"},
{"Browser":"Firefox ","Version":"8.0 ","Toatl":196,"Component":"6077447412293130422"},
{"Browser":"Mobile Safari ","Version":"5.1 ","Toatl":184,"Component":"6077447412293130422"}
];
var vis = d3.select("body")
.append("svg:svg") //create the SVG element inside the <body>
.data([data]) //associate our data with the document
.attr("width", w) //set the width and height of our visualization (these will be attributes of the <svg> tag
.attr("height", h)
.append("svg:g") //make a group to hold our pie chart
.attr("transform", "translate(" + r2 + "," + r2 + ")") //move the center of the pie chart from 0, 0 to radius, radius
var arc = d3.svg.arc() //this will create <path> elements for us using arc data
.outerRadius(r);
var pie = d3.layout.pie() //this will create arc data for us given a list of values
.value(function(d) { return d.Toatl; }); //we must tell it out to access the value of each element in our data array
var arcs = vis.selectAll("g.slice") //this selects all <g> elements with class slice (there aren't any yet)
.data(pie) //associate the generated pie data (an array of arcs, each having startAngle, endAngle and value properties)
.enter() //this will create <g> elements for every "extra" data element that should be associated with a selection. The result is creating a <g> for every object in the data array
.append("svg:g") //create a group to hold each slice (we will have a <path> and a <text> element associated with each slice)
.attr("class", "slice"); //allow us to style things in the slices (like text)
arcs.append("svg:path")
.attr("fill", function(d, i) { return color(i); } ) //set the color for each slice to be chosen from the color function defined above
.attr("d", arc); //this creates the actual SVG path using the associated data (pie) with the arc drawing function
arcs.append("svg:text") //add a label to each slice
.attr("transform", function(d) { //set the label's origin to the center of the arc
//we have to make sure to set these before calling arc.centroid
d.innerRadius = r2;
d.outerRadius = r;
return "translate(" + arc.centroid(d) + ")"; //this gives us a pair of coordinates like [50, 50]
})
.attr("text-anchor", "middle") //center the text on it's origin
.text(function(d, i) {
if(axis.yAxis == "Component"){
return data[i].Component;
}
return data[i].Browser; //get the label from our original data array
});
d3.select('#button').on('click', reColor);
var arcOver = d3.svg.arc()
.outerRadius(r + 30)
.innerRadius(0);
var arc = d3.svg.arc()
.outerRadius(r)
.innerRadius(0);
var arcs = vis.selectAll("g.slice")
.attr("class", "slice")
.on("mouseover", function(d) {
getAxis();
d3.select(this)
.select("path")
.transition()
.duration(500)
.attr("d", arcOver);
d3.select(this).select("text")
.text(function(d, i) {
if(axis.yAxis == "Component"){
return data[i].Component;
}
return data[i].Browser; //get the label from our original data array
});
})
.on("mouseout", function(d) {
getAxis();
d3.select(this)
.select("path")
.transition()
.duration(500)
.attr("d", arc);
d3.select(this)
.select("text")
.text(function(d, i) {
if(axis.yAxis == "Component"){
return data[i].Component;
}
return data[i].Browser; //get the label from our original data array
});
});
function reColor(){
var slices = d3.select('body').selectAll('path');
slices.transition()
.duration(2000)
.attr("fill", function(d, i) { return color(i+2); } );
slices.transition()
.delay(2000)
.duration(2000)
.attr("fill", function(d, i) { return color(i+10); } )
}
function makeData(){
}
// return an object containing the currently selected axis choices
function getAxis () {
var y = document.querySelector("#y-axis input:checked").value;
return {
yAxis: y,
};
}
function update() {
axis = getAxis()
arcs.selectAll("text") //add a label to each slice
.text(function(d, i) {
if(axis.yAxis == "Component"){
return data[i].Component;
}
return data[i].Browser; //get the label from our original data array
});
}
document.getElementById("controls").addEventListener ("click", update, false);
document.getElementById("controls").addEventListener ("keyup", update, false);
</script>
</body>
</html>
As others mentioned in the comments to your introduction-post it's possible to achieve a solution like you described it and it's possible using your code plus parts of the "moving-labels"-example. If I understand you correctly, you want to achieve non-overlapping labels using the force-layout, which is a pretty nice idea that I didn't stumble upon yet.
The code-part you pasted from the example just draws the labels and the lines as you already explained correctly. The next step is to rearrange the labels in a force-like layout around your pie chart.
The part that rearranges the labels (and links) in the example is the following:
function redrawLabels() {
labelBox
.attr("transform",function(d) { return "translate("+d.labelPos.x+" "+d.labelPos.y+")"})
links
.attr("x1",function(d) { return d.anchorPos.x})
.attr("y1",function(d) { return d.anchorPos.y})
.attr("x2",function(d) { return d.labelPos.x})
.attr("y2",function(d) { return d.labelPos.y})
}
// Initialize the label-forces
labelForce = d3.force_labels()
.linkDistance(0.0)
.gravity(0)
.nodes([]).links([])
.charge(-60)
.on("tick",redrawLabels)
The function is the one that changes the positions of the labels and lines. The force is calculated by D3 and started with the d3.force_labels().... As you can see, the function is assigned as an event-handler for the tick-event. In other words: After every step of calculating the force, D3 calls the 'drawer' for every label and updates the positions.
Unfortunately I'm not very familiar with the force_labels() method of D3, but I would assume it works pretty much like the regular force().
An anchor, in your case, is placed somewhere in each pie-piece for each label. The more centered within each pie-piece (NOT the pie itself), the better. Unfortunately you have to calculate this anchor-position somehow (sin and cos stuff) and set the line-ends to this fixed position within redrawLabels().
After you've done this you will see the first result. You may have to play around with gravity, linkDistance etc values of the force to achieve good results. (That's what the silders in the example do.)
See d3 docs for more info: https://github.com/mbostock/d3/wiki/Force-Layout
Then you will maybe stumble upon the problem that the labels are ordered around the pie without overlapping but in some strange order. You could solve this by initially placing the labels in correct order on a larger circle around your pie instead of positioning them randomly around the panel, which is the cause for the problem. This way you will experience way less jitter and misplacements.
The idea also is described in another blocks example: http://bl.ocks.org/mbostock/7881887
In this example, the nodes are initially placed on a virtual circle. The positioning is calculated by the following functions:
x: Math.cos(i / m * 2 * Math.PI) * 200 + width / 2 + Math.random(),
y: Math.sin(i / m * 2 * Math.PI) * 200 + height / 2 + Math.random()
They represent a circle with a radius of 200, place in the center of the drawing-panel. The circle is divided into m equally large pieces. i/m just calculates the 'piece-positions' where i ranges from 0 to m-1.
Hope I could help!
Yes, you can definitely combine force-labels with a pie chart! There is nothing particularly special about the pie chart labels you started with, they're just text elements that can be positioned like anything else using transform or x/y. It looks like you were initially positioning these labels according to the centroids of the arcs they were labelling, but you can just as easily use another criteria (like the output of a force layout).
D3's force layout calculates positions for things based on a set of constraints about what is fixed, what is movable, and which are connected to which. The labelForce.update method from Mike's bl.ocks example is being used to inform the force layout about how many objects need to be positioned, and where the fixed "anchor" points are. It then saves the computed positions for the labels into the diagram's data model, and they are used later on in the redrawLabels function.
You need to create two arcs. One for the pie chart drawing, and one which is large for the labels to sit on.
// first arc used for drawing the pie chart
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(0);
// label attached to first arc
g.append("text")
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.age; });
// second arc for labels
var arc2 = d3.svg.arc()
.outerRadius(radius + 20)
.innerRadius(radius + 20);
// label attached to second arc
g.append("text")
.attr("transform", function(d) { return "translate(" + arc2.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text(function(d) { return d.data.age; });

generate clipPaths for multiple elements in d3.js

I am trying to create partially filled circles like the ones in the final NYT political convention visualization: http://www.nytimes.com/interactive/2012/09/06/us/politics/convention-word-counts.html
The two clearest code examples I've found for clipPaths in d3 ( https://gist.github.com/1067636 and http://bl.ocks.org/3422480) create individual div elements with unique ids for each clip-path and then apply these paths to single elements.
I can not figure out how to go from these examples to a visualization with a unique circular clipPath for each element in a set of elements based on data values so that I can create my effect.
Here is what I have so far:
Given data with the following structure:
data = [
{value: 500, pctFull: 0.20, name: "20%"},
{value: 250, pctFull: 0.75, name: "75%"},
{value: 700, pctFull: 0.50, name: "50%"},
]
1) Create a force diagram with a circle for each object in the dataset. The area of the circle is derived from the objects value.
2) Calculate k (and h) from a proportion (pctFull) for each datapoint using the algorithm from the mbostock example http://bl.ocks.org/3422480
3) Use k to generate a rect for each datapoint that covers the appropriate area of the circle.
I think the illustration would be done if I could limit the visibility of each rect to its respective circle but this is where I am stuck. I've tried a bunch of things, none of which have worked.
Here's the jsfilddle: http://jsfiddle.net/G8YxU/2/
See a working fiddle here: http://jsfiddle.net/nrabinowitz/79yry/
// blue circle
node.append("circle")
.attr("r", function(d, i) {return rVals[i];})
.style("fill", "#80dabe")
.style("stroke", "#1a4876");
// clip path for the brown circle
node.append("clipPath")
// make an id unique to this node
.attr('id', function(d) { return "clip" + d.index })
// use the rectangle to specify the clip path itself
.append('rect')
.attr("x", function(d, i){ return rVals[i] * (-1);})
.attr("width", function(d, i){ return rVals[i] * 2;})
.attr("y", function(d, i) {return rVals[i] - (2 * rVals[i] * kVals[i]);})
.attr("height", function(d, i) {return 2 * rVals[i] * kVals[i];});
// brown circle
node.append("circle")
// clip with the node-specific clip path
.attr("clip-path", function(d) { return "url(#clip" + d.index + ")"})
.attr("r", function(d, i) {return rVals[i];})
.style("fill", "#dabe80")
.style("stroke", "#1a4876");
It looks like the only way to specify a clip path for an element is to use the url(IRI) notation in the clip-path attribute, which means that you'll need a unique id for each clip path based on the node data. I've used the form clip<node index> for the id - so each node gets its own clip path, and other sub-elements of the node can refer to it.
It seemed easiest, following Mike's example, to make two circles of different colors and use the rectangle itself for the clip path, rather than making a circle-based clip path. But you could do it either way.

Categories

Resources