How to make map draggable in D3v6 - javascript

I have a Drilldown world map(continent map + country map) where the second map(the country map) is zoomed-in onload by using fitExtent function. Since it is zoomed-in, I wanted to implement a draggable feature where I can drag the map and see other part of the map.
//My svg tag
<svg id="mapSVG" width="560"; height="350"></svg>
let zoomControl = function (event) {
svg.selectAll("path")
.attr("transform", event.transform);
}
function loadCountryMap(path, mapData) {
d3.json(path).then(function (json) {
var projection = d3.geoMercator();
var features = json.features;
//The reason why we have to do this is because d3.js has winding problem
//We need to rewind for the map to display correctly
var fixed = features.map(function (feature) {
return turf.rewind(feature, { reverse: true });
})
//Projections
var geoPath = d3.geoPath().projection(projection);
//Zoom in
projection.fitExtent([[mapData.XOffSet, mapData.YOffSet], [width*2, height*2]], { "type": "FeatureCollection", "features": fixed })
//Draggable
svg.selectAll("path")
.data(fixed)
.enter()
.append("path")
.attr("d", geoPath)
.attr("id", function (d) { return d.properties.FIPS_10_; })
.style("fill", "steelblue")
.style("stroke", "transparent")
.on("mouseover", mouseOver)
.on("mouseleave", mouseLeave)
.on("click", mouthClick)
.call(d3.zoom()
.on("zoom", zoomControl)
.scaleExtent([1, 1])
)
})
}
//How I select the svg
var svg = d3.select("svg")
.style("background-color", "white")
.style("border", "solid 1px black");
var width = +svg.attr("width");
var height = +svg.attr("height");
There are two problems with this:
1: By selecting the "svg" tag, this will drag the entire SVG HTML element, instead of the map content of SVG. I also changed it to "path" and "d", it didn't work either.
2: When the drag event first occurred, the dragged elements are being placed at the bottom right corner of the mouse cursor and follow the mouse cursor after that.
I want the zoomed-in map to be draggable to so I can see other part of the map.
The example desired behavior bin. This is the code from Andrew Reid's answer to a question. When the map is zoomed in, it became draggable. I don't see the drag behavior been defined anywhere in the code. I am assuming it is achieved by using d3.zoom(). However, since my map are zoomed-in by default(onload), and I have a separate mouse click event, I don't think I can use the similar approach.

var svg = d3.select("#mapDiv")
.append("svg")
.attr("width", width)
.attr("height", height)
.style("background-color", "white")
.style("border", "solid 1px black")
.call(d3.zoom()
.on("zoom", function (event) {
svg.attr("transform", event.transform)
})
.scaleExtent([1, 1])
)
.append("g");
I have achieved the functionality by grouping my path with .append("g"). Instead of assigning the zoom functionality path by path, I simply assigned it to the entire SVG and now the map is working fine.

Related

d3js v4: Scale circles with zoom

I have a world map made with d3js v4 and topojson which has Zoom / Drag / Circles. Everything seems fine except I cant scale the circles togheter with the zoom.
When I scroll into the map, my circles stay at the same size, which makes them way to big compared to the map.
How can I apply the transformation to the circles when I zoom?
var width = 660,
height = 400;
var zoom = d3.zoom()
.scaleExtent([1, 10])
.on("zoom", zoomed);
var projection = d3.geoMercator()
.center([50, 10]) //long and lat starting position
.scale(150) //starting zoom position
.rotate([10,0]); //where world split occurs
var svg = d3.select("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
var path = d3.geoPath()
.projection(projection);
var g = svg.append("g");
//Zoom functionality
function zoomed() {
const currentTransform = d3.event.transform;
g.attr("transform", currentTransform);
}
d3.select(".zoom-in").on("click", function() {
zoom.scaleBy(svg.transition().duration(750), 1.2);
});
d3.select(".zoom-out").on("click", function() {
zoom.scaleBy(svg.transition().duration(750), 0.8);
});
// load and display the world and locations
d3.json("https://gist.githubusercontent.com/d3noob/5193723/raw/world-110m2.json", function(error, topology) {
var world = g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries).geometries)
.enter()
.append("path")
.attr("d", path)
;
var locations = g.selectAll("circle")
.data(devicesAll)
.enter()
.append("circle")
.attr("cx", function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[0];})
.attr("cy", function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[1];})
.attr("r", 2)
.style("fill", "black")
.style("opacity", 1)
;
var simulation = d3.forceSimulation()
.force('x', d3.forceX().x(function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[0]}))
.force('y', d3.forceY().y(function(d) {return projection([d.LastLocation.lon, d.LastLocation.lat])[1]}))
.force("charge", d3.forceManyBody().strength(0.5)) // Nodes are attracted one each other of value is > 0
.force("collide", d3.forceCollide().strength(.1).radius(2).iterations(2)) // Force that avoids circle overlapping
// Apply these forces to the nodes and update their positions.
// Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
simulation
.nodes(devicesAll)
.on("tick", function(d){
locations
.attr("cx", function(d){ return d.x; })
.attr("cy", function(d){ return d.y; })
});
If i understood your problem correctly, you need to add it to your zoom behaviour.
//Zoom functionality
function zoomed() {
const currentTransform = d3.event.transform;
g.attr("transform", currentTransform);
}
here, you are applying your transformation to the elements, which is fine. However, you're not applying any logic to the radius.
That logic is up to you to make, and it will depend on the k property of the transform event (currentTransform.k).
I will use a some dummy logic for your radius. Your scale extent is between 1 and 10, you need a logic in which the radius decreases as the zoom increases (bigger k). It is also important that your radius doesn't go lower than 1, because the area of the circle will decrease much faster (remember the area depends on r^2, and r^2 < r for r < 1)
So my logic will be: the radius is 2.1 - (k / 10). Again, I'm oversimplifying, you can change it or tune it for your specific case.
In the end, it should look something like this:
//Zoom functionality
function zoomed() {
const currentTransform = d3.event.transform;
g.attr("transform", currentTransform);
g.selectAll("circle")
.attr("r", 2.1 - (currentTransform.k / 10))
}
I haven't tested the code, but tell me if this works! Maybe you can add it to a jsfiddle if needed

d3 Choropleth map with hover effect does not display tooltip

I am trying to add a tooltip to the d3 Choropleth map with hover effect from this page https://www.d3-graph-gallery.com/graph/choropleth_hover_effect.html
The tooltip seems to be appendend to the svg, as I can inspect the map and see an inner div with the suppossed text that should appear when hovering the countries, but when doing so, nothing shows up, even though the tooltip parameters seem to be updated on each different hover.
I've taken the tooltip from another graph as the current map I am trying to implement does not have tooltip. I've tried creating the tooltip variable within function ready(error, topo)and the styles/data inside the mouseover and mouseleave functions but it does not display the tooltip over the countries
Here is the coode in my xhtml
<head>
<!-- Load d3.js and the geo projection plugin -->
<script src="https://d3js.org/d3.v4.js"></script>
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<script src="https://d3js.org/d3-geo-projection.v2.min.js"></script>
<script>
jQuery(document).ready(function() {
// The svg
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
// Map and projection
var path = d3.geoPath();
var projection = d3.geoMercator()
.scale(70)
.center([0,20])
.translate([width / 2, height / 2]);
// Data and color scale
var data = d3.map();
var colorScale = d3.scaleThreshold()
.domain([100000, 1000000, 10000000, 30000000, 100000000, 500000000])
.range(d3.schemeBlues[7]);
// Load external data and boot
d3.queue()
.defer(d3.json, "https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson")
.defer(d3.csv, "https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world_population.csv", function(d) { data.set(d.code, +d.pop); })
.await(ready);
var tooltip = d3.select("#my_dataviz")
.append("div")
.style("opacity", 0)
.attr("class", "tooltip")
.style("background-color", "white")
.style("border", "solid")
.style("border-width", "2px")
.style("border-radius", "5px")
.style("padding", "5px")
function ready(error, topo) {
let mouseOver = function(d) {
d3.selectAll(".Country")
.transition()
.duration(200)
.style("opacity", .5)
d3.select(this)
.transition()
.duration(200)
.style("opacity", 1)
.style("stroke", "black")
tooltip.style("opacity", 1)
.html("The exact value of<br/>this cell is: " + d.pop)
.style("left", (d3.mouse(this)[0]+70) + "px")
.style("top", (d3.mouse(this)[1]) + "px")
}
let mouseLeave = function(d) {
d3.selectAll(".Country")
.transition()
.duration(200)
.style("opacity", .8)
d3.select(this)
.transition()
.duration(200)
.style("stroke", "transparent")
tooltip.style("opacity", 0)
}
// Draw the map
svg.append("g")
.selectAll("path")
.data(topo.features)
.enter()
.append("path")
// draw each country
.attr("d", d3.geoPath()
.projection(projection)
)
// set the color of each country
.attr("fill", function (d) {
d.total = data.get(d.id) || 0;
return colorScale(d.total);
})
.style("stroke", "transparent")
.attr("class", function(d){ return "Country" } )
.style("opacity", .8)
.on("mouseover", mouseOver )
.on("mouseleave", mouseLeave )
}
})
</script>
</head>
<div>
<div>
<h1>Graphs</h1>
<!-- Create an element where the map will take place -->
<svg id="my_dataviz" width="400" height="300"></svg>
</div>
</div>
After rendering the map (working with just hovering function) I inspect it and can see the following.
If I change the country, the data on the tooltip div varies.
--UPDATE--
Following #enxaneta 's advice, I tried taking the div outside svg. The result is having the tooltip in a separate div. This time the tooltip appears in the page but is displayed at the bottom of the map, not over it, how I would like to see it.
I am still figuring out how to implement foreignObject.
--UPDATE 2--
Adding tooltip position absolute toke the div and put it relative to the mouse hovering. However, even though it is vertically aligned with the country selected, it is way up at the top of the page.
<style>
.tooltip{position:absolute;}
</style>

D3: zoom fails to include elements as expected

I am trying to adjust my D3 zoom so that ALL elements zoom in as expected. My elements are as follows: countries, markers (circles) and flows (polygons).
So far, all elements load as expected. The countries first, then the circles and flows upon subsequent interaction. But the zoom only works for the countries. The circles and flows do not zoom but just stay static. What am I doing wrong?
Link to my jsfiddle
Countries I add to map as follows:
var country = g.append("g");
d3.json("countries.json", function(collection) {
country.selectAll("path")
.data(collection.features)
.enter().append("path")
.attr("d", path);
});
Circles I add after user interaction, as follows:
var g_circles = svg.append("g").attr("class", "circles");
$.each(circles, function(i, d) {
dz = projection(d);
g_circles.append("circle")
.attr("class", "marker")
.attr("d", path)
.attr("cx", dz[0])
.attr("cy", dz[1])
.call(zoom);
});
Flows I add to the map as follows:
var g_lines = svg.append("g").attr("class", "lines");
g_lines.selectAll(".link_line")
.data(links)
.enter()
.append("path")
.attr("class", "link_line")
.style('fill-opacity', 0.3)
.attr("d", "path")
.call(zoom);
Zoom is as follows:
var zoom = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([h, 350000 * h])
.on("zoom", zoomed);
function zoomed() {
projection.translate(d3.event.translate).scale(d3.event.scale);
svg.selectAll("path, circle, .link_line").attr("d", path);
}
Have you tried to add all the expected groups (g_circles and g_lines) into a new single group (g) and use the dimensions of this group to do the focus?
Zoom should be applied to the SVG:
See this other question as it's very similar to yours.
Zooms are commonly expected to work as a translation of the svg like in this example.

Zooming datasets in d3.js

I have overlayed two datasets, a boundary map and a point map in d3.js. I want to be able to zoom both datasets at the same time. With the current code, only the point map responds to the zoom. How can I zoom both datasets at the same time
The code is shown below
var canvas = d3.select("body").append("svg")
.attr("width",260)
.attr("height",400)
d3.json("/Maps/iowastate.json",function (data){
var group = canvas.selectAll("g")
.data(data.features)
.enter()
.append("g")
var projection =d3.geo.mercator()
.scale(250)
//.translate([0,0]);
var path = d3.geo.path().projection(projection);
var areas = group.append("path")
.attr("d", path)
.attr("class","area")
.attr("fill","black");
d3.csv("/Maps/detectors.csv",function (d){
var group = canvas.selectAll("g")
.data(d)
.enter()
.append("circle")
.attr("cx", function(d) {
return projection([d.StartLong,d.StartLat])[0];
})
.attr("cy", function(d,i) {
return projection([d.StartLong,d.StartLat])[1];
})
.attr("r", 0.1)
.style("fill", "red");
//console.log(projection(d[0].StartLat))
var zoom = d3.behavior.zoom()
.on("zoom",function(){
group.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
group.selectAll("path")
.attr("d", path.projection(projection));
});
canvas.call(zoom)
})
var zoom = d3.behavior.zoom()
.on("zoom",function(){
group.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
group.selectAll("path")
.attr("d", path.projection(projection));
});
canvas.call(zoom)
})
You are applying the right modifications, but twice to the same set of elements instead of the two different layers. To make it work, keep a reference to the other group (e.g. by using different variable names) and apply the transformations to both groups.

Automatic zoom on object in D3 force layout

I have a force directed graph and I implemented an autocomplete in order to highlight a node. Basically, once you select a node it is colored in red. I would now like to "zoom" on this node, which is change my window to be 400% the size of the node and the node should be centered in it.
Here are the relevant samples of my code: (or you can directly go to the jsFiddle I setup.)
First the code used to create the svg element:
var w = 4000,
h = 3000;
var vis = d3.select("#mysvg")
.append("svg:svg")
.attr("width", "100%")
.attr("height", "100%")
.attr("id","svg")
.attr("pointer-events", "all")
.attr("viewBox","0 0 "+w+" "+h)
.attr("perserveAspectRatio","xMinYMid")
.append('svg:g')
.call(d3.behavior.zoom().on("zoom", redraw))
.append('svg:g');
Then, as an example, the function used to redraw the directed graph on "normal" zoom.
function redraw() {
trans=d3.event.translate;
scale=d3.event.scale;
vis.attr("transform",
"translate(" + trans + ")"
+ " scale(" + scale + ")");
}
Here are the nodes of my graph:
vis.selectAll("g.node")
.data(nodes, function(d) {return d.id;})
.enter().append("g")
.append("circle")
.attr("id", function(d){return "circle-node-"+ d.id})
.attr("fill","white")
.attr("r","50px")
.attr("stroke", "black")
.attr("stroke-width","2px");
And finally here is my autocomplete.
$(function() {
$( "#tags" ).autocomplete({
source: nodes; //...
select: function( event, ui){
// ...
vis.selectAll("#circle-node-"+ui.item.value)
.transition()
.attr("fill", "red")
}
})
});
I tried to put as little code as possible so, sorry if I forgot something.
Update Here is a jsFiddle illustrating where I am for now.
The scaling and translation should be handled in the same function where you color the node red. You haven't really described how exactly you want the zoom to behave, but probably the easiest way is to apply translate and scale to the g element containing the graph.
I've changed your jsfiddle to do this; result here. I've assumed that by "400% the size of the node" you mean that the node should be magnified 400%? I've introduced a variable for the zoom factor if you want to change it.

Categories