I'm still learning to program and I'm currently trying out the d3 library.
So far I'm pretty happy with the result. fiddle
Q: If you check out the link (or part of the code under this question) you should try to plot a point. This is only possible on the x-axis ticks. You'll see it animates but it's not exactly what I want. I just want it to animate the newly added line. I have checked out .enter() and .append() but I was getting errors. I might be doing something wrong.
function lines(x, y) {
this.x = x;
this.y = y+h;
}
var lineArray = [{x: 0, y: h}, {x: 1, y: h}];
var lineArrayPrevious = lineArray[lineArray.length -1].x;
var d3line = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("monotone");
var path = svg.append("path").attr("d", d3line(lineArray)).attr("class", "line");
canPlot = true;
function plot() {
var m = d3.mouse(this);
if (m[0]-20 > lineArray[lineArray.length - 1].x) {
var lineX = lineArray.push(new lines(m[0], m[1]));
svg.selectAll("path")
.data(lineArray)
.attr("d", d3line(lineArray));
var point = svg.append("circle")
.attr("cx", function(d, i) { return m[0]; })
.attr("cy", function(d, i) { return m[1]+h; })
.attr("r", 0).transition().delay(150).attr("r", 6);
var totalLength = path.node().getTotalLength();
console.log();
path.attr("stroke-dasharray", totalLength + " " + totalLength)
.attr("stroke-dashoffset", totalLength)
.transition().duration(700).ease("linear").attr("stroke-dashoffset", 0).delay(200);
canPlot = true;
} else { console.log("error"); canPlot = false; }
}
Excuse my bad code, I'm learning and will clean it up eventually.
Q2: How hard would it be to make a circle that follows the mouse's y-position and moves on the ticks when you get near one?
Q3: If we solve my first question, would it be easy to get the lines to animate/update automatically when we do question 2?
Thanks in advance.
I've updated your jsfiddle here to include the points that you're asking for.
Regarding question 1, I've changed the way the line is drawn such that it can be interpolated from the previous to the current point in a transition. The relevant code is this.
svg.select("path.line")
.attr("d", d3line(lineArray))
.transition().duration(700)
.attrTween('d', pathTween)
.each("end", function() {
var lineX = lineArray.push(new lines(m[0], m[1]));
});
var last = lineArray[lineArray.length-1];
function pathTween() {
var xi = d3.interpolate(last.x, m[0]),
yi = d3.interpolate(last.y, m[1] + h);
return function(t) {
return d3line(lineArray.concat([{x: xi(t), y: yi(t)}]));
};
}
Note that the new data point is only added to the array of points once the transition finishes.
Regarding your second question, this is taken care of by attaching handlers to all tick marks and append a marker on mouse over:
d3.selectAll(".xaxis > .tick").on("mouseenter", mousein)
.on("mousemove", mousemove)
.on("mouseleave", mouseout);
function mousein() {
svg.append("circle").attr("class", "marker").attr("r", 3)
.attr("pointer-events", "none");
}
function mousemove() {
d3.select("circle.marker")
.attr("transform", d3.select(this).attr("transform"))
.attr("cy", d3.mouse(this)[1] + h);
}
function mouseout() {
d3.select("circle.marker").remove();
}
Related
I just started learning javascript and d3.js by taking a couple of lynda.com courses. My objective is to create a function that takes an array of numbers and a cutoff and produces a plot like this one:
I was able to write javascript code that generates this:
Alas, I'm having troubles figuring out a way to tell d3.js that the area to the left of -opts.threshold should be read, the area in between -opts.threshold and opts.threshold blue, and the rest green.
This is my javascript code:
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("MME: ", opts.MME);
console.log("threshold: ", opts.threshold);
console.log("prob: ", opts.prob);
console.log("colors: ", opts.colors);
var margin = {left:50,right:50,top:40,bottom:0};
var xMax = opts.x.reduce(function(a, b) {
return Math.max(a, b);
});
var yMax = opts.y.reduce(function(a, b) {
return Math.max(a, b);
});
var xMin = opts.x.reduce(function(a, b) {
return Math.min(a, b);
});
var yMin = opts.y.reduce(function(a, b) {
return Math.min(a, b);
});
var y = d3.scaleLinear()
.domain([0,yMax])
.range([height,0]);
var x = d3.scaleLinear()
.domain([xMin,xMax])
.range([0,width]);
var yAxis = d3.axisLeft(y);
var xAxis = d3.axisBottom(x);
var area = d3.area()
.x(function(d,i){ return x(opts.x[i]) ;})
.y0(height)
.y1(function(d){ return y(d); });
var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
chartGroup.append("path")
.attr("d", area(opts.y));
chartGroup.append("g")
.attr("class","axis x")
.attr("transform","translate(0,"+height+")")
.call(xAxis);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
In case this is helpful, I saved all my code on a public github repo.
There are two proposed solutions in this answer, using gradients or using multiple areas. I will propose an alternate solution: Use the area as a clip path for three rectangles that together cover the entire plot area.
Make rectangles by creating a data array that holds the left and right edges of each rectangle. Rectangle height and y attributes can be set to svg height and zero respectively when appending rectangles, and therefore do not need to be included in the array.
The first rectangle will have a left edge at xScale.range()[0], the last rectangle will have an right edge of xScale.range()[1]. Intermediate coordinates can be placed with xScale(1), xScale(-1) etc.
Such an array might look like (using your proposed configuration and x scale name):
var rects = [
[x.range()[0],x(-1)],
[x(-1),x(1)],
[x(1),x.range()[1]]
]
Then place them:
.enter()
.append("rect")
.attr("x", function(d) { return d[0]; })
.attr("width", function(d) { return d[1] - d[0]; })
.attr("y", 0)
.attr("height",height)
Don't forget to set a clip-path attribute for the rectangles:
.attr("clip-path","url(#areaID)"), and to set fill to three different colors.
Now all you have to do is set your area's fill and stroke to none, and append your area to a clip path with the specified id:
svg.append("clipPath)
.attr("id","area")
.append("path")
.attr( // area attributes
...
Here's the concept in action (albeit using v3, which shouldn't affect the rectangles or text paths.
Thanks to #andrew-reid suggestion, I was able to implement the solution that uses multiple areas.
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("MME: ", opts.MME);
console.log("threshold: ", opts.threshold);
console.log("prob: ", opts.prob);
console.log("colors: ", opts.colors);
console.log("data: ", opts.data);
var margin = {left:50,right:50,top:40,bottom:0};
xMax = d3.max(opts.data, function(d) { return d.x ; });
yMax = d3.max(opts.data, function(d) { return d.y ; });
xMin = d3.min(opts.data, function(d) { return d.x ; });
yMin = d3.min(opts.data, function(d) { return d.y ; });
var y = d3.scaleLinear()
.domain([0,yMax])
.range([height,0]);
var x = d3.scaleLinear()
.domain([xMin,xMax])
.range([0,width]);
var yAxis = d3.axisLeft(y);
var xAxis = d3.axisBottom(x);
var area = d3.area()
.x(function(d){ return x(d.x) ;})
.y0(height)
.y1(function(d){ return y(d.y); });
var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x< -opts.MME ;})))
.style("fill", opts.colors[0]);
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x > opts.MME ;})))
.style("fill", opts.colors[2]);
if(opts.MME !==0){
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return (d.x < opts.MME & d.x > -opts.MME) ;})))
.style("fill", opts.colors[1]);
}
chartGroup.append("g")
.attr("class","axis x")
.attr("transform","translate(0,"+height+")")
.call(xAxis);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
I'm moving an element from one point to another. But specifically I want to achieve this animation:
http://carto.net/svg/samples/path_animation.svg
My knowledge in d3.js are somewhat limited, I do not know how to generate this curve line and I also do not know how to do 3d perspective, in which the circle should appear, then be very small and grow and eventually become small until disappearing, as well As in the attached link. How can I do it?
http://jsfiddle.net/bzfs55bg/
var circle = svg.append("circle")
.attr("fill", "blue")
.attr("r", 4)
.attr("cx", centroids.ANTIOQUIA[0])
.attr("cy", centroids.ANTIOQUIA[1]);
circle.transition()
.delay(1000)
.duration(2000)
.attr("cx", centroids.BOYACA[0])
.attr("cy", centroids.BOYACA[1]);
My answer contain 3 main steps:
First, we have to create a path from point A to point B, simulating an arc. There are several ways to do that, and your question is not clear. Here, I'm using a quadratic curve:
var arc = svg.append("path")
.style("fill", "none")
.style("stroke", "yellow")
.style("stroke-width", 2)
.attr("d", "M" + centroids.ANTIOQUIA[0] + "," +
centroids.ANTIOQUIA[1] + " Q" + centroids.BOYACA[0] +
"," + centroids.ANTIOQUIA[1] + " " +
centroids.BOYACA[0] + "," + centroids.BOYACA[1]);
This path can have a colour or be transparent, it doesn't matter.
Second, we use Bostock's famous translate along path code to translate the element along that arc. I changed the function to pass the element along with the path, so we can change its size:
circle.transition()
.duration(5000)
.attrTween("transform", translateAlong(arc.node(), circle.node()));
//the second argument is the circle -----------------^
function translateAlong(path, circle) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
d3.select(circle).attr("r", circleSize(t))
//here we can change circle's size
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
Finally, I'm using a linear scale to make the circle big and small again:
var circleSize = d3.scale.linear()
.domain([0, 0.5, 1])
.range([4, 10, 4]);
The domain here goes from 0 to 1 because this is the range of values the parameter t can assume.
Here is the updated fiddle: http://jsfiddle.net/zkc2wton/
Here is a second fiddle, changing the opacity at the beginning and at the end of the movement: http://jsfiddle.net/4pdusase/
How to draw a curve line between points:
//draw a line
var curveData = [{ x: centroids.ANTIOQUIA[0], y: centroids.ANTIOQUIA[1] }, { x: centroids.BOYACA[0], y: centroids.BOYACA[0] }];
//make a line function
var lineFunction = d3.svg.line()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.interpolate("monotone");//change it as per your choice.
//make a path
var myPath = d3.select('svg').append("path")
.attr("d", lineFunction(curveData))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
Please refer interpolation for the curves you may like here
Further moving point along a line and disappearing.
//transition along a path
circle.transition()
.duration(10000)
.attrTween("transform", translateAlong(myPath.node()))
.style("opacity",0);//transitioning the opacity to make it vanish.
// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
3d i don't know :(
working code here
I'm trying to make a data visualisation with d3.js. Im new to d3 and i think i did not quite understand completely how to change data with a click, so i need your help!
Thats what i've got so far(which i'm quite happy that it works):
/*-------------------------data is parsed and proceedet above----------------------------------------------------*/
var SVGWidth = 1670;
var SVGeight = 800;
var kreis = 1;
var ringArea = 1;
var width = 1;
var multi = 3.5;
var color = d3.scale.ordinal().range(['#FFFFFF', '#DADAD9', '#9D9C9C']);
var arc = d3.svg.arc().innerRadius(function(d) {
donutWidth = Math.sqrt(d.data.WHI1 / Math.PI + Math.pow(d.data.E1, 2)) * multi - d.data.E1 * multi
return donutWidth;
}).outerRadius(function(d, i) {
width = Math.sqrt(d.data.WHI1 / Math.PI + Math.pow(d.data.E1, 2)) * multi
return width;
});
var pie = d3.layout.pie().value(function(d) {
return d.country;
}).sort(null);
var svg = d3.select('#chart')
.append('svg')
.attr('width', SVGWidth)
.attr('height', SVGeight)
.append("g")
svg.selectAll("g")
.data(data1995).
enter().append("g")
.attr({
transform: function(d, i) {
var pos = coord2Pt(geo[i][1], geo[i][2], 1.0);
return "translate(" + pos.x + ", " + pos.y + ")";
}
}).selectAll('path').data(function(country, i) {
return pie(country.map(function(value) {
return {
country: value,
WHI1: WHI1[i],
E1: E1[i]
};
}));
})
.enter()
.append('path')
.attr('d', function(d) {
return arc(d);
}).attr('fill', function(d, i) {
return color(i);
});
});
})(window.d3);
This results in this Visualisation:
Worldmap donut
What i'm now trying to do is to change my Data from "data1995" to "data2015", both "WHI1" to "WHI2" and both "E1" to "E2" with a click on the "test" button. In addition it would be great if they would change with a transition.
I'm not shure if I'm on the right path to accomplish this but is tried this so far:
d3.select("button").on('click', function() {
console.log("click")
d3.selectAll('path').transition().duration(500).attr("fill", function(d, i) {
return color(i);
}).attr("d", arc).each(function(d) {
this._current = d;
});
function change(data2015) {
return pie(country.map(function(value) {
return {
country: value,
WHI2: WHI2[i],
E2: E2[i]
};
}));
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
});
Thats what I've found on another thread on here (update d3 pie chart with new data.json)
but in fact i'm not even shure if i translated it right to my script and i'm curious if that would work with all of my instances of the Donut.
I suggest you to add a button with a onclick property, for example an update() function.
This function loads data2015 and change WHI1 to WHI2 and E1 to E2.
So this function is quite similar to what you have already written.
I think this approach is better because you do not need to update the chart as frequently as in the link you posted
I've found an example links, look at these:
http://www.d3noob.org/2013/02/update-d3js-data-dynamically-button.html
http://bl.ocks.org/d3noob/7030f35b72de721622b8
I am new to programming so apologies if the answer to this is obvious but after hours of searching I can't find out what's wrong.
I simply want to tween an arc in D3.js (in this case change the endAngle to 0). I've been through lots of examples but I must be missing something. I have built a function to change arc colour on clicking which works but it is the second function 'arcTween' to change the arc endAngle of the outermost arcs that doesn't work. Can you help?
Many thanks
Full JS fiddle here: http://jsfiddle.net/vaaa052h/
Extracts below
var chartArea = d3.select("#chart")
.append("svg") // d3 SVG function
.attr("width", 210)
.attr("height", 210);
var arcGroup = chartArea.append("g") // d3 g grouping function
.attr("transform", "translate(" + transX + "," + transY + ")")
.attr("class", "arc");
var arc = d3.svg.arc()
.innerRadius(function (d) {
return radius[level];
})
.outerRadius(function (d) {
return radius[level + 1];
})
.startAngle(function (d) {
return minAngArc;
})
.endAngle(function (d) {
return maxAngArc;
});
//////// chart building ///////////////
arcGroup.append("path")
.attr("d", arc)
.attr("fill", color(0, random, 0, i, j, k))
.attr("opacity", opacity(rating))
.on("click", arcTween());
////// click functions //////////
function arcTween(d) {
d3.select(this).transition().duration(1000)
.attrTween("d", function (d) {
var interpolate = d3.interpolate(d.endAngle, 0);
return function (t) {
d.endAngle = interpolate(t);
return arc(d);
};
});
};
I made a couple of changes in this updated fiddle: http://jsfiddle.net/henbox/a8r326m5/1/
First, when you set up the click handler, avoid calling it on page load by using:
.on("click", arcTween);
instead of
.on("click", arcTween());
as per Lars' explanation here. This will stop you getting "Object [object global] has no method 'getAttribute'" errors in the console
Second, bind some data to the path elements so we can manipulate it later:
arcGroup.append("path")
.datum({endAngle:maxAngArc, startAngle:minAngArc})
....
And thirdly, use this data in the arcTween function. By setting maxAngArc and minAngArc, and then tweening the value of maxAngArc to minAngArc (I've asumed you mean to do this rather than tweening to 0), you should get the behaviour you want. The tween function:
function arcTween(d) {
maxAngArc = d.endAngle;
minAngArc = d.startAngle;
d3.select(this).transition().duration(1000)
.attrTween("d", function (d) {
var interpolate = d3.interpolate(d.endAngle, d.startAngle);
return function (t) {
maxAngArc = interpolate(t);
return arc(d);
};
});
};
I'm creating a force-directed network in D3 where each node represents a webpage in a collection and directed links between them represent a hyperlink from one webpage to another. As the user moves between webpages, links are added to the network. The number of nodes/webpages in the collection doesn't change.
Whenever a new link is added, I reload the network, reading in JSON code containing the new and old links and all the nodes. At the moment, I'm trying to "calm" the initial loading of this network so that nodes aren't created at random places, potentially creating a network where the nodes are nowhere near where they were before. To do this, I want to set the initial position of each node to what it was in the previous network.
I've done this by setting the node's x and y attributes as seen in the following code snippet:
var links = data.links;
var nodes = data.nodes;
var nodeDictionary = new Object();
if (node != 0) {
force.stop();
node.each(function(d) {
var pos = [];
var thisNode = d3.select(this);
pos[0] = thisNode.attr("x");
pos[1] = thisNode.attr("y");
var page = thisNode.attr("page");
nodeDictionary[page] = pos;
});
nodes.forEach(function(node) {
var x = nodeDictionary[node.id][0];
var y = nodeDictionary[node.id][1];
//node.px = undefined;
//node.py = undefined;
node.x = x;
node.y = y;
});
}
This seems to work some of the time or for some nodes, but most of the time this results in the nodes having NaN positions. Specifically, these positions are NaN in the tick() method of the network, which looks like this:
// Animates the force-directed network
function tick() {
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
link.attr("d", drawCurve);
}
function drawCurve(d) {
var testing = "M" + d.source.x + "," + d.source.y + "L" + d.target.x + "," + d.target.y;
return testing
}
In other examples I've seen where an initial node position is set in a force-directed network, the tick() method looks more like this:
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
});
Is there something else I need to do to make my code work using the translate function? I've tried stopping the tick method before creating a new network as suggested in this thread https://groups.google.com/forum/#!topic/d3-js/k6VSYZfMjGA as well, but that didn't help. Do I need to clear some variable or element before generating a new network? I've also tried setting the px and py attributes and the px, py, x, and y attributes with various combinations of values with no success.
I've created a jsfiddle with the basic code showing the problem I'm experiencing here: http://jsfiddle.net/ew5qprhe/1/
Double clicking any of the nodes will cause a new network to be generated. Some of the nodes are correctly set to their previous positions, but ones with NaN positions move to the upper left corner.
The key was to change this business:
node.each(function(d) {
var pos = [];
var thisNode = d3.select(this);
pos[0] = thisNode.attr("x");
pos[1] = thisNode.attr("y");
var page = thisNode.attr("page");
nodeDictionary[page] = pos;
});
to this:
node.each(function(d) {
pos = [];
pos[0] = d.x;
pos[1] = d.y;
var page = d.id;
nodeDictionary[page] = pos;
});
Strange things were happening because of my use of .attr("x") and .attr("y"). Also, curse my unfamiliarity with D3!