Related
I am creating a map for a school project, involving the click-to-zoom function here and a scale bar from the model here without the zoom function from the script. I managed to program both in my source code, but I would like the scale bar to respond correctly when I click-to-zoom on a country, by having the right values adapted to the scale I am zooming to.
Here is the function for the click-to-zoom :
function clicked(d) {
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = .9 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
pays.selectAll("path").transition().duration(750).style("stroke-width", 1.5 / scale +
"px").style("width", 1.5 / scale + "px").style("height", 1.5 / scale + "px").attr("transform",
"translate(" + translate + ")scale(" + scale + ")");
villes.selectAll("path").transition().duration(750).style("stroke-width", 1.5 / scale +
"px").attr("transform", "translate(" + translate + ")scale(" + scale + ")");
capitales.selectAll("path").transition().duration(750).style("stroke-width", 1.5 / scale +
"px").attr("transform", "translate(" + translate + ")scale(" + scale + ")");
labels.selectAll("text").transition().duration(750).style("font-size", 11 / scale +
"px").attr("transform", "translate(" + translate + ")scale(" + scale + ")");
}
And here is the one from the scalebar :
// Start Scale ---------------------------------------------------------
function scale() {
// baseWidth refers to ideal scale width on the screen it also is the width of the initial measurement point
var g = svg.append("g");
var baseWidth = width / 4;
var p1 = projection.invert([width/2 - baseWidth/2, height / 2]);
var p2 = projection.invert([width/2 + baseWidth/2, height / 2]);
var distance = getDistance(p1,p2);
var unit = "m";
var multiply = 1;
var bestFit = 1;
var increment = 0.1; // This could be scaled to map width maybe width/10000;
var scaleDistance = 0;
var scaleWidth = 0;
if ( distance > 1000 ) {
unit = "km"; multiply = 0.001;
}
// Adjust distance to a round(er) number
var i = 0;
while (i < 400) {
var temp = getDistance( projection.invert([ width/2 - (baseWidth / 2) + (increment * i), height / 2 ]), projection.invert([ width/2 + baseWidth/2 - (increment * i), height / 2 ]));
var ratio = temp / temp.toPrecision(1);
// If the second distance is moving away from a cleaner number, reverse direction.
if (i == 1) {
if (Math.abs(1 - ratio) > bestFit) { increment = - increment; }
}
// If we are moving away from a best fit after that, break
else if (i > 2) {
if (Math.abs(1 - ratio) > bestFit) { break }
}
// See if the current distance is the cleanest number
if (Math.abs(1-ratio) < bestFit) {
bestFit = Math.abs(1 - ratio);
scaleDistance = temp;
scaleWidth = (baseWidth) - (2 * increment * i);
}
i++;
}
// Now to build the scale
var bars = [];
var smallBars = 10;
var bigBars = 4;
var odd = true;
var label = false;
// Populate an array to represent the bars on the scale
for (i = 0; i < smallBars; i++) {
if (smallBars - 1 > i ) { label = false; } else { label = true; }
bars.push( {width: 1 / (smallBars * (bigBars + 1)), offset: i / (smallBars * (bigBars + 1)), label: label, odd: odd } );
odd = !odd;
}
for (i = 0; i < bigBars; i++) {
bars.push( {width: 1 / (bigBars + 1), offset: (i + 1) / (bigBars + 1), label: true, odd: odd } );
odd = !odd;
}
// Append the scale
var scaleBar = g.selectAll(".scaleBar")
.data(bars);
// enter bars with no width
scaleBar
.enter()
.append("rect")
.attr("x", 20)
.attr("y", height - 40)
.attr("height",20)
.attr("width",0)
.attr("class","scaleBar")
.merge(scaleBar) // merge so that rect are updates if they are in the enter selection or the update selection.
.transition()
.attr("x", function(d) { return d.offset * scaleWidth + 20 })
//.attr("y", height - 30)
.attr("width", function(d) { return d.width * scaleWidth})
//.attr("height", 10)
.attr("fill", function (d) { if (d.odd) { return "#eee"; } else { return "#222"; } })
.duration(1000);
g.selectAll(".scaleText").remove();
g.selectAll(".scaleText")
.data(bars).enter()
.filter( function (d) { return d.label == true })
.append("text")
.attr("class","scaleText")
.attr("x",0)
.attr("y",0)
.style("text-anchor","start")
.text(function(d) { return d3.format(",")(((d.offset + d.width) * scaleDistance).toPrecision(2) * multiply); })
.attr("transform", function(d) { return "translate("+ ((d.offset + d.width) * scaleWidth + 20 )+","+ (height - 45) +") rotate(-45)" })
.style("opacity",0)
.transition()
.style("opacity",1)
.duration(1000);
g.append("text")
.attr("x", scaleWidth/2 + 20)
.attr("y", height - 5)
.text( function() { if(unit == "km") { return "Kilometres"; } else { return "metres";} })
.style("text-anchor","middle")
.attr("class","scaleText")
.style("opacity",0)
.transition()
.style("opacity",1)
.duration(1000);
}
// End Scale -----------------------------------------
scale();
function getDistance(p1,p2) {
var lat1 = p1[1];
var lat2 = p2[1];
var lon1 = p1[0];
var lon2 = p2[0];
var R = 6371e3; // metres
var φ1 = lat1* Math.PI / 180;
var φ2 = lat2* Math.PI / 180;
var Δφ = (lat2-lat1)* Math.PI / 180;
var Δλ = (lon2-lon1)* Math.PI / 180;
var a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ/2) * Math.sin(Δλ/2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
var distance = R * c;
return distance;
}
My level in d3.js mapping is weak, I will be thankful for any clues or solutions !
The scale bar example assumes that two coordinates on either side (horizontally) of [width/2,height/2] are representative of the map. This center coordinate is fixed. The example uses projection.invert() to calculate the real world distance between these two points.
The zoom example uses an SVG transform to zoom and pan the map. Updating the zoom transform is done independently of the projection, so projection.invert() will always return the same distance as long as the coordinates are fixed and independent of the transform.
In this example, the scale bar is updated when the projection is updated, but we can modify it so that we can take into account a zoom transform as well.
The scale bar example uses the following to get the two initial reference center points:
var baseWidth = width / 4;
var p1 = projection.invert([width/2 - baseWidth/2, height / 2]);
var p2 = projection.invert([width/2 + baseWidth/2, height / 2]);
This question covers how to convert coordinates to zoom coordinates:
var xy = d3.mouse(this); // relative to specified container
var transform = d3.zoomTransform(selection.node());
var xy1 = transform.invert(xy); // relative to zoom
So we can make a few modifications:
// Points relative to parent container:
var xy1 = [width/2 - baseWidth/2, height/2];
var xy2 = [width/2 + baseWidth/2, height/2];
// Zoom transform:
var transform = d3.zoomTransform(g.node());
// Points relative to zoom:
xy1 = transform.invert(xy1);
xy2 = transform.invert(xy2);
Now we have two points with the zoom transform applied and where they need to be. We can now proceed as before with one other change. The original scale bar example adjusted its length to a nice round number using:
var temp = getDistance( projection.invert([ width/2 - (baseWidth / 2) + (increment * i), height / 2 ]), projection.invert([ width/2 + baseWidth/2 - (increment * i), height / 2 ]));
We need to update this to use the points relative to the zoom transform and scale the incremental length in accordance with the zoom scale:
var temp = getDistance( projection.invert([xy1[0] + (increment * i/transform.k), xy1[1]]), projection.invert([ xy2[0] - (increment * i/transform.k), xy2[1] ]));
There are simpler ways to get round numbers then the approach I've taken in the example, but as screen distances are often not linear, this may introduce more error than otherwise needed
Here's an updated example (I have placed the zoom in it's own g container so as to not apply the zoom to it - there are a few changes due to this. I've also been lazy: the zoom only interacts with the land in this example).
Thank you very much, I really appreciate the effort you did for solving my problem. But I'm afraid I might not quite get the complete way to include your code. Here is the new version, I still don't get a change in the scalebar values by clicking on entities to zoom.
var startYear = 1990,
currentYear = startYear;
const width = 960, height = 600;
const path = d3.geoPath();
const projection = d3.geoMercator()
.center([9, 47])
.scale(1000)
.translate([width/2, height/2]);
path.projection(projection);
const svg = d3.select('#carte')
.append("svg")
.attr("id", "svg")
.attr("width", width)
.attr("height", height);
/***************************************************************************/
/*************************************** AJOUT DES OBJETS SUR LA CARTE *****/
/***************************************************************************/
const pays = svg.append("g");
pays.selectAll("path")
// La variable geojson est créée dans le fichier JS qui contient le GeoJSON
.data(geojson_ue.features)
.enter()
.append("path")
.attr("d", path)
// Sémiologie (par défaut) des objets
.style("fill", "#e6e6e6")
.style("stroke-width", 3)
.style("stroke", "#fff");
const pays2 = svg.append("g");
pays.selectAll("path")
// La variable geojson est créée dans le fichier JS qui contient le GeoJSON
.data(geojson_pays.features)
.enter()
.append("path")
.attr("d", path)
// Sémiologie (par défaut) des objets
.style("fill", "rgba(232, 232, 232,0.8)")
.style("stroke-width", .5)
.style("stroke", "#fff");
const rail = svg.append("g");
rail.selectAll("path")
// La variable geojson est créée dans le fichier JS qui contient le GeoJSON
.data(geojson_rail.features)
.enter()
.append("path")
.attr("d", path)
.attr("stroke-opacity",0)
.attr("fill-opacity",0)
.on("click", clicked);
// VILLES ET CAPITALES
const villes = svg.append("g");
villes.selectAll("path")
// La variable geojson est créée dans le fichier JS qui contient le GeoJSON
.data(geojson_villes.features)
.enter()
.append("path")
.attr("d", path)
// Sémiologie (par défaut) des objets
.style("fill", "black")
.style("stroke", "white")
.style("stroke-width", 0.5)
.attr("stroke-opacity",0)
.attr("fill-opacity",0);
const capitales = svg.append("g");
capitales.selectAll("path")
// La variable geojson est créée dans le fichier JS qui contient le GeoJSON
.data(geojson_capitales.features)
.enter()
.append("path")
.attr("d", path)
// Sémiologie (par défaut) des objets
.style("fill", "#180093")
;
/***************************************************************************/
/**************************** PREVOIR UNE ACTION AU CLIC SUR UN BOUTON *****/
/***************************************************************************/
//BOUTONS
$("#action1").click(function(){
affiche_rail(1980, projection.scale);
document.getElementById("legende").innerHTML = '<img src ="Untitled-1.png"/>';
});
$("#action2").click(function(){
affiche_rail(1990, projection.scale);
document.getElementById("legende").innerHTML = '<img src ="Untitled-2.png"/>';
});
$("#action3").click(function(){
affiche_rail(2000, projection.scale);
document.getElementById("legende").innerHTML = '<img src ="Untitled-3.png"/>';
});
$("#action4").click(function(){
affiche_rail(2010, projection.scale);
document.getElementById("legende").innerHTML = '<img src ="Untitled-4.png"/>';
});
$("#action5").click(function(){
affiche_rail(2020, projection.scale);
document.getElementById("legende").innerHTML = '<img src ="Untitled-5.png"/>';
});
$("#dezoom").click(function(){
var bounds = path.bounds(),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = 1 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
pays.selectAll("path").transition().duration(750).style("stroke-width", "1.5px").attr("transform", "translate(" + translate + ")scale(" + scale + ")");
villes.selectAll("path").transition().duration(750).style("stroke-width", 0.5).attr("d", path.pointRadius(4.5)).attr("transform", "translate(" + translate + ")scale(" + scale + ")");
capitales.selectAll("path").transition().duration(750).style("stroke-width", 3).attr("d", path.pointRadius(4.5)).attr("transform", "translate(" + translate + ")scale(" + scale + ")");
rail.selectAll("path").transition().duration(750).style("stroke-width", "2px").attr("transform", "translate(" + translate + ")scale(" + scale + ")");
});
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var tooltip2 = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
// FONCTION D'AFFICHAGE
function affiche_rail(date, scale){ // Affiche les lignes en fonction de la date de construction
rail.selectAll("path")
.attr("stroke", function(d,i){ // Couleur de contour des lignes concernées
if (geojson_rail.features[i].properties.Date_Const < date){
return "grey"
}
else if (geojson_rail.features[i].properties.Date_Const == date) {
return "red"
}
})
.attr("stroke-opacity", function(d,i){ // Opacité des contours des lignes
if (geojson_rail.features[i].properties.Date_Const <= date){
return "1"
}
else if (geojson_rail.features[i].properties.Date_Const > date) {
return "0"
}
})
.attr("fill-opacity", "0") // 0 sinon c'est moche
// Sémiologie (par défaut) des objets
.style("stroke-width", 2)
.style("stroke-dasharray", function(d,i){
if (geojson_rail.features[i].properties.Date_Const == 2020){
return 2
}
});
villes.selectAll("path")
.attr("stroke-opacity", function(d,i){ // Opacité des contours des points
if (geojson_villes.features[i].properties.date <= date){
return "1"
}
else if (geojson_villes.features[i].properties.date > date) {
return "0"
}
})
.attr("fill-opacity", function(d,i){ // Opacité des contours des points
if (geojson_villes.features[i].properties.date <= date){
return "1"
}
else if (geojson_villes.features[i].properties.date > date) {
return "0"
}
})
// Sémiologie (par défaut) des objets
.style("fill", "black")
.style("stroke", "white")
.style("stroke-width", 0.5);
// infos sur les villes au survol
villes.selectAll("path").filter(function(d) {
return d.properties.date <= date;
}).on("mouseover", function(d) {
d3.select(this)
.style("fill", "blue")
.style("stroke", "black")
.style("stroke-width", 1.5/scale)
.style("cursor", "pointer");
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d.properties.nom)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
}).on("mouseout", function(d) {
d3.select(this)
.style("fill", "black")
.style("stroke", "white")
.style("stroke-width", 2/scale)
tooltip.transition()
.duration(500)
.style("opacity", 0);
}).on("click",function(d){
// tooltip2.transition()
// .duration(200)
// .style("opacity", .9);
// tooltip2.html(d.properties.date)
// .style("left", (d3.event.pageX) + "px")
// .style("top", (d3.event.pageY - 28) + "px");
tooltip.html(d.properties.nom + "<br />Date : " + d.properties.date);
});
//infos sur les capitales au survol
capitales.selectAll("path").filter(function(d) {
return d.properties.nom != "NULL";
}).on("mouseover", function(d) {
d3.select(this)
.style("fill", "blue")
.style("stroke", "black")
.style("stroke-width", 1.5/scale)
.style("cursor", "pointer");
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d.properties.nom)
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
}).on("mouseout", function(d) {
d3.select(this)
.style("fill", "#180093")
.style("stroke", "rgba(0,0,0,0)")
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
// Changer le style des lignes au survol
rail.selectAll("path").filter(function(d) {
return d.properties.Date_Const <= date;
}).on("mouseover", function(d) {
d3.select(this)
.style("cursor", "pointer")
.style("fill-opacity", 0)
.style("stroke", "blue")
.style("stroke-width", 7/scale)
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html("Ligne " + d.properties.LGV + ". Ouverte en " + d.properties.ouverture + ".")
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
}).on("mouseout", function(d) {
d3.select(this)
.style("fill-opacity", 0)
.style("stroke", function(d,i){
if (geojson_rail.features[i].properties.Date_Const < date){
return "grey"
}
else if (geojson_rail.features[i].properties.Date_Const == date) {
return "red"
}
})
.style("stroke-width", 2/scale)
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
}
function clicked(d) {
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = .5 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
pays.selectAll("path").transition().duration(750).style("stroke-width", 1.5 / scale + "px").attr("transform", "translate(" + translate + ")scale(" + scale + ")");
pays2.selectAll("path").transition().duration(750).style("stroke-width", 1.5 / scale + "px").attr("transform", "translate(" + translate + ")scale(" + scale + ")");
villes.selectAll("path").transition().duration(750).style("stroke-width", 1.5 / scale + "px").attr("d", path.pointRadius(1.5)).attr("transform", "translate(" + translate + ")scale(" + scale + ")");
capitales.selectAll("path").transition().duration(750).attr("d", path.pointRadius(1.5)).attr("transform", "translate(" + translate + ")scale(" + scale + ")");
rail.selectAll("path").transition().duration(750).style("stroke-width", 1.5 / scale +"%").attr("transform", "translate(" + translate + ")scale(" + scale + ")");
}
// zoom
var zoom = d3.zoom()
.on("end",zoomed)
rail.call(zoom);
function zoomed() {
rail.attr("transform",d3.event.transform);
scale();
}
//*/
// Start Scale ---------------------------------------------------------
function scale() {
// baseWidth refers to ideal scale width on the screen it also is the width of the initial measurement point
var baseWidth = width / 4;
// Points relative to parent container:
var xy1 = [width/2 - baseWidth/2, height/2];
var xy2 = [width/2 + baseWidth/2, height/2];
// Zoom transform:
var transform = d3.zoomTransform(rail.node());
// Points relative to zoom:
xy1 = transform.invert(xy1);
xy2 = transform.invert(xy2);
// With a few changes below:
var p1 = projection.invert(xy1);
var p2 = projection.invert(xy2);
var distance = getDistance(p1,p2);
var unit = "m";
var multiply = 1;
var bestFit = 1;
var increment = 0.1; // This could be scaled to map width maybe width/10000;
var scaleDistance = 0;
var scaleWidth = 0;
if ( distance > 1000 ) {
unit = "km"; multiply = 0.001;
}
// Adjust distance to a round(er) number
var i = 0;
while (i < 400) {
var temp = getDistance( projection.invert([xy1[0] + (increment * i/transform.k), xy1[1]]), projection.invert([ xy2[0] - (increment * i/transform.k), xy2[1] ]));
var ratio = temp / temp.toPrecision(1);
// If the second distance is moving away from a cleaner number, reverse direction.
if (i == 1) {
if (Math.abs(1 - ratio) > bestFit) { increment = - increment; }
}
// If we are moving away from a best fit after that, break
else if (i > 2) {
if (Math.abs(1 - ratio) > bestFit) { break }
}
// See if the current distance is the cleanest number
if (Math.abs(1-ratio) < bestFit) {
bestFit = Math.abs(1 - ratio);
scaleDistance = temp;
scaleWidth = (baseWidth) - (2 * increment * i);
}
i++;
}
// Now to build the scale
var bars = [];
var smallBars = 10;
var bigBars = 4;
var odd = true;
var label = false;
// Populate an array to represent the bars on the scale
for (i = 0; i < smallBars; i++) {
if (smallBars - 1 > i ) { label = false; } else { label = true; }
bars.push( {width: 1 / (smallBars * (bigBars + 1)), offset: i / (smallBars * (bigBars + 1)), label: label, odd: odd } );
odd = !odd;
}
for (i = 0; i < bigBars; i++) {
bars.push( {width: 1 / (bigBars + 1), offset: (i + 1) / (bigBars + 1), label: true, odd: odd } );
odd = !odd;
}
// Append the scale
var scaleBar = rail.selectAll(".scaleBar")
.data(bars);
// enter bars with no width
scaleBar
.enter()
.append("rect")
.attr("x", 20)
.attr("y", height - 40)
.attr("height",20)
.attr("width",0)
.attr("class","scaleBar")
.merge(scaleBar) // merge so that rect are updates if they are in the enter selection or the update selection.
.transition()
.attr("x", function(d) { return d.offset * scaleWidth + 20 })
//.attr("y", height - 30)
.attr("width", function(d) { return d.width * scaleWidth})
//.attr("height", 10)
.attr("fill", function (d) { if (d.odd) { return "#eee"; } else { return "#222"; } })
.duration(1000);
rail.selectAll(".scaleText").remove();
rail.selectAll(".scaleText")
.data(bars).enter()
.filter( function (d) { return d.label == true })
.append("text")
.attr("class","scaleText")
.attr("x",0)
.attr("y",0)
.style("text-anchor","start")
.text(function(d) { return d3.format(",")(((d.offset + d.width) * scaleDistance).toPrecision(2) * multiply); })
.attr("transform", function(d) { return "translate("+ ((d.offset + d.width) * scaleWidth + 20 )+","+ (height - 45) +") rotate(-45)" })
.style("opacity",0)
.transition()
.style("opacity",1)
.duration(1000);
rail.append("text")
.attr("x", scaleWidth/2 + 20)
.attr("y", height - 5)
.text( function() { if(unit == "km") { return "kilometers"; } else { return "metres";} })
.style("text-anchor","middle")
.attr("class","scaleText")
.style("opacity",0)
.transition()
.style("opacity",1)
.duration(1000);
}
// End Scale -----------------------------------------
scale();
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* Latitude/longitude spherical geodesy tools (c) Chris Veness 2002-2016 */
/* MIT Licence */
/* www.movable-type.co.uk/scripts/latlong.html */
/* www.movable-type.co.uk/scripts/geodesy/docs/module-latlon-spherical.html */
function getDistance(p1,p2) {
var lat1 = p1[1];
var lat2 = p2[1];
var lon1 = p1[0];
var lon2 = p2[0];
var R = 6371e3; // metres
var φ1 = lat1* Math.PI / 180;
var φ2 = lat2* Math.PI / 180;
var Δφ = (lat2-lat1)* Math.PI / 180;
var Δλ = (lon2-lon1)* Math.PI / 180;
var a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ/2) * Math.sin(Δλ/2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
var distance = R * c;
return distance;
}
Just started learning meteor and d3 / crossfilter charting libraries.
Picked up some example code off the Web, and have it working in my local app.
I do have an empty this.autorun() function in my meteor client code, but have no idea what part of the lengthy d3 initialization and composition routine should be put into autorun, in order for these charts to react to the data changes.
I have tried to just put the Flights.find().fetch() inside the autorun, but in that case, the page never seem to finish loading.
Here is my entire meteor code:
if (Meteor.isClient) {
Template.dashboard.helpers({
});
Template.dashboard.events({
});
Template.dashboard.rendered = function(){
var flights = Flights.find().fetch();
if (!flights.length) return;
var crossData = crossfilter(flights);
// d3.csv(data, function(error, flights) {
// Various formatters.
var formatNumber = d3.format(",d"),
formatChange = d3.format("+,d"),
formatDate = d3.time.format("%B %d, %Y"),
formatTime = d3.time.format("%I:%M %p");
// A nest operator, for grouping the flight list.
var nestByDate = d3.nest()
.key(function(d) { return d3.time.day(d.date); });
// A little coercion, since the CSV is untyped.
flights.forEach(function(d, i) {
d.index = i;
d.date = parseDate(d.date);
d.delay = +d.delay;
d.distance = +d.distance;
});
// Create the crossfilter for the relevant dimensions and groups.
var flight = crossfilter(flights),
all = flight.groupAll(),
date = flight.dimension(function(d) { return d.date; }),
dates = date.group(d3.time.day),
hour = flight.dimension(function(d) { return d.date.getHours() + d.date.getMinutes() / 60; }),
hours = hour.group(Math.floor),
delay = flight.dimension(function(d) { return Math.max(-60, Math.min(149, d.delay)); }),
delays = delay.group(function(d) { return Math.floor(d / 10) * 10; }),
distance = flight.dimension(function(d) { return Math.min(1999, d.distance); }),
distances = distance.group(function(d) { return Math.floor(d / 50) * 50; });
var charts = [
barChart()
.dimension(hour)
.group(hours)
.x(d3.scale.linear()
.domain([0, 24])
.rangeRound([0, 10 * 24])),
barChart()
.dimension(delay)
.group(delays)
.x(d3.scale.linear()
.domain([-60, 150])
.rangeRound([0, 10 * 21])),
barChart()
.dimension(distance)
.group(distances)
.x(d3.scale.linear()
.domain([0, 2000])
.rangeRound([0, 10 * 40])),
barChart()
.dimension(date)
.group(dates)
.round(d3.time.day.round)
.x(d3.time.scale()
.domain([new Date(2001, 0, 1), new Date(2001, 3, 1)])
.rangeRound([0, 10 * 90]))
.filter([new Date(2001, 1, 1), new Date(2001, 2, 1)])
];
// Given our array of charts, which we assume are in the same order as the
// .chart elements in the DOM, bind the charts to the DOM and render them.
// We also listen to the chart's brush events to update the display.
var chart = d3.selectAll(".chart")
.data(charts)
.each(function(chart) { chart.on("brush", renderAll).on("brushend", renderAll); });
// Render the initial lists.
var list = d3.selectAll(".list")
.data([flightList]);
// Render the total.
d3.selectAll("#total")
.text(formatNumber(flight.size()));
renderAll();
// Renders the specified chart or list.
function render(method) {
d3.select(this).call(method);
}
// Whenever the brush moves, re-rendering everything.
function renderAll() {
chart.each(render);
list.each(render);
d3.select("#active").text(formatNumber(all.value()));
}
// Like d3.time.format, but faster.
function parseDate(d) {
return new Date(2001,
d.substring(0, 2) - 1,
d.substring(2, 4),
d.substring(4, 6),
d.substring(6, 8));
}
window.filter = function(filters) {
filters.forEach(function(d, i) { charts[i].filter(d); });
renderAll();
};
window.reset = function(i) {
charts[i].filter(null);
renderAll();
};
function flightList(div) {
var flightsByDate = nestByDate.entries(date.top(40));
div.each(function() {
var date = d3.select(this).selectAll(".date")
.data(flightsByDate, function(d) { return d.key; });
date.enter().append("div")
.attr("class", "date")
.append("div")
.attr("class", "day")
.text(function(d) { return formatDate(d.values[0].date); });
date.exit().remove();
var flight = date.order().selectAll(".flight")
.data(function(d) { return d.values; }, function(d) { return d.index; });
var flightEnter = flight.enter().append("div")
.attr("class", "flight");
flightEnter.append("div")
.attr("class", "time")
.text(function(d) { return formatTime(d.date); });
flightEnter.append("div")
.attr("class", "origin")
.text(function(d) { return d.origin; });
flightEnter.append("div")
.attr("class", "destination")
.text(function(d) { return d.destination; });
flightEnter.append("div")
.attr("class", "distance")
.text(function(d) { return formatNumber(d.distance) + " mi."; });
flightEnter.append("div")
.attr("class", "delay")
.classed("early", function(d) { return d.delay < 0; })
.text(function(d) { return formatChange(d.delay) + " min."; });
flight.exit().remove();
flight.order();
});
}
function barChart() {
if (!barChart.id) barChart.id = 0;
var margin = {top: 10, right: 10, bottom: 20, left: 10},
x,
y = d3.scale.linear().range([100, 0]),
id = barChart.id++,
axis = d3.svg.axis().orient("bottom"),
brush = d3.svg.brush(),
brushDirty,
dimension,
group,
round;
function chart(div) {
var width = x.range()[1],
height = y.range()[0];
y.domain([0, group.top(1)[0].value]);
div.each(function() {
var div = d3.select(this),
g = div.select("g");
// Create the skeletal chart.
if (g.empty()) {
div.select(".title").append("a")
.attr("href", "javascript:reset(" + id + ")")
.attr("class", "reset")
.text("reset")
.style("display", "none");
g = div.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.append("clipPath")
.attr("id", "clip-" + id)
.append("rect")
.attr("width", width)
.attr("height", height);
g.selectAll(".bar")
.data(["background", "foreground"])
.enter().append("path")
.attr("class", function(d) { return d + " bar"; })
.datum(group.all());
g.selectAll(".foreground.bar")
.attr("clip-path", "url(#clip-" + id + ")");
g.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height + ")")
.call(axis);
// Initialize the brush component with pretty resize handles.
var gBrush = g.append("g").attr("class", "brush").call(brush);
gBrush.selectAll("rect").attr("height", height);
gBrush.selectAll(".resize").append("path").attr("d", resizePath);
}
// Only redraw the brush if set externally.
if (brushDirty) {
brushDirty = false;
g.selectAll(".brush").call(brush);
div.select(".title a").style("display", brush.empty() ? "none" : null);
if (brush.empty()) {
g.selectAll("#clip-" + id + " rect")
.attr("x", 0)
.attr("width", width);
} else {
var extent = brush.extent();
g.selectAll("#clip-" + id + " rect")
.attr("x", x(extent[0]))
.attr("width", x(extent[1]) - x(extent[0]));
}
}
g.selectAll(".bar").attr("d", barPath);
});
function barPath(groups) {
var path = [],
i = -1,
n = groups.length,
d;
while (++i < n) {
d = groups[i];
path.push("M", x(d.key), ",", height, "V", y(d.value), "h9V", height);
}
return path.join("");
}
function resizePath(d) {
var e = +(d == "e"),
x = e ? 1 : -1,
y = height / 3;
return "M" + (.5 * x) + "," + y
+ "A6,6 0 0 " + e + " " + (6.5 * x) + "," + (y + 6)
+ "V" + (2 * y - 6)
+ "A6,6 0 0 " + e + " " + (.5 * x) + "," + (2 * y)
+ "Z"
+ "M" + (2.5 * x) + "," + (y + 8)
+ "V" + (2 * y - 8)
+ "M" + (4.5 * x) + "," + (y + 8)
+ "V" + (2 * y - 8);
}
}
brush.on("brushstart.chart", function() {
var div = d3.select(this.parentNode.parentNode.parentNode);
div.select(".title a").style("display", null);
});
brush.on("brush.chart", function() {
var g = d3.select(this.parentNode),
extent = brush.extent();
if (round) g.select(".brush")
.call(brush.extent(extent = extent.map(round)))
.selectAll(".resize")
.style("display", null);
g.select("#clip-" + id + " rect")
.attr("x", x(extent[0]))
.attr("width", x(extent[1]) - x(extent[0]));
dimension.filterRange(extent);
});
brush.on("brushend.chart", function() {
if (brush.empty()) {
var div = d3.select(this.parentNode.parentNode.parentNode);
div.select(".title a").style("display", "none");
div.select("#clip-" + id + " rect").attr("x", null).attr("width", "100%");
dimension.filterAll();
}
});
chart.margin = function(_) {
if (!arguments.length) return margin;
margin = _;
return chart;
};
chart.x = function(_) {
if (!arguments.length) return x;
x = _;
axis.scale(x);
brush.x(x);
return chart;
};
chart.y = function(_) {
if (!arguments.length) return y;
y = _;
return chart;
};
chart.dimension = function(_) {
if (!arguments.length) return dimension;
dimension = _;
return chart;
};
chart.filter = function(_) {
if (_) {
brush.extent(_);
dimension.filterRange(_);
} else {
brush.clear();
dimension.filterAll();
}
brushDirty = true;
return chart;
};
chart.group = function(_) {
if (!arguments.length) return group;
group = _;
return chart;
};
chart.round = function(_) {
if (!arguments.length) return round;
round = _;
return chart;
};
return d3.rebind(chart, brush, "on");
}
// });
this.autorun(function(){
})
}
}
if (Meteor.isServer) {
Meteor.startup(function () {
});
}
If this helps, here is my attempt at reproducing one of the d3 force layout examples with collision detection / custom gravity functions https://gist.github.com/gmlnchv/80dd206440cca39800b8. I'm using observe() to react to changes.
I have an openlayers 3 map where I can draw Polygon.
I would have returned the WKT String that rapresent the drawed polygon.
How Can I do it?
JSFiddle Code http://jsfiddle.net/michelejs/3zawt33b/7/
Here my map:
map = new ol.Map({
target: 'map',
layers: [raster,vector],
view: new ol.View({
center: ol.proj.fromLonLat([11.249367, 43.774298]),
zoom: 15
})
});
Here the intaractions that help me to draw the polygon:
function addInteraction() {
var ct = 0;
draw = new ol.interaction.Draw({
source: source,
type: 'Polygon',
geometryFunction: function (c, g) {
if (goog.isDef(g)) {
g.setCoordinates(c);
} else {
g = new ol.geom.Polygon(c);
}
if (c[0].length > ct) {
console.log('click coord : ' + c[0][c[0].length - 1]);
var coord = c[0][c[0].length - 1];
$('div#coordinate').html( $('div#coordinate').html() + "<p>" + ( Number(coord[0]).toFixed(2) ) + " - " + ( Number(coord[1]).toFixed(2) ) + "</p>" );
coordinates.push(coord);
ct = c[0].length;
} else {
console.log('move coord : ' + c[0][c[0].length - 1]);
}
return g;
}
});
draw.on('drawend', function(e) {
isin = e;
checkIfIn();
lastFeature = e.feature;
//write WKT Polygon Code in div#getAsWK
})
draw.on('drawstart', function (e) {
source.clear();
});
map.addInteraction(draw);
}
map.addInteraction(draw);
ol3 contains the ol.format.WKT class for this purpose.
Use writeGeometry() method like this:
var format = new ol.format.WKT(),
wkt = format.writeGeometry(yourFeature.getGeometry());
See API-docs: http://openlayers.org/en/v3.0.0/apidoc/ol.format.WKT.html#writeGeometry
Fiddle: http://jsfiddle.net/igor23/3zawt33b/9/
On d3js.org they have this sea of hexagons that is fully interactive, but there are no d3 docs that show how one would even start to make something like this.
From inspecting the source, you can see it's made with something called hexbin and d3js itself, but there's no other source code that actually helps understand how it's made.
Can anyone shed light on how they implemented this?
Thanks to Lars Kotthoff this is how they did it assuming you have a structure called data:
data.forEach(function(d, i) {
d.i = i % 10;
d.j = i / 10 | 0;
});
Math.seedrandom(+d3.time.hour(new Date));
d3.shuffle(data);
var height = 460,
imageWidth = 132,
imageHeight = 152,
radius = 75,
depth = 4;
var currentFocus = [innerWidth / 2, height / 2],
desiredFocus,
idle = true;
var style = document.body.style,
transform = ("webkitTransform" in style ? "-webkit-"
: "MozTransform" in style ? "-moz-"
: "msTransform" in style ? "-ms-"
: "OTransform" in style ? "-o-"
: "") + "transform";
var hexbin = d3.hexbin()
.radius(radius);
if (!("ontouchstart" in document)) d3.select("#examples")
.on("mousemove", mousemoved);
var deep = d3.select("#examples-deep");
var canvas = deep.append("canvas")
.attr("height", height);
var context = canvas.node().getContext("2d");
var svg = deep.append("svg")
.attr("height", height);
var mesh = svg.append("path")
.attr("class", "example-mesh");
var anchor = svg.append("g")
.attr("class", "example-anchor")
.selectAll("a");
var graphic = deep.selectAll("svg,canvas");
var image = new Image;
image.src = "ex.jpg?3f2d00ffdba6ced9c50f02ed42f12f6156368bd2";
image.onload = resized;
d3.select(window)
.on("resize", resized)
.each(resized);
function drawImage(d) {
context.save();
context.beginPath();
context.moveTo(0, -radius);
for (var i = 1; i < 6; ++i) {
var angle = i * Math.PI / 3,
x = Math.sin(angle) * radius,
y = -Math.cos(angle) * radius;
context.lineTo(x, y);
}
context.clip();
context.drawImage(image,
imageWidth * d.i, imageHeight * d.j,
imageWidth, imageHeight,
-imageWidth / 2, -imageHeight / 2,
imageWidth, imageHeight);
context.restore();
}
function resized() {
var deepWidth = innerWidth * (depth + 1) / depth,
deepHeight = height * (depth + 1) / depth,
centers = hexbin.size([deepWidth, deepHeight]).centers();
desiredFocus = [innerWidth / 2, height / 2];
moved();
graphic
.style("left", Math.round((innerWidth - deepWidth) / 2) + "px")
.style("top", Math.round((height - deepHeight) / 2) + "px")
.attr("width", deepWidth)
.attr("height", deepHeight);
centers.forEach(function(center, i) {
center.j = Math.round(center[1] / (radius * 1.5));
center.i = Math.round((center[0] - (center.j & 1) * radius * Math.sin(Math.PI / 3)) / (radius * 2 * Math.sin(Math.PI / 3)));
context.save();
context.translate(Math.round(center[0]), Math.round(center[1]));
drawImage(center.example = data[(center.i % 10) + ((center.j + (center.i / 10 & 1) * 5) % 10) * 10]);
context.restore();
});
mesh.attr("d", hexbin.mesh);
anchor = anchor.data(centers, function(d) { return d.i + "," + d.j; });
anchor.exit().remove();
anchor.enter().append("a")
.attr("xlink:href", function(d) { return d.example.url; })
.attr("xlink:title", function(d) { return d.example.title; })
.append("path")
.attr("d", hexbin.hexagon());
anchor
.attr("transform", function(d) { return "translate(" + d + ")"; });
}
function mousemoved() {
var m = d3.mouse(this);
desiredFocus = [
Math.round((m[0] - innerWidth / 2) / depth) * depth + innerWidth / 2,
Math.round((m[1] - height / 2) / depth) * depth + height / 2
];
moved();
}
function moved() {
if (idle) d3.timer(function() {
if (idle = Math.abs(desiredFocus[0] - currentFocus[0]) < .5 && Math.abs(desiredFocus[1] - currentFocus[1]) < .5) currentFocus = desiredFocus;
else currentFocus[0] += (desiredFocus[0] - currentFocus[0]) * .14, currentFocus[1] += (desiredFocus[1] - currentFocus[1]) * .14;
deep.style(transform, "translate(" + (innerWidth / 2 - currentFocus[0]) / depth + "px," + (height / 2 - currentFocus[1]) / depth + "px)");
return idle;
});
}
I have a createMarker() function that is used to place multiple markers on the google map. The following is the code:
function createMarker(point,custid,streetadd,city,state,zip,address,phone,website,co)
{
var infowindowHover,infowindowClick;
var marker = new google.maps.Marker({
position: point,
map: map,
icon: image,
});
var boxClickText = document.createElement("div");
boxClickText.className = "infoBackground";
var markerMarkup = "<div id='infobox'><TABLE class='test'><TR><TD colspan='2'><B class='title'>";
markerMarkup = markerMarkup + co + "</B></TD></TR><TR><TD colspan='2'>";
markerMarkup = markerMarkup + streetadd + "</TD></TR><TR><TD colspan='2'>";
markerMarkup = markerMarkup + city + "," + state + " " + zip + "</TD></TR><TR><TD colspan='2'>";
markerMarkup = markerMarkup + phone + "</TD></TR><TR><TD colspan='2'>";
if(website.indexOf("http://")>0) { markerMarkup = markerMarkup +"<a href="; }
else{ markerMarkup = markerMarkup +"<a href=http://"; }
markerMarkup = markerMarkup + website + " target=_blank>" + website + "</a></TD></TR><TR><TD class='availableStyle'>";
markerMarkup = markerMarkup +'see available styles';
//markerMarkup = markerMarkup + '<input type="button" value="see available styles" onclick="setstyles('+ custid +',\'' + streetadd + '\'' + ',\'' + city + '\'' + ',\'' + state + '\'' + ',\'' + zip + '\'' + ',\'' + address + '\''+ ',\'' + phone + '\''+ ',\'' + website + '\''+ ',\'' + co + '\'' + ')" />';
markerMarkup = markerMarkup + "</TD></TR></TABLE></div>";
boxClickText.innerHTML = markerMarkup;
var myOptions_click = {
content: boxClickText
//,disableAutoPan: true
,disableAutoPan: false
,maxWidth: 0
,pixelOffset: new google.maps.Size(-140, 0)
,zIndex: null
,boxStyle: {
//opacity: 0.75
//,width: "280px"
margin:"-58px 0px 0px 148px"
}
,closeBoxMargin: "10px 2px 2px 2px"
,closeBoxURL: "http://mansi:2525/pc-new/images/mapclosebutton.gif"
,infoBoxClearance: new google.maps.Size(1, 1)
,isHidden: false
,pane: "floatPane"
,id: "infoWindowClick"
,enableEventPropagation: false
};
var ib = new InfoBox();
google.maps.event.addListener(marker, "click", function (e) {
ib.close();
ib.setOptions(myOptions_click);
ib.open(map, this);
});
return marker;
}
I have referred similar question from Google Map V3 - Allow only one infobox to be displayed at a time and applied the code in the same way but I am not getting the desired result.
Google Map V3 - Allow only one infobox to be displayed at a time
You declare a new infobox every time you create a marker. So 'ib' refers to the infobox created for that marker and not the others.
You need to set the infobox variable outside the createMarker function scope. Then inside your event listener, close the old infobox and then create a new one.
var ib;
function createMarker(<params>) {
google.maps.event.addListener(marker, "click", function (e) {
if (typeof ib === 'object') {
ib.close();
}
ib = new Infobox();
ib.setOptions(myOptions_click);
ib.open(map, this);
});
}
If you need a new infobox for each marker, then you could store an array of infoboxes.
var ibs = [];
var closeInfoBox = function() {
for (var i in ibs) {
ibs[i].close();
}
}
function createMarker(<params>) {
var ibIndex = ibs.push(new Infobox()) - 1,
ib = ibs[ibIndex];
google.maps.event.addListener(marker, "click", function (e) {
closeInfoBox();
ib.setOptions(myOptions_click);
ib.open(map, this);
});
}