I have this code, that basically creates a D3.js map and creates a 'circle' when someone 'hits' the page based on their IP location, this was originally done in Raphael and i am trying to just use the D3.js library, but I am stuck on how to place the 'circle' directly on the map with the right co-ordinates:-
function ZmgcClient() {
var personPath = "M255.968,166.154c34.206,0,61.936-27.727,61.936-61.934c0-34.208-27.729-61.936-61.936-61.936s-61.936,27.728-61.936,61.936 C194.032,138.428,221.762,166.154,255.968,166.154z M339.435,194.188c-13.082-13.088-28.625-20.924-84.83-20.924 c-56.214,0-71.38,8.505-83.796,20.924c-12.422,12.416-8.23,144.883-8.23,144.883l27.485-65.304l17.28,194.554l49.856-99.57 l46.521,99.57l16.456-194.554l27.487,65.304C347.664,339.07,352.521,207.271,339.435,194.188z";
if (! (this instanceof arguments.callee)) {
return new arguments.callee(arguments);
}
var self = this,
width = $('#map').width(),
mapCanvasHeight = (width * 0.45);
this.init = function() {
self.drawMap();
self.setupBayeuxHandlers();
};
this.setupBayeuxHandlers = function() {
$.getJSON("/config.json", function (config) {
self.client = new Faye.Client("http://" + window.location.hostname + ':' + config.port + '/faye', {
timeout: 120
});
self.client.subscribe('/stat', function (message) {
// console.log("MESSAGE", message);
self.drawMarker(message);
});
});
};
this.drawMap = function () {
var data;
// Most parts of D3 don't know anything about SVG—only DOM.
self.map = d3.geo.equirectangular().scale(width);
self.path = d3.geo.path().projection(self.map);
self.svg = d3.select('#map').append('svg:svg')
.attr("width", "100%")
.attr("height", "100%")
.attr("viewBox", "0 0 " + width + " " + mapCanvasHeight);
self.countries = self.svg.append('svg:g').attr('id', 'countries');
// Load data from .json file
d3.json("/ui/data/world-countries.json", function(json) {
self.countries.selectAll("path") // select all the current path nodes
.data(json.features) // bind these to the features array in json
.enter().append("path") // if not enough elements create a new path
.attr("d", self.path) // transform the supplied jason geo path to svg
.on("mouseover", function(d) {
d3.select(this).style("fill","#6C0")
.append("svg:title")
.text(d.properties.name);})
.on("mouseout", function(d) {
d3.select(this).style("fill","#000000");})
});
}
this.geoCoordsToMapCoords = function (latitude, longitude) {
latitude = parseFloat(latitude);
longitude = parseFloat(longitude);
var mapWidth = width,
mapHeight = mapCanvasHeight,
x, y, mapOffsetX, mapOffsetY;
x = (mapWidth * (180 + longitude) / 360) % mapWidth;
latitude = latitude * Math.PI / 180;
y = Math.log(Math.tan((latitude / 2) + (Math.PI / 4)));
y = (mapHeight / 2) - (mapWidth * y / (2 * Math.PI));
mapOffsetX = mapWidth * 0.026;
mapOffsetY = mapHeight * 0.141;
return {
x: (x - mapOffsetX) * 0.97,
y: (y + mapOffsetY + 15),
xRaw: x,
yRaw: y
};
}
this.drawMarker = function (message) {
var lon = message.longitude,
lat = message.latitude,
city = message.city;
self.countries.append('svg:circle')
.attr("cy", lon, lat)
.attr("cx", lon, lat)
.attr('r', 5);
}
// Initialise
this.init();
};
var ZmgcClient;
jQuery(function() {
ZmgcClient = new ZmgcClient();
});
I am trying to pass lon/lat data and draw a circle at that point on the map, this code:
this.drawMarker = function (message) {
var latitude = message.latitude,
longitude = message.longitude,
text = message.title,
city = message.city,
x, y;
var mapCoords = this.geoCoordsToMapCoords(latitude, longitude);
x = mapCoords.x;
y = mapCoords.y;
console.log(x,y);
self.countries.append('svg:circle')
.attr("r", 40.5)
.attr("cx", longitude)
.attr("cy", latitude);
console.log(latitude, longitude);
The circle is created, but it is not in the correct position.
What am I missing?
Any advice much appreciated.
it must have been late, it should be
var mapCoords = this.geoCoordsToMapCoords(latitude, longitude);
x = mapCoords.x;
y = mapCoords.y;
self.countries.append('svg:circle')
.attr("r", 5)
.style("fill", "steelblue")
.attr("cx", x)
.attr("cy", y);
and not the latitude, longitude values.
Related
So I've created a pie chart that takes in some JSON data. Upon clicking a radio button, the pie chart's data will update depending on what option is chosen. However, I'm running into an issue because I'd like to put a D3 tooltip on the pie chart and have its values update whenever the pie chart updates... anyone know the best way to do that?
Here's what my pie chart looks like so far:
<form>
<label><input type="radio" name="dataset" value="females" checked> Females </label>
<label><input type="radio" name="dataset" value="males"> Males </label>
<label><input type="radio" name="dataset" value="totalNumber"> Both </label>
</form>
<script>
var width = 760, height = 300, radius = Math.min(width, height) / 2;
var legendRectSize = 18;
var legendSpacing = 4;
var color = d3.scale.ordinal()
.domain(["7-15", "16-24", "25-33", "34-42", "43-51", "52-60", "61-70"])
.range(["#ff8b3d", "#d65050", "#ab0e4d", "#6f0049", "#4a004b", "#d0743c", "#BE2625"]);
var arc = d3.svg.arc()
.outerRadius(radius * 0.9)
.innerRadius(radius * 0.3);
var outerArc = d3.svg.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d.females; });
var svg = d3.select("#donut1").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 4 + "," + height / 2 + ")")
d3.json("graphdata.php", function(error, data) {
data.forEach(function(d) {
var path = svg.datum(data).selectAll("path")
.data(pie)
d.females = +d.females;
d.males = +d.males;
d.totalNumber = +d.totalNumber;
});
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([55, 0])
.html(function(d) {
return ("Victims:") + " " + d[value] + "<br>";
})
svg.call(tip);
var donut1 = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc")
.on("mouseover", tip.show)
.on("mouseout", tip.hide);
donut1.append("path")
.style("fill", function(d) { return color(d.data.age); })
.attr("d", arc)
.each(function(d) {this._current = d; }); //stores the initial angles
/*Control for changing the radio buttons & filtering the data*/
d3.selectAll("input")
.on("change", change);
var timeout = setTimeout(function() {
d3.select("input[value=\"females\"]").property("checked", true).each(change);}, 2000);
function change() {
var path = svg.datum(data).selectAll("path")
var value = this.value;
clearTimeout(timeout);
pie.value(function(d) { return d[value]; }); //change value function
tip.html(function(d) { return "Victims:" + " " + d[value]; }); //change the tooltip
path = path.data(pie); //compute new angles
path.transition().duration(1800).attrTween("d", arcTween); //redraw the arcs, please!
}
function type(d) {
d.females = +d.females;
d.males = +d.males;
d.totalNumber = +d.totalNumber;
return d;
}
//Store displayed angles in _current
//Interpolate them from _current to new angles
//_current is updated in-place by d3.interpolate during transition
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
I know that the key lies in the change() function and within this line:
tip.html(function(d) { return "Victims:" + " " + d[value]; }); //change the tooltip
But d[value] is undefined when I try to use it on that line. I'm just not sure what else it could be..
edit:
Here's the graphdata.php file contents:
<?php include("connectDB.php");
$query = "SELECT sum(sex ='M') males, sum(sex ='F') females, CASE
WHEN age BETWEEN 7 AND 15 THEN '7-15'
WHEN age <= 24 THEN '16-24'
WHEN age <= 33 THEN '25-33'
WHEN age <= 42 THEN '34-42'
WHEN age <= 51 THEN '43-51'
WHEN age <= 52 THEN '52-60'
WHEN age <= 70 THEN '61-70'
END AS age,
COUNT(*) AS totalNumber
FROM people
GROUP BY CASE
WHEN age <= 15 THEN '7-15'
WHEN age <= 24 THEN '16-24'
WHEN age <= 33 THEN '25-33'
WHEN age <= 42 THEN '34-42'
WHEN age <= 51 THEN '43-51'
WHEN age <= 52 THEN '52-60'
WHEN age <= 70 THEN '61-70'
END";
$data = array();
if($result = mysqli_query($db,$query)) {
while ($row = mysqli_fetch_assoc($result))
{
$data[] = $row;
}
}
echo json_encode($data, JSON_PRETTY_PRINT);
?>
I got a friend to help me out. What he ended up doing was this:
function change() {
var path = svg.datum(data).selectAll("path")
var value = this.value;
clearTimeout(timeout);
pie.value(function(d) { return d[value]; }); //change value function
tip.html(function(d) {
console.log($( "input:checked" ).val() );
if($( "input:checked" ).val() == "males") {
hoverValue = d.data.males;
}
if($( "input:checked" ).val() == "females") {
hoverValue = d.data.females;
}
if($( "input:checked" ).val() == "totalNumber") {
hoverValue = d.data.totalNumber;
}
return "Victims:" + " " +hoverValue;
}); //change the tooltip
path = path.data(pie); //compute new angles
path.transition().duration(1800).attrTween("d", arcTween); //redraw the arcs, please!
}
In particular, what has changed is the tip.html function. We take the value of whatever radio value input is checked and use it to determine what data to display. However, my data was located further down into the object, so we had to navigate like so: d.data.males, for example. We set that value to a variable and call it in the return so that it displays in the tooltip.
I am working on a query builder project. I am trying to build a query generator using d3.js. I am stucked in a part where I want to move certain elements inside a transforming group. This is the repo and I am stucked in this function. I want to move the operators after connecting it and update the connected lines. Can anyone help me?
var circleDrag = d3.behavior.drag()
.on('dragstart', function () {
d3.event.sourceEvent.stopPropagation();
})
.on('drag', function () {
var parentQboxGroupId = d3.select(this).select(function () {
return this.parentNode;
});
var grandParent = parentQboxGroupId.select(function(){
return this.parentNode;
});
var drawingGroup = d3.select('#'+grandParent.attr('id'));
var currentC = d3.select(this);
dragging = true;
drawingGroup
.select('.lineInsideQbox')
.attr('x1', currentC.attr('cx'))
.attr('y1', currentC.attr('cy'))
.style('stroke','green')
.style('stroke-width','2px');
dummyLine.src = currentC.attr('id');
console.log('CIRCLE IS BEING DRAGGED' + JSON.stringify(dummyLine));
})
.on('dragend', function () {
console.log('drag circle end');
//if(!selectedCircle.id){
// dummyLine.target = selectedQbox.id;
//}
dummyLine.target = selectedCircle.id;
dragging = false;
console.log('DRAG END : SELCTED NODE : '+ JSON.stringify(selectedCircle));
console.log('DRAG END : DUMMY LINE : '+ JSON.stringify(dummyLine));
var targetNode = d3.select('#'+dummyLine.target);
var srcNode = d3.select('#'+dummyLine.src);
console.log('split : ' + dummyLine.src.split('--'));
var group = '#' + (dummyLine.src).split('--')[1];
console.log('G: ' + group);
d3.select(group).append('line')
.attr('id', function () {
var a = (dummyLine.src).split('--');
var b = (dummyLine.target).split('--');
if( a[0]== 'nodeRight'){
return dummyLine.src + '__' + dummyLine.target;
}else{
return dummyLine.target + '__' + dummyLine.src;
}
})
.attr('class', function () {
var a = (dummyLine.src).split('--');
var b = (dummyLine.target).split('--');
return 'line '+ a[1]+' '+b[1];
})
.attr('x1', srcNode.attr('cx'))
.attr('y1',srcNode.attr('cy'))
.attr('x2',targetNode.attr('cx'))
.attr('y2',targetNode.attr('cy'))
.style('stroke', 'black')
.style('stroke-width', '3px')
;
dummyLine.src = null;
dummyLine.target = null;
});
EDIT : When I try to drop a query Box. I can drop other operators inside it. Then I should be able to connect them inside. Here is the image showing what I am trying.
After the connections made, I try yo move large box & small operators individually. That's where the code break.
The main issue is that to move the operator, you use a translate to move the whole group ( tag) which includes the image, the two circles and the line. You then set the other end of the line using CX, CY values of the other operator it is connected to.
This wont work because the CX and CY values of the circles are not updated when you perform a translate, so on a second move, it would put the x, y values at the original point of the circles, not to the moved point.
To resolve, instead of translating the whole group, translate only the image, update the cx and cy values of the circles and then update the line x, y values with the new cx, cy of the circles:
All of the amendments needed are within your operatorDrag.js file.
First of all, when you append the circles, add an attribute which holds the original cx and cy values. We will need these when calculating the new cx, cy when dragging the operator:
change from this:
var op = currGroup
.append('image')
.attr('class', 'operator')
.attr('width', elem.attr('width') * 0.75)
.attr('height', elem.attr('height') * 0.75)
.attr('x', d3.mouse(this)[0])
.attr('y', d3.mouse(this)[1])
.attr('xlink:href', elem.attr('href'));
currGroup
.append('circle')
.attr('class', 'node nodeLeft')
.attr('id', function () {
return 'nodeLeft--' + currGroup.attr('id');
})
.attr('cx', op.attr('x'))
.attr('cy', op.attr('height') / 2 + Number(op.attr('y')))
.attr('r', 5)
.style('fill', 'red')
.on('mouseover', function () {
selectedCircle = {
id: d3.select(this).attr('id'),
cls: 'nodeLeft'
}
})
.call(circleDrag)
;
currGroup
.append('circle')
.attr('class', 'node nodeRight')
.attr('id', function () {
return 'nodeRight--' + currGroup.attr('id');
})
.attr('cx', Number(op.attr('x')) + Number(op.attr('width')))
.attr('cy', op.attr('height') / 2 + Number(op.attr('y')))
.attr('r', 5)
.style('fill', 'red')
.on('mouseover', function () {
selectedCircle = {
id: d3.select(this).attr('id'),
cls: 'nodeRight'
}
})
.call(circleDrag)
;
To this (the updated code is contained in comments starting with #SB):
var op = currGroup
.append('image')
.attr('class', 'operator')
.attr('width', elem.attr('width') * 0.75)
.attr('height', elem.attr('height') * 0.75)
.attr('x', d3.mouse(this)[0])
.attr('y', d3.mouse(this)[1])
.attr('xlink:href', elem.attr('href'));
currGroup
.append('circle')
.attr('class', 'node nodeLeft')
.attr('id', function () {
return 'nodeLeft--' + currGroup.attr('id');
})
.attr('cx', op.attr('x'))
.attr('cy', op.attr('height') / 2 + Number(op.attr('y')))
// #SB: add a reference to the original cx and cy position.
// we will need it to set new cx cy when moving operator
.attr('data-cx', op.attr('x'))
.attr('data-cy', op.attr('height') / 2 + Number(op.attr('y')))
//----------------------------------------------------------------------
.attr('r', 5)
.style('fill', 'red')
.on('mouseover', function () {
selectedCircle = {
id: d3.select(this).attr('id'),
cls: 'nodeLeft'
}
})
.call(circleDrag)
;
currGroup
.append('circle')
.attr('class', 'node nodeRight')
.attr('id', function () {
return 'nodeRight--' + currGroup.attr('id');
})
.attr('cx', Number(op.attr('x')) + Number(op.attr('width')))
.attr('cy', op.attr('height') / 2 + Number(op.attr('y')))
// #SB: add a reference to the original cx and cy position.
// we will need it to set new cx cy when moving operator
.attr('data-cx', Number(op.attr('x')) + Number(op.attr('width')))
.attr('data-cy', op.attr('height') / 2 + Number(op.attr('y')))
//----------------------------------------------------------------------
.attr('r', 5)
.style('fill', 'red')
.on('mouseover', function () {
selectedCircle = {
id: d3.select(this).attr('id'),
cls: 'nodeRight'
}
})
.call(circleDrag)
;
Once you have done this, go to your on drag method for the operators. This is the code we are going to change:
.on('drag', function () {
var g = d3.select(this);
var currentOp = g.select('.operator');
var parent = g.select(function () {
return this.parentNode;
}).select('.qbox');
var dx = d3.event.x;
var dy = d3.event.y;
var mouse = {dx: d3.event.x, dy: d3.event.y};
var currentObj = {
x: currentOp.attr('x'),
y: currentOp.attr('y'),
width: currentOp.attr('width'),
height: currentOp.attr('height')
};
var parentObj = {
x: parent.attr('x'),
y: parent.attr('y'),
width: parent.attr('width'),
height: parent.attr('height')
};
//console.log('parent width : ' + parent.attr('width'));
//console.log('parent width : ' + currentOp.attr('width'));
//g.attr('transform', 'translate(' + x + ',' + y + ')');
var loc = getXY(mouse, currentObj, parentObj);
g.attr('transform', 'translate(' + loc.x + ',' + loc.y + ')');
d3.select('#' + g.attr('id')).selectAll('.line')[0].forEach(function (e1) {
var line = d3.select(e1);
console.log('-------------------');
console.log('line : ' + line.attr('id'));
console.log('-------------------');
var split = line.attr('id').split('__');
if(g.attr('id') == split[0]){
//change x2, y2
var otherNode = d3.select('#'+split[1]);
line.attr('x2', otherNode.attr('cx'));
line.attr('y2', otherNode.attr('cy'));
}else{
var otherNode = d3.select('#'+split[0]);
line.attr('x1', otherNode.attr('cx'));
line.attr('y1', otherNode.attr('cy'));
}
})
}))
First thing is, do not translate the whole object, only the image:
var g = d3.select(this);
var currentOp = g.select('.operator');
var parent = g.select(function () {
return this.parentNode;
}).select('.qbox');
//#SB: added a reference to the parent id
var parent_id = g.select(function () {
return this.parentNode;
}).attr('id');
//---------------------------------------
var dx = d3.event.x;
var dy = d3.event.y;
var mouse = {dx: d3.event.x, dy: d3.event.y};
var currentObj = {
x: currentOp.attr('x'),
y: currentOp.attr('y'),
width: currentOp.attr('width'),
height: currentOp.attr('height')
};
var parentObj = {
x: parent.attr('x'),
y: parent.attr('y'),
width: parent.attr('width'),
height: parent.attr('height')
};
var loc = getXY(mouse, currentObj, parentObj);
//#SB: Do not translate everything, the cx, cy values of the circle are not updated
// when translating which will make future moves calculate incorrectly
g.selectAll('image').attr('transform', 'translate(' + loc.x + ',' + loc.y + ')');
Then, instead of translating the circles, change their cx, and cy values using the original cx, cy and translate values:
g.selectAll('circle')
.attr('cx', function () {
return parseFloat(d3.select(this).attr('data-cx')) + parseFloat(loc.x);
})
.attr('cy', function () {
return parseFloat(d3.select(this).attr('data-cy')) + parseFloat(loc.y);
});
The last thing is the update to the lines. In your original code, you were selecting all lines within the operator group but you will actually miss some lines by only selecting this group. Some lines can be a part of another operator group but be connected to the operator that is moving.
In this case we should select all lines within the parent group and check if the line is connected to the operator we are moving.
If it is connected then we update the x and y values:
//#SB: Select all the lines in the parent group instead of only group of the
// operator we are moving. There can be lines that exists on other groups that
// do not exist within the group that is being moved.
d3.select('#' + parent_id).selectAll('.line')[0].forEach(function (el) {
var parent_id = g.attr('id')
var line = d3.select(el)
var nodeType = line.attr('id').split("__"); // id tells us if the line is connected to the left or right node
var operators = line.attr('class').split(" "); // class holds info on what operators the line is connected to
var sourceCircleId = nodeType[0].split("--")[0] + '--' + operators[1];
var targetCircleId = nodeType[1].split("--")[0] + '--' + operators[2];
if (parent_id == operators[1] || parent_id == operators[2]) { // the line is connected to the operator we are moving
line.attr('x1', d3.select('#' + sourceCircleId).attr('cx'))
line.attr('y1', d3.select('#' + sourceCircleId).attr('cy'))
line.attr('x2', d3.select('#' + targetCircleId).attr('cx'))
line.attr('y2', d3.select('#' + targetCircleId).attr('cy'))
}
});
Complete OnDrag code:
.on('drag', function () {
var g = d3.select(this);
var currentOp = g.select('.operator');
var parent = g.select(function () {
return this.parentNode;
}).select('.qbox');
//#SB: added a reference to the parent id
var parent_id = g.select(function () {
return this.parentNode;
}).attr('id');
//---------------------------------------
var dx = d3.event.x;
var dy = d3.event.y;
var mouse = {dx: d3.event.x, dy: d3.event.y};
var currentObj = {
x: currentOp.attr('x'),
y: currentOp.attr('y'),
width: currentOp.attr('width'),
height: currentOp.attr('height')
};
var parentObj = {
x: parent.attr('x'),
y: parent.attr('y'),
width: parent.attr('width'),
height: parent.attr('height')
};
var loc = getXY(mouse, currentObj, parentObj);
//#SB: Do not translate everything, the cx, cy values of the circle are not updated
// when translating which will make future moves calculate incorrectly
g.selectAll('image').attr('transform', 'translate(' + loc.x + ',' + loc.y + ')');
g.selectAll('circle')
.attr('cx', function () {
return parseFloat(d3.select(this).attr('data-cx')) + parseFloat(loc.x);
})
.attr('cy', function () {
return parseFloat(d3.select(this).attr('data-cy')) + parseFloat(loc.y);
});
//#SB: Select all the lines in the parent group instead of only group of the
// operator we are moving. There can be lines that exists on other groups that
// do not exist within the group that is being moved.
d3.select('#' + parent_id).selectAll('.line')[0].forEach(function (el) {
var parent_id = g.attr('id')
var line = d3.select(el)
var nodeType = line.attr('id').split("__"); // id tells us if the line is connected to the left or right node
var operators = line.attr('class').split(" "); // class holds info on what operators the line is connected to
var sourceCircleId = nodeType[0].split("--")[0] + '--' + operators[1];
var targetCircleId = nodeType[1].split("--")[0] + '--' + operators[2];
if (parent_id == operators[1] || parent_id == operators[2]) { // the line is connected to the operator we are moving
line.attr('x1', d3.select('#' + sourceCircleId).attr('cx'))
line.attr('y1', d3.select('#' + sourceCircleId).attr('cy'))
line.attr('x2', d3.select('#' + targetCircleId).attr('cx'))
line.attr('y2', d3.select('#' + targetCircleId).attr('cy'))
}
});
}))
I can't tell if I'm not understanding something Javascript-specific, or if I'm not understanding how D3 is supposed to does things.
I'm simply trying to create a reusable way of generating these: http://bl.ocks.org/mbostock/5100636
The following works just fine:
function ArcGraph(selector, color) {
this.width = 80;
this.height = 80;
var arc = d3.svg.arc().innerRadius(25).outerRadius(40).startAngle(0);
var svg = d3.select("body').append("svg").attr("width", this.width).attr("height", this.height).append("g").attr("transform", "translate(" + this.width * .5 + "," + this.height * .5 + ")");
this.background = svg.append("path").datum({endAngle: tau}).style("fill", "#f5f8fd").attr("d", arc);
this.foreground = svg.append("path").datum({endAngle: .127 * tau}).style("fill", color).attr("d", arc);
this.foreground.transition().duration(750).call(arcTween, Math.random() * tau);
function arcTween(transition, newAngle) {
transition.attrTween("d", function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc(d);
};
});
}
}
But when I try and properly prototype it everything into functions, it stops working:
function ArcGraph(selector, color) {
this.width = 80;
this.height = 80;
this.arc = d3.svg.arc().innerRadius(25).outerRadius(40).startAngle(0);
this.svg = d3.select("body').append("svg").attr("width", this.width).attr("height", this.height).append("g").attr("transform", "translate(" + this.width * .5 + "," + this.height * .5 + ")");
this.background = this.svg.append("path").datum({endAngle: tau}).style("fill", "#f5f8fd").attr("d", this.arc);
this.foreground = thus.svg.append("path").datum({endAngle: .127 * tau}).style("fill", color).attr("d", this.arc);
}
ArcGraph.prototype.renderArc = function() {
this.foreground.transition().duration(750).call(arcTween, Math.random() * tau);
function arcTween(transition, newAngle) {
transition.attrTween("d", function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return this.arc(d);
};
});
}
}
The problem all lies in the "return this.arc(d)" moment. I hundreds of errors like this:
Error: Problem parsing d="MNaN,NaNA160,160 0 1,1 114.55205494059831,-111.70419288856652L71.59503433787394,-69.81512055535408A100,100 0 1,0 NaN,NaNZ"
What am I not understanding here? It's not like it doesn't see the variable 'this.arc'. Why would everything work when I declare arc as a variable, and not work in this latter case?
this.arc inside a function, which is inside a method, is not the this.arc of your ArcGraph.
Check this out: http://jsfiddle.net/58wTN/
I'm trying to give a polygon - drawn with d3 - smooth edges using the d3.svg.line().interpolate() option but I get strange looking results.
I receive the polygon data from the nokia HERE api as world coordinate data in the form [lat1, long1, alt1, lat2, long2, alt2 ...] So in the routingCallback function - which is called when the response is in - I first refine it so it looks like this [[lat1, long1], [lat2, long2] ...]. In d3.svg.line() I then use this array of coordinates to calculate the pixel positions. Im using Leaflet to draw the polygon on a map so I use the map.latLngToLayerPoint() function to do that. The actual drawing of the polygon happens in reset() which is called from the routingCallback immediately after the data is available and every time the map gets zoomed
var map = new L.Map("map", {"center": [52.515, 13.38], zoom: 12})
.addLayer(new L.TileLayer('http://{s}.tile.cloudmade.com/---account key---/120322/256/{z}/{x}/{y}.png'));
map.on("viewreset", reset);
var svg = d3.select(map.getPanes().overlayPane).append("svg"),
g = svg.append("g").attr("class", "leaflet-zoom-hide group-element"),
bounds = [[],[]],
polygon,
refinedData,
line = d3.svg.line()
.x(function(d) {
var location = L.latLng(d[0], d[1]),
point = map.latLngToLayerPoint(location);
return point.x;
})
.y(function(d) {
var location = L.latLng(d[0], d[1]),
point = map.latLngToLayerPoint(location);
return point.y;
})
.interpolate("cardinal"),
routingCallback = function(observedRouter, key, value) {
if(value == "finished") {
var rawData = observedRouter.calculateIsolineResponse.isolines[0].asArray(),
refinedData = [];
for(var i = 2; i < rawData.length; i += 3) {
var lon = rawData[i-1],
lat = rawData[i-2];
refinedData.push([lat, lon]);
}
if(polygon)
polygon.remove();
polygon = g
.data([refinedData])
.append("path")
.style("stroke", "#000")
.style("fill", "none")
.attr("class", "isoline");
reset();
}
if(value == "failed") {
console.log(observedRouter.getErrorCause());
}
};
getIsolineData = function(isoline) {
return data;
};
function reset() {
var xExtent = d3.extent(refinedData, function(d) {
var location = L.latLng(d[0], d[1]);
var point = map.latLngToLayerPoint(location);
return point.x;
});
var yExtent = d3.extent(refinedData, function(d) {
var location = L.latLng(d[0], d[1]);
var point = map.latLngToLayerPoint(location);
return point.y;
});
bounds[0][0] = xExtent[0];
bounds[0][1] = yExtent[0];
bounds[1][0] = xExtent[1];
bounds[1][1] = yExtent[1];
var topLeft = bounds[0],
bottomRight = bounds[1];
svg .attr("width", bottomRight[0] - topLeft[0])
.attr("height", bottomRight[1] - topLeft[1])
.style("left", topLeft[0] + "px")
.style("top", topLeft[1] + "px");
g .attr("transform", "translate(" + -topLeft[0] + "," + -topLeft[1] + ")");
polygon.attr("d", line);
}
I expect this to produce smooth edges but instead I get a small loop at every corner. The red overlay is the same polygon without interpolation. There are only points at the corners. No points added inbetween.
Does it have something to do with the order of the points (clockwise/counter clockwise)? I tried to rearrange the points but nothing seemed to happen.
The only way I can recreate the pattern you're getting is if I add every vertex to the path twice. That wouldn't be noticeable with a linear interpolation, but causes the loops when the program tries to connect points smoothly.
http://fiddle.jshell.net/weuLs/
Edit:
Taking a closer look at your code, it looks like the problem is in your calculateIsolineResponse function; I don't see that name in the Leaflet API so I assume it's custom code. You'll need to debug that to figure out why you're duplicating points.
If you can't change that code, the simple solution would be to run your points array through a filter which removes the duplicated points:
refinedData = refinedData.filter(function(d,i,a){
return ( (!i) || (d[0] != a[i-1][0]) || (d[1] != a[i-1][1]) );
});
That filter will return true if either it's the first point in the array, or if either the lat or lon value is different from the previous point. Duplicated points will return false and be filtered out of the array.
I have a map which has been translated to make it fit on the canvas properly.
I'm trying to implement a way to zoom it and it does work, but it moves away from center when you zoom in, rather than centering on the mouse or even the canvas.
This is my code:
function map(data, total_views) {
var xy = d3.geo.mercator().scale(4350),
path = d3.geo.path().projection(xy),
transX = -320,
transY = 648,
init = true;
var quantize = d3.scale.quantize()
.domain([0, total_views*2/Object.keys(data).length])
.range(d3.range(15).map(function(i) { return "map-colour-" + i; }));
var map = d3.select("#map")
.append("svg:g")
.attr("id", "gb-regions")
.attr("transform","translate("+transX+","+transY+")")
.call(d3.behavior.zoom().on("zoom", redraw));
d3.json(url_prefix + "map/regions.json", function(json) {
d3.select("#regions")
.selectAll("path")
.data(json.features)
.enter().append("svg:path")
.attr("d", path)
.attr("class", function(d) { return quantize(data[d.properties.fips]); });
});
function redraw() {
var trans = d3.event.translate;
var scale = d3.event.scale;
if (init) {
trans[0] += transX;
trans[1] += transY;
init = false;
}
console.log(trans);
map.attr("transform", "translate(" + trans + ")" + " scale(" + scale + ")");
}
}
I've found that adding the initial translation to the new translation (trans) works for the first zoom, but for all subsequent zooms it makes it worse. Any ideas?
Here's a comprehensive starting-point: semantic zooming of force directed graph in d3
And this example helped me specifically (just rip out all the minimap stuff to make it simpler): http://codepen.io/billdwhite/pen/lCAdi?editors=001
var zoomHandler = function(newScale) {
if (!zoomEnabled) { return; }
if (d3.event) {
scale = d3.event.scale;
} else {
scale = newScale;
}
if (dragEnabled) {
var tbound = -height * scale,
bbound = height * scale,
lbound = -width * scale,
rbound = width * scale;
// limit translation to thresholds
translation = d3.event ? d3.event.translate : [0, 0];
translation = [
Math.max(Math.min(translation[0], rbound), lbound),
Math.max(Math.min(translation[1], bbound), tbound)
];
}
d3.select(".panCanvas, .panCanvas .bg")
.attr("transform", "translate(" + translation + ")" + " scale(" + scale + ")");
minimap.scale(scale).render();
}; // startoff zoomed in a bit to show pan/zoom rectangle
Though I had to tweak that function a fair bit to get it working for my case, but the idea is there. Here's part of mine. (E.range(min,max,value) just limits value to be within the min/max. The changes are mostly because I'm treating 0,0 as the center of the screen in this case.
// limit translation to thresholds
var offw = width/2*scale;
var offh = height/2*scale;
var sw = width*scale/2 - zoomPadding;
var sh = height*scale/2- zoomPadding;
translate = d3.event ? d3.event.translate : [0, 0];
translate = [
E.range(-sw,(width+sw), translate[0]+offw),
E.range(-sh,(height+sh), translate[1]+offh)
];
}
var ts = [translate[0], translate[1]];
var msvg = [scale, 0, 0, scale, ts[0], ts[1]];