I have currently have a line graph that looks like this:
on jsfiddle http://jsfiddle.net/vertaire/kttndjgc/1/
I've been trying to manually position the values on the graph so they get get printed next to the legend looking something like this:
Unintentional Injuries: 1980, 388437
I tried to set the positions manually, but it seems when I try and adjust to positioning, that positioning is relative to the that of the circle on the line like this:
How can I set the coordinates so that the values appear next to the legend?
Here is the code snippet for printing the values:
var mouseCircle = causation.append("g") // for each line, add group to hold text and circle
.attr("class","mouseCircle");
mouseCircle.append("circle") // add a circle to follow along path
.attr("r", 7)
.style("stroke", function(d) { console.log(d); return color(d.key); })
.style("fill", function(d) { console.log(d); return color(d.key); })
.style("stroke-width", "1px");
mouseCircle.append("text")
.attr("transform", "translate(10,3)"); // text to hold coordinates
.on('mousemove', function() { // mouse moving over canvas
if(!frozen) {
d3.select(".mouseLine")
.attr("d", function(){
yRange = y.range(); // range of y axis
var xCoor = d3.mouse(this)[0]; // mouse position in x
var xDate = x.invert(xCoor); // date corresponding to mouse x
d3.selectAll('.mouseCircle') // for each circle group
.each(function(d,i){
var rightIdx = bisect(data[1].values, xDate); // find date in data that right off mouse
yVal = data[i].values[rightIdx-1].VALUE;
yCoor = y(yVal);
var interSect = get_line_intersection(xCoor, // get the intersection of our vertical line and the data line
yRange[0],
xCoor,
yRange[1],
x(data[i].values[rightIdx-1].YEAR),
y(data[i].values[rightIdx-1].VALUE),
x(data[i].values[rightIdx].YEAR),
y(data[i].values[rightIdx].VALUE));
d3.select(this) // move the circle to intersection
.attr('transform', 'translate(' + interSect.x + ',' + interSect.y + ')');
d3.select(this.children[1]) // write coordinates out
.text(xDate.getFullYear() + "," + yVal);
yearCurrent = xDate.getFullYear();
console.log(yearCurrent)
return yearCurrent;
});
return "M"+ xCoor +"," + yRange[0] + "L" + xCoor + "," + yRange[1]; // position vertical line
});
}
});
First thing I would do is create the legend dynamically instead of hard coding each item:
var legEnter = chart1.append("g")
.attr("class","legend")
.selectAll('.legendItem')
.data(data)
.enter();
legEnter.append("text")
.attr("class","legendItem")
.attr("x",750)
.attr("y", function(d,i){
return 6 + (20 * i);
})
.text(function(d){
return d.key;
});
legEnter.append("circle")
.attr("cx",740)
.attr("cy", function(d,i){
return 4 + (20 * i);
})
.attr("r", 7)
.attr("fill", function(d,i){
return color(d.key);
});
Even if you leave it as you have it, the key here is to assign each text a class of legendItem. Then in your mouseover, find it and update it's value:
d3.select(d3.selectAll(".legendItem")[0][i]) // find it by index
.text(function(d,i){
return d.key + ": " + xDate.getFullYear() + "," + yVal;
});
Updated fiddle.
Related
I am trying to create an animation where circles are being animated on multiple paths.
I am able to get the animation I want for one of the paths but am not sure why the circles are only animating on that particular path, instead of being distributed according to the path they belong.
The full code can be found on my bl.ocks page: https://bl.ocks.org/JulienAssouline/4a11b54fc68c3255a85b31f34e171649
This is the main part of it
var path = svg.selectAll("path")
.data(data.filter(function(d){
return d.From > 2010
}))
.enter()
.append("path")
.style("stroke", "#832129")
.attr("class", "arc")
.attr("d", function(d){
var To_scale = xScale(d.experience),
From_scale = xScale(0),
y = yScale(0),
dx = To_scale - From_scale,
dy = y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + From_scale + " " + y + " A 43 50 0 0 1 " + To_scale + " " + y;
})
.style("fill", "none")
.style("opacity", 0)
.call(transition)
.on("mouseover", function(d){
var thisClass = d3.select(this).attr("class")
d3.selectAll(".path").style("opacity", 0.1)
d3.select(this).style("stroke", "white").style("opacity", 1).style("stroke-width", 2)
})
.on("mouseout", function(d){
d3.select(this).style("stroke", "#832129").style("opacity", 1)
})
function transition(path){
path.each(function(PathItem, index){
d3.select(this).transition()
// .delay(index + 200)
.duration(index * 5 + 1000)
.on("start", function(){
d3.select(this).style("opacity", 1)
})
.attrTween("stroke-dasharray", tweenDash)
})
}
function tweenDash(){
var l = this.getTotalLength(),
i = d3.interpolateString("0," + l, l + "," + l)
return function(t){ return i(t); };
}
console.log(data[0])
var circle = svg.selectAll("circle")
.data(data.filter(function(d){
return d.From > 2010
}))
.enter()
.append("circle")
.attr("r", 5)
.attr("cx", function(d){
return xScale(d.experience)
})
.style("fill", "red")
.attr("transform", "translate(" + 0 + ")")
.style("opacity", 0)
transition_circles();
function transition_circles(){
circle.each(function(pathItem, index){
d3.select(this)
.transition()
.delay(index * 200)
.duration(index * 10 + 1000)
.on("start", function(){
d3.select(this).style("opacity", 1)
})
.on("end",function(){
d3.select(this).style("opacity", 0)
})
.attrTween("transform", translateAlong(path.node(), index))
})
}
function translateAlong(path, index){
var l = path.getTotalLength();
return function(d, i , a){
return function(t){
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
}
}
}
Basically, I followed this https://bl.ocks.org/mbostock/1705868 example to get the point-along-path interpolation, but am having trouble adapting it to get the same effect on multiple lines.
I also tried adding .attr("cx", function(d){ return d.experience} to the circles but that didn't work.
You're always passing the same path (the first one) to the translateAlong function:
.attrTween("transform", translateAlong(path.node(), index))
//this is always the first path ---------^
You have to pass different paths to the translateAlong function. There are different ways for doing that (I don't know which one you want), one of those is:
.attrTween("transform", translateAlong(path.nodes()[index], index))
In this approach, the indices of the circles go from 0 to the data array length minus 1. So, since path.nodes() is an array of elements, it's selecting different ones by their indices.
Here is the updated bl.ocks: https://bl.ocks.org/anonymous/f54345ed04e1a66b7cff3ebeef271428/76fc9fbaeed5dfa867fdd57b24c6451346852568
PS: regarding optimisation, you don't need to draw several paths at the same position! Right now you have dozens of paths which are exactly the same. Just draw the different paths (in your case, only 3).
I have the following function that translates a set of circle objects along a predefined SVG path. Per this post, I am attempting to use the getCTM() function to capture the new position of each circle element after each transition runs on each of the respective elements. However, when the below code is executed, it isn't returning the updated transition after each transform. When I look at the matrix values that the getCTM() function is returning for each element, they are:
SVGMatrix {a: 1, b: 0, c: 0, d: 1, e: 0, f: 0}
Each circle moves along the SVG path without a hitch, but I can't figure out why the transform values aren't being returned in the SVGMatrix using the code below. Here is a sample of the data being bound to each circle:
trip_headsign
:
"Ashmont"
trip_id
:
"31562570"
trip_name
:
"11:05 pm from Alewife to Ashmont - Outbound"
vehicle_lat
:
42.33035301964327
vehicle_lon
:
-71.0570772306528
stops
:
Array[5]
0
:
Array[6]
0
:
"130"
1
:
"70085"
2
:
124
3
:
Array1
4
:
124
5
:
0
var map = L.map('map').setView([42.365, -71.10], 12),
svg = d3.select(map.getPanes().overlayPane).append("svg"),
ashmontG = svg.append("g").attr("class", "leaflet-zoom-hide"),
inboundG = svg.append("g").attr("class", "leaflet-zoom-hide");
var transform = d3.geo.transform({point: projectPoint}),
path = d3.geo.path().projection(transform);
var track = d3.svg.line()
.interpolate("linear")
.x(function(d) {
return applyLatLngToLayer(d).x
})
.y(function(d) {
return applyLatLngToLayer(d).y
});
var ashmontPath = ashmontG.selectAll("path")
.data([ashmont.features])
.enter()
.append("path")
.style("fill", "none")
.style("stroke", "black")
.style("stroke-width", 2)
.style("opacity", 0.1)
.attr("d", track)
var trains = inboundG.selectAll("circle")
.data(a_live_trains)
.enter()
.append("circle")
.attr("r", 6)
.style("fill", "blue")
.attr("class", "train");
d3.selectAll(".train").each(function(d) {
//the convertCoords function takes a lat/lng pair bound to the circle element and returns the coordinates in pixels using the leaflet latlngtolayerpoint function
var x = convertCoords(d).x,
y = convertCoords(d).y;
console.log(x, y);
for(j=0; j<d.stops.length; j++){
var matrix, xn, xy;
d3.select(this).transition()
.duration(d.stops[j][4]*50)
.delay(d.stops[j][5]*50)
.attrTween("transform", pathMove(d.stops[j][3]))
.each("end", ctm(this))
function ctm(x) {
console.log(x);
matrix = x.getCTM();
xn = matrix.e + x*matrix.a + y*matrix.c,
yn = matrix.f + x*matrix.b + y*matrix.d;
console.log(xn, yn)
}
}
})
function pathMove(path) {
return function (d, i, a) {
return function(t) {
var length = path.node().getTotalLength();
var p = path.node().getPointAtLength(t*length);
//var ptoPoint = map.layerPointToLatLng(new L.Point(p.x, p.y
return "translate(" + p.x + "," + p.y + ")";
}
}
}
moveTrains();
map.on("viewreset", reset);
reset();
function reset() {
svg.attr("width", bottomRight[0] - topLeft[0] + padding)
.attr("height", bottomRight[1] - topLeft[1] + padding)
.style("left", (topLeft[0]-(padding/2)) + "px")
.style("top", (topLeft[1]-(padding/2)) + "px");
ashmontG.attr("transform", "translate(" + (-topLeft[0] + (padding/2)) + ","
+ (-topLeft[1] + (padding/2)) + ")");
inboundG.attr("transform", "translate(" + (-topLeft[0] + (padding/2)) + ","
+ (-topLeft[1] + (padding/2)) + ")");
ashmontPath.attr("d", track);}
function projectPoint(x, y) {
var point = map.latLngToLayerPoint(new L.LatLng(y, x))
//var point = latLngToPoint(new L.LatLng(y, x));
this.stream.point(point.x, point.y)
}
Writing the code as shown immediately below returns the pre-transformation svg matrix. I believe this is happening because the 'this' keyword for each circle object was being selected pre-transform, and was passing the pre-transform SVG position into the ctm function. Additionally, the ctm function was executing before the pathMove function was even being called.
d3.select(this).transition()
.duration(d.stops[j][4]*50)
.delay(d.stops[j][5]*50)
.attrTween("transform", pathMove(d.stops[j][3]))
.each("end", ctm(this))
function ctm(x) {
console.log(x);
matrix = x.getCTM();
Slightly modifying the code using the example Mark provided above executes properly. I believe this is due to the fact that the 'this' keyword is being called a second time post-transform within the ctm function. Writing it this way grabs the SVGmatrix each time after the pathMove function has been called.
d3.select(this).transition()
.duration(d.stops[j][4]*50)
.delay(d.stops[j][5]*50)
.attrTween("transform", pathMove(d.stops[j][3]))
.each("end", ctm);
function ctm(x) {
console.log(this);
matrix = this.getCTM();
I have json object which generate dynamically,look like
var data =
[
{"hour":"16","percentage":50,"activity":[{"activity_id":"1","cnt":"1"} {"activity_id":"2","cnt":"1"}]},
{"hour":17,"percentage":0,"activity":[]}
{"hour":"18","percentage":20,"activity":[{"activity_id":"1","cnt":"1"} {"activity_id":"2","cnt":"5"}]},
{"hour":"19","percentage":80,"activity":[{"activity_id":"1","cnt":"5"} {"activity_id":"3","cnt":"7"}]},
];
and i want to draw chart with the help of d3
var can = d3.select("#chart").append("svg").attr("height",200).attr("width",800);
var r =100;
var p = Math.PI*2;
//give color to arc
//if 0 range to yellow and 100 is red
var color = d3.scale.linear()
.domain([0,100])
.range(["#D6EBFD","#FF0000"]);
var group = can.append("g")
.attr("transform","translate(100,100)");
//draw arc with outer and inner radius
var arc = d3.svg.arc()
.innerRadius(r - 30)
.outerRadius(r)
var pie = d3.layout.pie()
.sort(null)
.value(function (d){return data.length;});
var arcs = group.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc")
.attr('stroke','#fff')
.attr('stroke-width', '2')
.attr('fill',function(d){return color(d.data.percentage)})
.on("mouseover", function(d){
div.style("display", "inline")
//.text(d.data.percentage + ", " + d.data.hour)
.data(d.data.activity)
//problem is here to make tooltip when mouseover to the chart where i want data from activity array object?
.text(function(a){
a.activity_id + ", " + a.cnt
})
.text(function(d){
for(var i = 0;i>data.activity.length;i++){
return data.activity['activity_id'] + ", " + data.activity['cnt'];
}
})
.style("left", (d3.event.pageX - 34) + "px")
.style("top", (d3.event.pageY - 12) + "px");
})
.on("mouseout", mouseout);
arcs.append("path")
.attr("d", arc)
.style("fill", function (d) {
return color(d.data.percentage);
});
arcs.append("text")
.attr('transform',function(d){return "translate("+arc.centroid(d)+")";})
.attr('fill','#0000FF')
.attr('z-index',1)
.text(function(d){return d.data.hour});
var div = d3.select("#chart").append("div")
.attr("class", "tooltip")
.style("display", "none");
function mouseout() {
div.style("display", "none");
}
which draw a donut chart but i want to make tooltip when mouseover to the chart which is activity_id,cnt in loop.
(Please ignore the design)
What i need is when mouseover to
16 tooltip must be 1,1
17 tooltip must be
18 tooltip must be 1,1
2,5
19 tooltip must be 1,5
3,7
This is my first time to d3,so please can anyone help me.Thanks in advance.
Instead of doing it like this having 2 text function which is wrong:
.text(function(a){
a.activity_id + ", " + a.cnt
})
.text(function(d){
for(var i = 0;i>data.activity.length;i++){
return data.activity['activity_id'] + ", " + data.activity['cnt'];
}
})
write a single text function like this:
.text(function() {
var str = "";
d.data.activity.forEach(function(a){
//make the display string by iterating over activity.
str += a.activity_id + ", " + a.cnt + " ";
})
return str;
})
Working code here
I'm drawing a pie chart with d3.js. I want to transition the pie slices when new data is added. (i'm using the reusable chart API). I'm first creating a chart group using enter and then appending the chart arc path to that:
http://jsfiddle.net/EuK6H/4/
var arcGroup = svg.select(".pie").selectAll(".arc " + selector)
.data(pie(data))
.enter().append("g")
.attr("class", "arc " + selector);
if (options.left) {
arcGroup.attr('transform', 'translate(' + options.left + ',' + options.top + ')');
} else {
arcGroup.attr('transform', 'translate(' + options.width / 2 + ',' + options.height / 2 + ')');
}
//append an arc path to each group
arcGroup.append("path")
.attr("d", arc)
//want to add the transition in here somewhere
.attr("class", function (d) { return 'slice-' + d.data.type; })
.style("fill", function (d, i) {
return color(d.data.amount)
});
//...
Problem is when new data comes in I need to be able to transition the path (and also the text nodes shown in the the fiddle) but the enter selection is made on the the parent group. How can I add a transition() so it applies to the path?
You can use .select(), which will propagate the data to the selected element. Then you can apply a transition based on the new data. The code would look something like this:
var sel = svg.selectAll(".pie").data(pie(newData));
// handle enter + exit selections
// update paths
sel.select("path")
.transition()
.attrTween("d", arcTween);
I'm using the function d3.svg.symbol() to plot symbols on a scatterplot. I'd like to have some tooltip show up on mouseover on the symbols. To place them accordingly, I need to get the center of the symbols, but don't really know how to do this. The code I use to generate the symbols is:
var symbols = svg.append("g")
.attr("id", "circles")
.selectAll("path")
.data(dataset)
.enter()
.append("path")
.attr("transform", function (d) { return "translate(" + x(d[SelX]) + "," + y(d[SelY]) + ")"; })
.attr("d", d3.svg.symbol()
.size(50)
.type(function (d) { if (d.Spaziatura == "Proportional") { return "circle";} else { return "diamond"; }; }))
.attr("fill", function (d) {
if (d.Grazie == "Sans") { return colore(parseFloat(d[SelCol])); }
else { return colore2(parseFloat(d[SelCol])); };
})
.attr("id", function (d) { return d.FamilyName;})
.attr("opacity", 1)
.attr("visibility", "visible")
Then the mouseover event:
.on("mouseover", function (d) {
//Get this symbbol's x/y values, then augment for the tooltip
var centroid = symbols.centroid(d);
var xPosition = centroid[0];
var yPosition = centroid[1];
//Update the tooltip position and value
svg .append("text")
.attr("class", "tooltip")
.attr("x", xPosition)
.attr("y", yPosition - (height/20))
//and then other stuff happens
I tried to reuse the centroid function I used for a map, but it doesn't work. I just need to get the center of the symbol's path to get this working, so any help on this is really appreciated, thanks!
You can get the center of the symbol like this (in the mouse handler):
var bibox = this.getBBox(),
t = d3.transform(d3.select(this).attr("transform")),
centroidX = t.translate[0] + (bibox.x + bibox.width)/2,
centroidY = t.translate[1] + (bibox.y + bibox.height)/2;
Demo here.