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;
}
Related
I'm creating my first package using htmlwidgetsand I'm having some troubles integrating the output with shiny. When I update an input my widget draws a new plot below the original one instead of just updating the original plot. For example:
This my js code, my guess is that the problem is there:
HTMLWidgets.widget({
name: 'IMPosterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
//transition
var transDuration = 2500;
var dataDiscrete = opts.bars.map((b, i) => {
b.y = Number(b.y);
b.desc = opts.text[i];
return b;
});
var distParams = {
min: d3.min(opts.data, d => d.x),
max: d3.max(opts.data, d => d.x)
};
distParams.cuts = [-opts.MME, opts.MME, distParams.max];
opts.data = opts.data.sort((a,b) => a.x - b.x);
var dataContinuousGroups = [];
distParams.cuts.forEach((c, i) => {
let data = opts.data.filter(d => {
if (i === 0) {
return d.x < c;
} else if (i === distParams.cuts.length - 1) {
return d.x > distParams.cuts[i - 1];
} else {
return d.x < c && d.x > distParams.cuts[i - 1];
}
});
data.unshift({x:data[0].x, y:0});
data.push({x:data[data.length - 1].x, y:0});
dataContinuousGroups.push({
color: opts.colors[i],
data: data
});
});
var margin = {
top: 50,
right: 20,
bottom: 80,
left: 70
},
dims = {
width: width - margin.left - margin.right,
height: height - margin.top - margin.bottom
};
var xContinuous = d3.scaleLinear()
.domain([distParams.min - 1, distParams.max + 1])
.range([0, dims.width]);
var xDiscrete = d3.scaleBand()
.domain(dataDiscrete.map(function(d) { return d.x; }))
.rangeRound([0, dims.width]).padding(0.1);
var y = d3.scaleLinear()
.domain([0, 1])
.range([dims.height, 0]);
var svg = d3.select(el).append("svg")
.attr("width", dims.width + margin.left + margin.right)
.attr("height", dims.height + margin.top + margin.bottom);
var g = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xAxis = d3.axisBottom()
.scale(xDiscrete);
var yAxis = d3.axisLeft()
.scale(y)
.ticks(10)
.tickFormat(d3.format(".0%"));
var yLabel = g.append("text")
.attr("class", "y-axis-label")
.attr("transform", "rotate(-90)")
.attr("y", -52)
.attr("x", -160)
.attr("dy", ".71em")
.style("text-anchor", "end")
.style("font-size", 14 + "px")
.text("Probability");
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + dims.height + ")")
.call(xAxis);
g.append("g")
.attr("class", "y axis")
.call(yAxis);
var areas = g.selectAll(".area")
.data(dataDiscrete)
.enter().append("path")
.attr("class", "area")
.style("fill", function(d) { return d.color; })
.attr("d", function(d, i) {
let numPts = dataContinuousGroups[i].data.length - 2;
var path = d3.path();
path.moveTo(xDiscrete(d.x), y(0));
for (j=0; j<numPts; j++) {
path.lineTo(xDiscrete(d.x) + j*xDiscrete.bandwidth()/(numPts-1), y(d.y));
}
path.lineTo(xDiscrete(d.x) + xDiscrete.bandwidth(), y(0));
return path.toString();
});
var tooltip = d3.tip()
.attr('class', 'd3-tip chart-data-tip')
.offset([30, 0])
.direction('s')
.html(function(d, i) {
return "<span>" + dataDiscrete[i].desc + "</span>";
});
g.call(tooltip);
areas
.on('mouseover', tooltip.show)
.on('mouseout', tooltip.hide);
var thresholdLine = g.append("line")
.attr("stroke", "black")
.style("stroke-width", "1.5px")
.style("stroke-dasharray", "5,5")
.style("opacity", 1)
.attr("x1", 0)
.attr("y1", y(opts.threshold))
.attr("x2", dims.width)
.attr("y2", y(opts.threshold));
var updateXAxis = function(type, duration) {
if (type === "continuous") {
xAxis.scale(xContinuous);
} else {
xAxis.scale(xDiscrete);
}
d3.select(".x").transition().duration(duration).call(xAxis);
};
var updateYAxis = function(data, duration) {
var extent = d3.extent(data, function(d) {
return d.y;
});
extent[0] = 0;
extent[1] = extent[1] + 0.2*(extent[1] - extent[0]);
y.domain(extent);
d3.select(".y").transition().duration(duration).call(yAxis);
};
var toggle = function(to, duration) {
if (to === "distribution") {
updateYAxis(dataContinuousGroups[0].data.concat(dataContinuousGroups[1].data).concat(dataContinuousGroups[2].data), 0);
updateXAxis("continuous", duration);
areas
.data(dataContinuousGroups)
.transition()
.duration(duration)
.attr("d", function(d) {
var gen = d3.line()
.x(function(p) {
return xContinuous(p.x);
})
.y(function(p) {
return y(p.y);
});
return gen(d.data);
});
thresholdLine
.style("opacity", 0);
g.select(".y.axis")
.style("opacity", 0);
g.select(".y-axis-label")
.style("opacity", 0);
} else {
y.domain([0, 1]);
d3.select(".y").transition().duration(duration).call(yAxis);
updateXAxis("discrete", duration);
areas
.data(dataDiscrete)
.transition()
.duration(duration)
.attr("d", function(d, i) {
let numPts = dataContinuousGroups[i].data.length - 2;
var path = d3.path();
path.moveTo(xDiscrete(d.x), y(0));
for (j=0; j<numPts; j++) {
path.lineTo(xDiscrete(d.x) + j*xDiscrete.bandwidth()/(numPts-1), y(d.y));
}
path.lineTo(xDiscrete(d.x) + xDiscrete.bandwidth(), y(0));
return path.toString();
});
thresholdLine
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1)
.attr("y1", y(opts.threshold))
.attr("y2", y(opts.threshold));
g.select(".y.axis")
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1);
g.select(".y-axis-label")
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1);
}
};
// Add buttons
//container for all buttons
var allButtons = svg.append("g")
.attr("id", "allButtons");
//fontawesome button labels
var labels = ["B", "D"];
//colors for different button states
var defaultColor = "#E0E0E0";
var hoverColor = "#808080";
var pressedColor = "#000000";
//groups for each button (which will hold a rect and text)
var buttonGroups = allButtons.selectAll("g.button")
.data(labels)
.enter()
.append("g")
.attr("class", "button")
.style("cursor", "pointer")
.on("click", function(d, i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode));
d3.select("#numberToggle").text(i + 1);
if (d === "D") {
toggle("distribution", transDuration);
} else {
toggle("discrete", transDuration);
}
})
.on("mouseover", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill", hoverColor);
}
})
.on("mouseout", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill", defaultColor);
}
});
var bWidth = 40; //button width
var bHeight = 25; //button height
var bSpace = 10; //space between buttons
var x0 = 20; //x offset
var y0 = 10; //y offset
//adding a rect to each toggle button group
//rx and ry give the rect rounded corner
buttonGroups.append("rect")
.attr("class", "buttonRect")
.attr("width", bWidth)
.attr("height", bHeight)
.attr("x", function(d, i) {
return x0 + (bWidth + bSpace) * i;
})
.attr("y", y0)
.attr("rx", 5) //rx and ry give the buttons rounded corners
.attr("ry", 5)
.attr("fill", defaultColor);
//adding text to each toggle button group, centered
//within the toggle button rect
buttonGroups.append("text")
.attr("class", "buttonText")
.attr("x", function(d, i) {
return x0 + (bWidth + bSpace) * i + bWidth / 2;
})
.attr("y", y0 + bHeight / 2)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr("fill", "white")
.text(function(d) {
return d;
});
function updateButtonColors(button, parent) {
parent.selectAll("rect")
.attr("fill", defaultColor);
button.select("rect")
.attr("fill", pressedColor);
}
toggle("distribution", 0);
setTimeout(() => {
toggle("discrete", transDuration);
}, 1000);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
This is the code for the shiny app:
# Gen data --------------------------------------------------------------
set.seed(9782)
x <- rnorm(1000)
# Usy my widget -----------------------------------------------------------
library(IMPosterior)
IMPosterior(x= x, MME=1)
## app.R ##
library(shiny)
library(shinydashboard)
library(IMPosterior) # https://github.com/ignacio82/IMPosterior
ui <- dashboardPage(
dashboardHeader(),
dashboardSidebar(
sliderInput(
"threshold",
h4("Threshold:"),
min = 50,
max = 99,
value = 75,
step = 1,
post = "%"
)
),
dashboardBody(
box(
title = "Posterior Distribution",
status = "primary",
solidHeader = TRUE,
width = 6,
IMPosteriorOutput("plot", height = "350px")
)
)
)
server <- function(input, output) {
output$plot <- renderIMPosterior({
p <- IMPosterior(x = x, MME = 1, threshold = input$threshold/100)
return(p)
})
}
shinyApp(ui, server)
What am I doing wrong?
I'm not sure if this is the best way to solve the problem, but it does. I just needed to add .html("") to my svg right after I select el
var svg = d3.select(el).html("").append("svg")
.attr("width", dims.width + margin.left + margin.right)
.attr("height", dims.height + margin.top + margin.bottom);
I am using d3.js , when calling box plot function.
It's working fine with small dataset. But its not working with large
dataset (900 000 data).
I tried with many data set but its only working with small set of
data and its getting hang on large set of data.
I tried to debug the d3.js script for box plot but was not able to
find where the dataset becomes null.
Does the performance of d3.js graphs degrade with increase in dataset? How can I optimize it?
Here's the code :
function box(g) {
g.each(function(data, i) {
//d = d.map(value).sort(d3.ascending);
//var boxIndex = data[0];
//var boxIndex = 1;
var d = data[1].sort(d3.ascending);
var t = tableData.sort(function(a, b){return a-b;});
// console.log(boxIndex);
//console.log(d);
var g = d3.select(this),
n = t.length;
min = 0,
max = d[n-1];
// Compute quartiles. Must return exactly 3 elements.
var quartileData = [];
quartileData[0]=d[1];
quartileData[1]=parseFloat(d[2]);
quartileData[2]=d[3];
var data1 = parseFloat(d[1]);
var data2 = parseFloat(d[3]);
var common = data2-data1;
var value = common.toFixed(5);
// Compute whiskers. Must return exactly 2 elements, or null.
var whiskerData= [];
whiskerData[0] = (data1 - ((value)*1.5)).toFixed(5);
whiskerData[1]= (data2 + ((value)*1.5)).toFixed(5);
// Compute whiskers. Must return exactly 2 elements, or null.
var whiskerIndices = [];
var isRangeFound = false;
whiskerIndices[0] = $.map( t , function( val,i )
{
if(whiskerData[0]< parseFloat(val) && !isRangeFound)
{
isRangeFound = true;
return i;
}
});
var isRangeFound1 = false;
whiskerIndices[1] = $.map( t , function( val,i )
{
if(whiskerData[1] < parseFloat(val) && !isRangeFound1)
{
isRangeFound1 = true;
return i-1;
}
});
// Compute outliers. If no whiskers are specified, all data are "outliers".
// We compute the outliers as indices, so that we can join across transitions!
var outlierIndices = whiskerIndices
? d3.range(0, whiskerIndices[0]).concat(d3.range(parseInt(whiskerIndices[1]) + 1, n))
: d3.range(n);
// Compute the new x-scale.
var x1 = d3.scale.linear()
.domain(domain && domain.call(this,t, i) || [minBound, maxBound])
.range([height, 0]);
// Retrieve the old x-scale, if this is an update.
var x0 = this.__chart__ || d3.scale.linear()
.domain([0, Infinity])
// .domain([0, max])
.range(x1.range());
// Stash the new scale.
this.__chart__ = x1;
// Note: the box, median, and box tick elements are fixed in number,
// so we only have to handle enter and update. In contrast, the outliers
// and other elements are variable, so we need to exit them! Variable
// elements also fade in and out.
// Update center line: the vertical line spanning the whiskers.
var center = g.selectAll("line.center")
.data(whiskerData ? [whiskerData] : []);
//vertical line
center.enter().insert("line", "rect")
.attr("class", "center")
.attr("x1", width / 2)
.attr("y1", function(d) { return x0(d[0]); })
.attr("x2", width / 2)
.attr("y2", function(d) { return x0(d[1]); })
.style("opacity", 1e-6)
.transition()
.duration(duration)
.style("opacity", 1)
.attr("y1", function(d) { return x1(d[0]); })
.attr("y2", function(d) { return x1(d[1]); });
center.transition()
.duration(duration)
.style("opacity", 1)
.attr("y1", function(d) { return x1(d[0]); })
.attr("y2", function(d) { return x1(d[1]); });
center.exit().transition()
.duration(duration)
.style("opacity", 1e-6)
.attr("y1", function(d) { return x1(d[0]); })
.attr("y2", function(d) { return x1(d[1]); })
.remove();
// Update innerquartile box.
var box = g.selectAll("rect.box")
.data([quartileData]);
box.enter().append("rect")
.attr("class", "box")
.attr("x", 0)
.attr("y", function(d) { return x0(d[2]); })
.attr("width", width)
.attr("height", function(d) { return x0(d[0]) - x0(d[2]); })
.transition()
.duration(duration)
.attr("y", function(d) { return x1(d[2]); })
.attr("height", function(d) { return x1(d[0]) - x1(d[2]); });
box.transition()
.duration(duration)
.attr("y", function(d) { return x1(d[2]); })
.attr("height", function(d) { return x1(d[0]) - x1(d[2]); });
// Update median line.
var medianLine = g.selectAll("line.median")
.data((quartileData[1]));
medianLine.enter().append("line")
.attr("class", "median")
.attr("x1", 0)
.attr("y1", x0)
.attr("x2", width)
.attr("y2", x0)
.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1);
medianLine.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1);
// Update whiskers.
var whisker = g.selectAll("line.whisker")
.data(whiskerData || []);
whisker.enter().insert("line", "circle, text")
.attr("class", "whisker")
.attr("x1", 0)
.attr("y1", x0)
.attr("x2", 0 + width)
.attr("y2", x0)
.style("opacity", 1e-6)
.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1)
.style("opacity", 1);
whisker.transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1)
.style("opacity", 1);
whisker.exit().transition()
.duration(duration)
.attr("y1", x1)
.attr("y2", x1)
.style("opacity", 1e-6)
.remove();
var outlier = g.selectAll("circle.outlier")
.data(outlierIndices, Number);
outlier.enter().insert("circle", "text")
.attr("class", "outlier")
.attr("r", 5)
.attr("cx", width / 2)
.attr("cy", function(i) { return x0(t[i]); })
.style("opacity", 1e-6)
.transition()
.duration(duration)
.attr("cy", function(i) { return x1(t[i]); })
.style("opacity", 1);
outlier.transition()
.duration(duration)
.attr("cy", function(i) { return x1(t[i]); })
.style("opacity", 1);
outlier.exit().transition()
.duration(duration)
.attr("cy", function(i) { return x1(t[i]); })
.style("opacity", 1e-6)
.remove();
// Compute the tick format.
var format = tickFormat || x1.tickFormat(8);
// Update box ticks.
var boxTick = g.selectAll("text.box")
.data(quartileData);
if(showLabels == true) {
boxTick.enter().append("text")
.attr("class", "box")
.attr("dy", ".3em")
.attr("dx", function(d, i) { return i & 1 ? 6 : -6 ;})
.attr("x", function(d, i) { return i & 1 ? + width : 0; })
.attr("y", x0)
.attr("text-anchor", function(d, i) { return i & 1 ? "start" : "end"; })
.text(format)
.transition()
.duration(duration)
.attr("y", x1);
}
boxTick.transition()
.duration(duration)
.text(format)
.attr("y", x1);
// Update whisker ticks. These are handled separately from the box
// ticks because they may or may not exist, and we want don't want
// to join box ticks pre-transition with whisker ticks post-.
var whiskerTick = g.selectAll("text.whisker")
.data(whiskerData || []);
if(showLabels == true) {
whiskerTick.enter().append("text")
.attr("class", "whisker")
.attr("dy", ".3em")
.attr("dx", 6)
.attr("x", width)
.attr("y", x0)
.text(format)
.style("opacity", 1e-6)
.transition()
.duration(duration)
.attr("y", x1)
.style("opacity", 1);
}
whiskerTick.transition()
.duration(duration)
.text(format)
.attr("y", x1)
.style("opacity", 1);
whiskerTick.exit().transition()
.duration(duration)
.attr("y", x1)
.style("opacity", 1e-6)
.remove();
});
d3.timer.flush();
}
function getBoxPlotChart(csvFetch,chartDiv,outilerTotalValue,tableDataValue)
{
var labels = true;
$("#" + chartDiv).empty();
var margin = {top: 30, right: 50, bottom: 70, left: 50};
var width = 410 - margin.left - margin.right;
var height = 380 - margin.top - margin.bottom;
var min = Infinity,
max = -Infinity;
tableData = tableDataValue;
var data = [];
data[0] = [];
// add more rows if your csv file has more columns
// add here the header of the csv file
data[0][0] = "";
// add more rows if your csv file has more columns
data[0][1] = [];
var v1 = parseFloat(csvFetch.firstQuartile),
v2 = parseFloat(csvFetch.medianValue),
v3=parseFloat(csvFetch.thirdQuartile),
v4=parseFloat(csvFetch.minValue),
v5=parseFloat(csvFetch.maxValue);
outlierData = outilerTotalValue;
var rowMax = Math.max(v1, Math.max(v2, Math.max(v3,v5)));
//var rowMin = Math.min(v4, Math.max(v2, Math.max(v3,v1)));
var rowMin=0;
// add more variables if your csv file has more columns
var data1 = parseFloat(v1);
var data2 = parseFloat(v3);
var absDiff = Math.abs(data1-data2);
var iqrValue = absDiff * 1.5;
//var common = data2-data1;
//var value = common.toFixed(5);
// Compute whiskers. Must return exactly 2 elements, or null.
var fence= [];
fence[0] = data1 - iqrValue;
fence[1]= data2 + iqrValue;
var lowerFence=fence[0];
var upperFence=fence[1];
/*End : To correct Y axis labelling : Pranjal */
var rowMax1 = Math.max(v1, Math.max(v2, Math.max(v3,v5)));
var rowMin1 = Math.min(v4, Math.max(v2, Math.max(v3,v1)));
minBound=Math.floor(Math.min(lowerFence,rowMin1));
maxBound=Math.ceil(Math.max(upperFence,rowMax1));
data[0][1].push(v1);
data[0][1].push(v2);
data[0][1].push(v3);
data[0][1].push(v4);
data[0][1].push(v5);
// add more rows if your csv file has more columns
if (rowMax > max) max = rowMax;
if (rowMin < min) min = rowMin;
var chart = d3.box()
//.whiskers(iqr(1.5))
.height(height)
.domain([minBound, maxBound])
.showLabels(labels);
var svg = d3.select("#" + chartDiv).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("class", "box")
.append("g")
.attr("transform", "translate(" + (margin.left+200) + "," + margin.top + ")");
// the x-axis
var x = d3.scale.ordinal()
.domain( data.map(function(d) { console.log(d); return d[0]; } ) )
.rangeRoundBands([0 , width], 0.7, 0.3);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
// the y-axis
var y = d3.scale.linear()
.domain([minBound, maxBound])
.range([height + margin.top, 0 + margin.top]);
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
// draw the boxplots
svg.selectAll(".box")
.data(data)
.enter().append("g")
.attr("transform", function(d) { return "translate(" + x(d[0]) + "," + margin.top + ")"; } )
.call(chart.width(x.rangeBand()));
/*// add a title
svg.append("text")
.attr("x", (width / 2))
.attr("y", 0 + (margin.top / 2))
.attr("text-anchor", "middle")
.style("font-size", "18px");*/
/* Start: Bug: attribute label should present : Salman : 11/03/2016 */
// .text(csvFetch.attributeName+" "+"Vs"+" "+"Distribution");
/* End: Bug: attribute label should present : Salman : 11/03/2016 */
// draw y axis labelling
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text") // and text1
.attr("transform", "rotate(0)")
.attr("y", 6)
.attr("dy", ".71em")
/* Start: Bug: y axis label was getting hidden : Salman : 11/03/2016 */
.style("text-anchor", "start")
/* End: Bug: y axis label was getting hidden : Salman : 11/03/2016 */
.text(csvFetch.attributeName);
// draw x axis labelling
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height + margin.top + 10) + ")")
.call(xAxis)
.append("text") // text label for the x axis
.attr("x", (width / 2)+350 )
.attr("y", 10 )
.attr("dy", ".71em");
// Returns a function to compute the interquartile range.
/*function iqr(k) {
return function(d, i) {
var q1 =d[1];
var q3 = d[3];
var iqr = (q3 - q1)*k;
};
}*/
}
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.
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;
});
}
http://i.imgur.com/F2Nqz4b.png?1
Hello Community! That in the picture is what I'd like to achieve...
This is my working code so far (associated to the left half of the picture):
var diagramElement = this.getElement();
var links = eval(this.getState().string);
// string = e.g. "[{source: \"Germany\", target: \"Europe\", property: \"is type\", type: \"connection\"}]"
var width = 800, height = 300;
var svg = d3.select(diagramElement).append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.size([width, height])
.linkDistance(100)
.charge(-400)
.on("tick", tick);
var link;
var circle;
var text;
var nodes = {};
var linktext;
svg.append("defs")
.selectAll("marker").data(["connection", "new"]).enter()
.append("marker")
.attr("id", function(d) { return d; })
.attr("viewBox", "0 -5 10 10")
.attr("refX", 15)
.attr("refY", 0)
.attr("markerWidth", 8)
.attr("markerHeight", 8)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
this.onStateChange = function() {
svg.selectAll("g").remove();
nodes = {};
links = [];
links = eval(this.getState().string);
links.forEach(function(link) {
link.source = nodes[link.source] || (nodes[link.source] = {name : link.source});
link.target = nodes[link.target] || (nodes[link.target] = {name : link.target});
});
force
.nodes(d3.values(nodes))
.links(links)
.on("tick", tick)
.start();
link = svg.append("g").selectAll("line.link")
.data(force.links())
.enter()
.append("svg:line")
.attr("class", function(d) {return "link " + d.type;})
.attr("marker-end", function(d) {return "url(#" + d.type + ")";});
circle = svg.append("g").selectAll("circle")
.data(force.nodes())
.enter()
.append("circle")
.attr("r", 8)
.call(force.drag);
text = svg.append("g").selectAll("text")
.data(force.nodes())
.enter()
.append("text")
.style("font-size","15px")
.attr("x", 10)
.attr("y", ".42em")
.text(function(d) {return d.name;});
// this is where the linktext is aligned relatively to the links
// must change something here
linktext = svg.append("g").selectAll("text")
.data(force.links())
.enter()
.append("text")
.style("font-size","15px")
.attr("dx", 1)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) {return d.type;});
};
function tick() {
circle.attr("cx", function(d) { return d.x = Math.max(12, Math.min(798 - 12, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(12, Math.min(279 - 12, d.y)); });
text.attr("transform", transform);
// linktext position update
linktext.attr("transform", function(d) {return "translate(" + (d.source.x + d.target.x) / 2 + "," + (d.source.y + d.target.y) / 2 + ")";});
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
}
function transform(d) {
return "translate(" + d.x + "," + d.y + ")";
}
So I've appended the text successfully to the links, but now how would I position in slightly above the link, in link direction, as shown in my attached picture?
I appreciate any help
Inside on('tick') after determining the position of your links you can also apply some transformations like so:
linkLabels
.attr('transform', d => {
// Adding to the transform. Multiple transforms would just get overwritten.
let transformation = ``
// The text position should be at the middle of the line
const x = (d.source.x + d.target.x) / 2
const y = (d.source.y + d.target.y) / 2
transformation += `translate(${x}, ${y}) `
// Make sure the text is always upright
if(d.source.x > d.target.x) {
transformation += `rotate(180) `
}
// Aligning text to the slope.
// The formula for the tangent of a slope is (y2 - y1) / (x2 - x1)
// where (xi, yi) are the coordinates of the line endings
const angle = Math.atan2(d.target.y - d.source.y, d.target.x - d.source.x)
transformation += `rotate(${angle * 180/Math.PI}) `
return transformation
})