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.
Related
Hi I'm fairly new to the D3 library, and I cannot figure out how to get a click event to work when I'm clicking on the country, I understand the 'd' variable contains the country information but how do I get it to console log on click. I've tried a few methods but cant figure it out. any help appreciated
const svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height");
// Map and projection
const projection = d3.geoNaturalEarth1()
.scale(width / 1.5 ) // Lower the num closer the zoom
.translate([200, 700]) // (Horizontal, Vertical)
// Load external data and boot
d3.json("https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson").then( function(data) {
// Draw the map
svg.append("g")
.selectAll("path")
.data(data.features)
.join("path")
.attr("fill", "#348C31") // Color Of Country
.attr("d", d3.geoPath().projection(projection))
.style("stroke", "white")// Border Lines
.append("title")
.text(d => console.log(d))
//Want to run on click of country,
})
const $countyPaths = svg.append("g")
.selectAll("path")
.data(data.features)
.join("path")
$countyPaths
.attr("fill", d => color(data.get(d.id)))
.attr("d", path)
.append("title")
$countyPaths.on('click',d=>console.log(d.id))
function buttonClick() {
window.alert("Boom");
}
</script>
d3 provides the .on(event, fx(event, d)) method in order to handle events.
To log the country, you could do then the following:
svg.append("g")
.selectAll("path")
.data(data.features)
.join("path")
.attr("fill", "#348C31") // Color Of Country
.attr("d", d3.geoPath().projection(projection))
.style("stroke", "white")// Border Lines
.on("click", (_, d) => console.log(d))
Note that the first thing that the function from the event listener will receive is the event info, and the second one the data
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
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.
I made d3.js pie chart and related legend with population data popu. When I hover over pie segments I achieved to enlarge related legend square parts and the pie segment itself (larger outerRadius). Now I am trying to do contrary. When I hover over square of legend I want to enlarge square itself and related pie segment as well. Something like this example here https://www.amcharts.com/demos/pie-chart-with-legend/. I will write down just part of the code related to pie chart problem that I have.
var pie = d3.pie()
.value(function(d) {return d.pop})(popu);
var seg = d3.arc()
.innerRadius(100)
.outerRadius(150)
.padAngle(.1)
.padRadius(45);
var segover = d3.arc()
.innerRadius(100)
.outerRadius(170)
.padAngle(.1)
.padRadius(45);
So this part is working great.
svg.append("g")
.attr("class", "pieChart")
.attr("transform", "translate(1250,570)")
.selectAll("path")
.data(pie)
.append("path")
.attr("class", "pie")
.attr("id", function(d){return d.data.id})
.attr("d", seg)
.on("mouseenter", function(d){
d3.select(this)
.transition(10)
.duration(100)
.attr("d", segover)
})
Then I tried to change pie chart segment when hovering on legend related segments.
var pieEl = svg.selectAll(".pie");
var piePath = pieEl.nodes();
svg.append("g")
.attr("class", "legend")
.attr("transform", "translate(-50,280)")
.selectAll(".mySquers")
.data(pie)
.enter()
.append("rect")
.attr("class", "rec")
.attr("x", 100)
.attr("y", function(d,i){ return 100 + i*25})
.attr("width", "15")
.attr("height", "15")
.attr("id", function(d,i){ return (popu[d,i].id)})
.style("fill",function(d,i){
if (this.id == piePath[i].id){
return piePath[i].getAttribute("fill")
}
})
.on("mouseenter", function(d){
for (var i=0; i<piePath.length; i++){
if (piePath[i].id == d.data.id){
piePath[i].setAttribute("d", segover);
}}
})
When I tray to setAttribute("d", segover) in DOM instead of d attribute written as string as usually (d="M144.58.....") I have a function (d="function(pie){ _e);}" and on hover pie segment dissapear. But for example if I set attribute fill to red on hover it change and segment is painted. So the notation of code is good. Is there some behavior of d path generated with d3.arc() that I am missing? Any suggestion is welcome.
I think you should be passing your data as an argument in your function. Normally, it is taken as default argument when you return the function directly.
piePath[i].setAttribute("d", segover(*data associated with segment*));
svg.append("g")
.attr("class", "pieChart")
.attr("transform", "translate(1250,570)")
.selectAll("path")...
.attr("d", seg) // this is same as : attr("d", seg(d))
.on("mouseenter", function(d){
d3.select(this)
.transition(10)
.duration(100)
.attr("d", segover) // same here
})
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.