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();
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'm trying to combine two examples from the d3 examples on bl.ocks (Choropleth and click-to-zoom). Presently I have this (response is an AJAX response from my backend that passes in things like us.json that I need for displaying the choropleth).
Style
.background {
fill: transparent;
pointer-events: all;
}
#states {
fill: #aaa;
}
#state-borders {
fill: none;
stroke: #fff;
stroke-linejoin: round;
pointer-events: none;
}
Javascript
response = parseJSON(response);
var us = response['us'];
var data = response['data'];
var reportID = response['reportID'];
var thresholds = response['thresholds'];
var colorScheme = response['colorScheme'];
var max = response['max'];
var options = response['options'];
var name = options['name'];
var width = 900, height = 500, centered;
//define the display threshold
var color = d3.scale.threshold()
.domain(thresholds)
// .range(["#f2f0f7", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"]); //purple
.range(colorScheme); //all colors
var rateById = {};
for(var i in data){
rateById[data[i]['id']] = +data[i]['value'];
}
var projection = d3.geo.albersUsa()
.scale(1070)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("#" + rowID + " .choropleth:nth-of-type(" + (parseInt(options['i']) + 1) + ")").append("svg")
.attr("width", width)
.attr("height", height)
var g = svg.append("g");
svg.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("path")
.attr("d", path)
.style("fill", function(d) { return color(rateById[d.id]); });
g.append("g")
.attr("id", "states")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.enter().append("path")
.attr("d", path)
g.append("path")
.datum(topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))
.attr("id", "state-borders")
.attr("d", path);
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.on("click", clicked);
function clicked(d){
console.log(typeof 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;
}
console.log(x + "\n" + y + "\n" + k + "\n" + centered);
g.transition()
.duration(750)
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
.style("stroke-width", 1.5 / k + "px");
}
I put the console.log(typeof d); to check if the status of the parameter being passed to the click function and find that it's null but if I take out the block that adds in the county lines then the click function is passed the appropriate value and the zoom function works as expected. I tried rearranging the various blocks that are adding in the SVG elements' order but without any success. I couldn't find any documentation as to where exactly the parameter passed to the click function comes from so I don't know what could cause it to be null.
I solved the problem by moving the block that created the counties
svg.append("g")
.attr("class", "counties")
.selectAll("path")
.data(topojson.feature(us, us.objects.counties).features)
.enter().append("path")
.attr("d", path)
.style("fill", function(d) { return color(rateById[d.id]); })
.on("click", clicked);
To the end (right before I define the clicked function) and putting the click event handler on that element instead of the rect.
I use http://d3pie.org/#docs-settings
But there is no such parameter as the distance from the center to the internal labels.
Can someone tried to do it?
I want to move the internal labels closer to the outer edge of the circle.
Thank you so much.
now so:
need:
You can position the labels by defining a new arc as suggested in https://stackoverflow.com/a/8270668/2314737 and then applying the centroid function.
I defined a new arc newarc with an inner radius equal to 2/3 of the outer radius.
var newarc = d3.svg.arc()
.innerRadius(2 * radius / 3)
.outerRadius(radius);
Here's the JS code:
var width = 300;
var height = 300;
var svg = d3.select("body").append("svg");
svg.attr("width", width)
.attr("height", height);
var dataset = [11, 13, 18, 25, 31];
var radius = width / 2;
var innerRadius = 0;
var arc = d3.svg.arc()
.innerRadius(0)
.outerRadius(radius);
var pie = d3.layout.pie();
var arcs = svg.selectAll("g.arc")
.data(pie(dataset))
.enter()
.append("g")
.attr("class", "arc")
.attr("transform", "translate(" + radius + ", " + radius + ")");
//Draw arc paths
var color = d3.scale.category10();
arcs.append("path")
.attr("fill", function (d, i) {
console.log(d);
return color(i);
})
.attr("stroke", "white")
.attr("d", arc);
var newarc = d3.svg.arc()
.innerRadius(2 * radius / 3)
.outerRadius(radius);
// Place labels
arcs.append("text")
.attr("transform", function (d) {
return "translate(" + newarc.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.attr("fill", "white")
.text(function (d) {
return d.value + "%";
});
Here is a working demo: http://jsfiddle.net/user2314737/kvz8uev8/2/
I decided to enroll in another way.
I added my property in the object and function of positioning inner labels in D3pie file d3pie.js
This function is located on the line - 996 d3pie.js
positionLabelGroups: function(pie, section) {
d3.selectAll("." + pie.cssPrefix + "labelGroup-" + section)
.style("opacity", 0)
.attr("transform", function(d, i) {
var x, y;
if (section === "outer") {
x = pie.outerLabelGroupData[i].x;
y = pie.outerLabelGroupData[i].y;
} else {
var pieCenterCopy = extend(true, {}, pie.pieCenter);
// now recompute the "center" based on the current _innerRadius
if (pie.innerRadius > 0) {
var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
var newCoords = math.translate(pie.pieCenter.x, pie.pieCenter.y, pie.innerRadius, angle);
pieCenterCopy.x = newCoords.x;
pieCenterCopy.y = newCoords.y;
//console.log('i ='+i , 'angle='+angle, 'pieCenterCopy.x='+pieCenterCopy.x, 'pieCenterCopy.y='+pieCenterCopy.y);
}
var dims = helpers.getDimensions(pie.cssPrefix + "labelGroup" + i + "-inner");
var xOffset = dims.w / 2;
var yOffset = dims.h / 4; // confusing! Why 4? should be 2, but it doesn't look right
// ADD VARAIBLE HERE !!! =)
var divisor = pie.options.labels.inner.pieDistanceOfEnd;
x = pieCenterCopy.x + (pie.lineCoordGroups[i][0].x - pieCenterCopy.x) / divisor;
y = pieCenterCopy.y + (pie.lineCoordGroups[i][0].y - pieCenterCopy.y) / divisor;
x = x - xOffset;
y = y + yOffset;
}
return "translate(" + x + "," + y + ")";
});
},
I add var divisor = pie.options.labels.inner.pieDistanceOfEnd;
Then I spotted this property devoltnyh the configuration file bhp and passed for plotting parameters.
inner: {
format: "percentage",
hideWhenLessThanPercentage: null,
pieDistanceOfEnd : 1.8
},
Meaning pieDistanceOfEnd: 1 hang tag on the outer radius of the chart
value pieDistanceOfEnd: 1.25 turn them slightly inward ....
You can play these parameters and to achieve the desired option.
In d3pie.js look for the function positionLabelGroups. In this function both labels (outer and inner) are positioned.
To modify the distance from the center you can play with the x,y here:
x = pieCenterCopy.x + (pie.lineCoordGroups[i][0].x - pieCenterCopy.x) / 1.8;
y = pieCenterCopy.y + (pie.lineCoordGroups[i][0].y - pieCenterCopy.y) / 1.8;
What I did was decreasing the 1.8 to 1.2 and obtained what youre looking for. Dont know what the other vars do, but you can study the code to figure it out
I have a d3 globe, and I have it scaling up (zooming in) when I doubleclick it. However, the zoom only works the first time I doubleclick. After that, I see that the program is entering the dblclick function, but no zooming is taking place. This is probably a stupid question, but I would be grateful if anyone were able to tell me how to make the zoom happen each time the globe is doubleclicked.
var width = 800,
height = 800,
centered;
var feature;
var projection = d3.geo.azimuthal()
.scale(380)
.origin([-71.03,42.37])
.mode("orthographic")
.translate([380, 400]);
var circle = d3.geo.greatCircle()
.origin(projection.origin());
// TODO fix d3.geo.azimuthal to be consistent with scale
var scale = {
orthographic: 380,
stereographic: 380,
gnomonic: 380,
equidistant: 380 / Math.PI * 2,
equalarea: 380 / Math.SQRT2
};
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("#globe").append("svg:svg")
.attr("width", 800)
.attr("height", 800)
.on("dblclick", dblclick)
.on("mousedown", mousedown);
var g = svg.append("g");
d3.json("simplified.geojson", function(collection) {
g.append("g")
.attr("id", "countries")
.selectAll("path")
.data(collection.features)
.enter().append("svg:path")
.attr("d", clip)
.attr("id", function(d) { return d.properties.ISO3; })
.on("mouseover", pathOver)
.on("mouseout", pathOut)
.on( "dblclick", dblclick)
.on("click", click);
feature = svg.selectAll("path");
feature.append("svg:title")
.text(function(d) { return d.properties.NAME; });
});
...
function dblclick(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 + ")")
.attr("transform", "scale(1.5)");
//.style("stroke-width", 1.5 / k + "px");
}
I agree with Erik E. Lorenz (no way to link to Erik's answer, it appears). Right now you're setting the zoomscale in the line
.attr("transform", "scale(1.5)");
The problem is that each time you call dblclick(), you're "resetting" it to 1.5. It's not multiplying by 1.5 it's just getting set. D3 doesn't remember what it used to be. That's why the first time you call dblclick() it works (because you're transforming the scale to 1.5 from 1). But from then on, the scale is already transformed to 1.5 and you just keep setting the scale transform to 1.5.
You need to keep track of "how far you've zoomed". And to do that you need a variable that keeps it's value between calls to dblclick(). I'd do something like this:
/* given the structure of your code, you can probably just declare the
variable before the function declaration. the function `dblclick` will
have access to the variable via closure */
var zoomScale = 1;
/* then you can just do this */
function dblclick(d) {
// you'll probably want to play with the math here
// that is, "1.5" might not be best
zoomScale = zoomScale * 1.5; // or some shorthand
g.transition()
.duration(750)
.attr("transform", "scale(" + zoomScale + ")");
}
I think that that scale(1.5) might be the problem. Have you tried dynamically increasing that factor every time dblclick() is called?
So I am trying to adapt M Bostock's x-value mouseover example to my own graph, the main difference being that I have multiple series instead of his one. For the moment I'm just trying to get the circles to work. My problem is that when I mouseover the graph (in Firebug) I get the message: Unexpected value translate(<my_x>, NaN) parsing transform attribute. I've tried several different ways to fix it but I get the same response each time. What am I doing wrong?
I have a jsFiddle, and the issue is at the bottom:
var focus = main.append('g')
.attr('class', 'focus')
.style('display', 'none');
var circles = focus.selectAll('circle')
.data(sets) // sets = [{name: ..., values:[{date:..., value:...}, ]}, ]
.enter()
.append('circle')
.attr('class', 'circle')
.attr('r', 4)
.attr('stroke', function (d) {return colour(d.name);});
main.append('rect')
.attr('class', 'overlay')
.attr('width', w)
.attr('height', h)
.on('mouseover', function () {focus.style('dispaly', null);})
.on('mouseout', function () {focus.style('display', 'none');})
.on('mousemove', mousemove);
function mousemove() {
var x0 = x_main.invert(d3.mouse(this)[0]),
i = bisectDate(dataset, x0, 1),
d0 = dataset[i - 1].date,
d1 = dataset[i].date,
c = x0 - d0 > d1 - x0 ? [d1, i] : [d0, i - 1];
circles.attr('transform', 'translate(' +
x_main(c[0]) + ',' +
y_main(function (d) {return d.values[c[1]].value;}) + ')'
);
== EDIT ==
Working jsFiddle
You're passing in a function definition into your y_main scale:
circles.attr('transform', 'translate(' +
x_main(c[0]) + ',' +
y_main(function (d) {return d.values[c[1]].value;}) + ')'
);
selection.attr can take a string value or a callback function but this is trying mixing both of those. You're passing in a string and as the string is constructed it tries to scale the function itself as a value, which will return NaN.
The function version should look like this (returning the entire transform value):
circles.attr('transform', function(d) {
return 'translate(' +
x_main(c[0]) + ',' +
y_main(d.values[c[1]].value) + ')';
});