add image with anchor center not work as expected - d3.js

Below code put image at (50,50), the anchor point is left top corner. How can I change the anchor point to image center, image dimension maybe changed, so if just change (50,50) to (0,0) will work for this image, but not work if image dimension changed.
I try add anchor to center but not work.
var svg = d3.select('body')
.append('svg')
.attr('width',100)
.attr('height',100)
.style('border','1px solid red')
.attr('transform','translate(0,0)')
svg.append("image")
.attr("xlink:href", "https://dummyimage.com/100x100/ececec/000000")
.attr("x", 50)
.attr("y", 50)
.attr('anchor','center')
<script src="https://unpkg.com/d3#7.0.4/dist/d3.min.js"></script>

According to the MDN documentation for SVG Image, image has no anchor attribute. After the SVG is initially layed out, you could use get the size of the SVG and the size of the image and do it manually. Something like so:
let s = d3.select(svg);
let svg_width = s.attr("width");
let svg_height = s.attr("height");
let img = s.select("image").node();
let rect = img.getBoundingClientRect();
let img_width = rect.right - rect.left;
let img_height = rect.bottom - rect.top;
img.setAttribute("x", (svg_width - img_width)/2)
img.setAttribute("y", (svg_height - img_height)/2)

Related

d3 v6 pointer function not adjusting for scale and translate

I am upgrading my app from d3 v5 to v6 and am having an issue migrating the d3.mouse functionality. In my app I apply a transform to the top level svg group and use the zoom functionality to zoom and pan (scale and translate). When I double click on the screen I take the mouse position and draw a square.
Now I am replacing the d3.mouse function with d3.pointer. In my double click event I get the mouse position by calling d3.pointer(event). However this function is not producing a position that is relative to where my top level svg group is positioned and scaled. When I remove the translate and scale from the top level group, the position matches up.
In the older version of d3 I could call d3.mouse(this.state.svg.node()) and it would produce the exact position I clicked corrected for pan and scale. Is this available in version 6? If not, is there a clean way I can adjust for this? The new event object is coming through with a host of different position properties: pagex, offsetx, screenx, x. None of these is producing the position I clicked on. Is there a clean way to acheive this?
You could specify a container element which would factor in a zoom transform in v5 and earlier:
d3.mouse(container)
Returns the x and y coordinates of the current event relative to the specified container. The container may be an HTML or SVG container element, such as a G element or an SVG element. The coordinates are returned as a two-element array of numbers [x, y]. (source)
In d3v6 you can specify this by using the second parameter of d3.pointer:
d3.pointer(event[, target])
Returns a two-element array of numbers [x, y] representing the coordinates of the specified event relative to the specified target. event can be a MouseEvent, a PointerEvent, a Touch, or a custom event holding a UIEvent as event.sourceEvent.
...
If the target is an SVG element, the event’s coordinates are transformed using the inverse of the screen coordinate transformation matrix. If the target is an HTML element, the event’s coordinates are translated relative to the top-left corner of the target’s bounding client rectangle. (source)
So as far as I'm aware, you should be use:
d3.pointer(event,this.state.svg.node());
Instead of
d3.mouse(this.state.svg.node());
Here's a d3v6 example:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var rect = svg.append("rect")
.attr("width",500)
.attr("height",200)
.attr("fill", "#eee")
var g = svg.append("g");
var zoomed = function(event) {
g.attr("transform", event.transform);
}
rect.call(d3.zoom().on("zoom",zoomed))
.on("click", function(event) {
var xy = d3.pointer(event,g.node());
g.append("circle")
.attr("r", 5)
.attr("cx", xy[0])
.attr("cy", xy[1])
.attr("fill","crimson");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
Adapting this v5 example:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var rect = svg.append("rect")
.attr("width",500)
.attr("height",200)
.attr("fill", "#eee")
var g = svg.append("g");
var zoomed = function() {
g.attr("transform", d3.event.transform);
}
rect.call(d3.zoom().on("zoom",zoomed))
.on("click", function() {
var xy = d3.mouse(g.node());
g.append("circle")
.attr("r", 5)
.attr("cx", xy[0])
.attr("cy", xy[1])
.attr("fill","crimson");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

plotting tick text above the axis

I am using d3 v4 for ploting the graph. And currently the tick text on the x-axis is coming below the axis. and I want that text on above the axis.
//Set the Xaxis scale Range
let x = scaleLinear().rangeRound([0, width]);
let x_axis = axisBottom(x);
x.domain(extent(graphData, function (d) {
return d.weeks;
}));
g.append("g").attr("transform", "translate(0," + height + ")").call(axisBottom(x).ticks(5)).attr("transform", "translate(0, 120)");
so can you help me how to put the tick text above the x-axis.
If you want the ticks on top of the axis, you should use axisTop, instead of axisBottom.
The names are pretty easy to understand and the API is very clear:
d3.axisTop(scale): In this orientation, ticks are drawn above the horizontal domain path.
d3.axisBottom(scale): In this orientation, ticks are drawn below
the horizontal domain path. (emphases mine)
Here is a demo, the first axis uses axisTop, and the second one, below, uses axisBottom:
var svg = d3.select("svg");
var x = d3.scaleLinear().range([20, 280]);
var xAxisTop = d3.axisTop(x)(svg.append("g").attr("transform", "translate(0,50)"))
var xAxisBottom = d3.axisBottom(x)(svg.append("g").attr("transform", "translate(0,100)"))
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>

d3.js. How to make map full width?

Code available here. Map looks like full width, but not at all. There is some indents at the top and left and right sides. All examples of d3.js maps use some strange magic numbers in scale method. I try use such numbers from this answer, but looks like it is not true for my case. So what is true way to scale map? I want to make it full width without any indents on any screen.
var body = d3.select("body").node().getBoundingClientRect();
var coef = 640 / 360;
var width = body.width;
var height = width / coef;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
var projection = d3.geo.equirectangular()
.rotate([-180, 0])
.scale(width / 640 * 100)
.translate([width / 2, height / 2])
var path = d3.geo.path()
.projection(projection);
d3.json("https://dl.dropboxusercontent.com/s/20g0rtglh9xawb8/world-50m.json?dl=1", function(error, world) {
svg.append("path")
.datum(topojson.feature(world, world.objects.land))
.attr("d", path);
});

d3 pie chart transition with attrtween

i'm trying to somehow sweep in a half-donut-chart, meaning starting with a blank screen the chart starts drawing at -90 degree (or 270) and performs a halfcircle until reaching 90 degree. the code looks like:
var width = 800;
var height = 400;
var radius = 300;
var grad=Math.PI/180;
var data = [30, 14, 4, 4, 5];
var color = d3.scale.category20();
var svg = d3.select("body").append("svg").attr("width", width).attr("height",
`height).append("g").attr("transform", "translate(" + radius*1.5 + "," + radius*1.5 +
")");
var arc = d3.svg.arc().innerRadius(radius - 100).outerRadius(radius - 20);
var pie = d3.layout.pie().sort(null);
svg.selectAll("path").data(pie(data)).enter().append("path").attr("d",
arc).attr("fill",
function(d, i) { return color(i); }).transition().duration(500).attrTween("d", sweep);
function sweep(a) {
var i = d3.interpolate({startAngle: -90*grad, endAngle: -90*grad},{startAngle: -90*grad, endAngle: 90*grad});
return function(t) {
return arc(i(t));
};
}
looking at several examples i managed to get the animation, however, i fail at binding (or converting) the data to the arc. my feeling is that there is only one path drawn and then it stops.
if i change the interpolation to start/end -90/90 and a, i get different colors but not all of them. adding the start/end-angle to the pie-var gives me a transition where a one-colored-arc is shown at the beginning and then the other parts slide in (which would be correct if there was no arc at the beginning - the proportions also seem a bit wrong). setting the initial color to white does not help because then everything stays white.
i'm afraid i'm missing an obvious point, but so far i'm stuck, maybe someone can point me in the right direction.
after quite some variations and tests it somehow started to work, using these to lines of code:
var pie = d3.layout.pie().sort(null).startAngle(-90*grad).endAngle(90*grad);
var i = d3.interpolate({startAngle: -90*grad, endAngle: -90*grad},a);
one final "problem" was that the height of the svg was too small and so some segments got cut off, so changing it to
var height = 800;
ended my search. thanks for any considerations.
A small typo on the
var svg = d3.select("body").append("svg").attr("width", width).attr("height", `height)
should be:
var svg = d3.select("body").append("svg").attr("width", width).attr("height", height)

Center a map in d3 given a geoJSON object

Currently in d3 if you have a geoJSON object that you are going to draw you have to scale it and translate it in order to get it to the size that one wants and translate it in order to center it. This is a very tedious task of trial and error, and I was wondering if anyone knew a better way to obtain these values?
So for instance if I have this code
var path, vis, xy;
xy = d3.geo.mercator().scale(8500).translate([0, -1200]);
path = d3.geo.path().projection(xy);
vis = d3.select("#vis").append("svg:svg").attr("width", 960).attr("height", 600);
d3.json("../../data/ireland2.geojson", function(json) {
return vis.append("svg:g")
.attr("class", "tracts")
.selectAll("path")
.data(json.features).enter()
.append("svg:path")
.attr("d", path)
.attr("fill", "#85C3C0")
.attr("stroke", "#222");
});
How the hell do I obtain .scale(8500) and .translate([0, -1200]) without going little by little?
My answer is close to Jan van der Laan’s, but you can simplify things slightly because you don’t need to compute the geographic centroid; you only need the bounding box. And, by using an unscaled, untranslated unit projection, you can simplify the math.
The important part of the code is this:
// Create a unit projection.
var projection = d3.geo.albers()
.scale(1)
.translate([0, 0]);
// Create a path generator.
var path = d3.geo.path()
.projection(projection);
// Compute the bounds of a feature of interest, then derive scale & translate.
var b = path.bounds(state),
s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
// Update the projection to use computed scale & translate.
projection
.scale(s)
.translate(t);
After comping the feature’s bounding box in the unit projection, you can compute the appropriate scale by comparing the aspect ratio of the bounding box (b[1][0] - b[0][0] and b[1][1] - b[0][1]) to the aspect ratio of the canvas (width and height). In this case, I’ve also scaled the bounding box to 95% of the canvas, rather than 100%, so there’s a little extra room on the edges for strokes and surrounding features or padding.
Then you can compute the translate using the center of the bounding box ((b[1][0] + b[0][0]) / 2 and (b[1][1] + b[0][1]) / 2) and the center of the canvas (width / 2 and height / 2). Note that since the bounding box is in the unit projection’s coordinates, it must be multiplied by the scale (s).
For example, bl.ocks.org/4707858:
There’s a related question where which is how to zoom to a specific feature in a collection without adjusting the projection, i.e., combining the projection with a geometric transform to zoom in and out. That uses the same principles as above, but the math is slightly different because the geometric transform (the SVG "transform" attribute) is combined with the geographic projection.
For example, bl.ocks.org/4699541:
The following seems to do approximately what you want. The scaling seems to be ok. When applying it to my map there is a small offset. This small offset is probably caused because I use the translate command to center the map, while I should probably use the center command.
Create a projection and d3.geo.path
Calculate the bounds of the current projection
Use these bounds to calculate the scale and translation
Recreate the projection
In code:
var width = 300;
var height = 400;
var vis = d3.select("#vis").append("svg")
.attr("width", width).attr("height", height)
d3.json("nld.json", function(json) {
// create a first guess for the projection
var center = d3.geo.centroid(json)
var scale = 150;
var offset = [width/2, height/2];
var projection = d3.geo.mercator().scale(scale).center(center)
.translate(offset);
// create the path
var path = d3.geo.path().projection(projection);
// using the path determine the bounds of the current map and use
// these to determine better values for the scale and translation
var bounds = path.bounds(json);
var hscale = scale*width / (bounds[1][0] - bounds[0][0]);
var vscale = scale*height / (bounds[1][1] - bounds[0][1]);
var scale = (hscale < vscale) ? hscale : vscale;
var offset = [width - (bounds[0][0] + bounds[1][0])/2,
height - (bounds[0][1] + bounds[1][1])/2];
// new projection
projection = d3.geo.mercator().center(center)
.scale(scale).translate(offset);
path = path.projection(projection);
// add a rectangle to see the bound of the svg
vis.append("rect").attr('width', width).attr('height', height)
.style('stroke', 'black').style('fill', 'none');
vis.selectAll("path").data(json.features).enter().append("path")
.attr("d", path)
.style("fill", "red")
.style("stroke-width", "1")
.style("stroke", "black")
});
With d3 v4 or v5 its getting way easier!
var projection = d3.geoMercator().fitSize([width, height], geojson);
var path = d3.geoPath().projection(projection);
and finally
g.selectAll('path')
.data(geojson.features)
.enter()
.append('path')
.attr('d', path)
.style("fill", "red")
.style("stroke-width", "1")
.style("stroke", "black");
Enjoy, Cheers
I'm new to d3 - will try to explain how I understand it but I'm not sure I got everything right.
The secret is knowing that some methods will operate on the cartographic space (latitude,longitude) and others on the cartesian space (x,y on the screen). The cartographic space (our planet) is (almost) spherical, the cartesian space (screen) is flat - in order to map one over the other you need an algorithm, which is called projection. This space is too short to deep into the fascinating subject of projections and how they distort geographic features in order to turn spherical into plane; some are designed to conserve angles, others conserve distances and so on - there is always a compromise (Mike Bostock has a huge collection of examples).
In d3, the projection object has a center property/setter, given in map units:
projection.center([location])
If center is specified, sets the projection’s center to the specified location, a two-element array of longitude and latitude in degrees and returns the projection. If center is not specified, returns the current center which defaults to ⟨0°,0°⟩.
There is also the translation, given in pixels - where the projection center stands relative to the canvas:
projection.translate([point])
If point is specified, sets the projection’s translation offset to the specified two-element array [x, y] and returns the projection. If point is not specified, returns the current translation offset which defaults to [480, 250]. The translation offset determines the pixel coordinates of the projection’s center. The default translation offset places ⟨0°,0°⟩ at the center of a 960×500 area.
When I want to center a feature in the canvas, I like to set the projection center to the center of the feature bounding box - this works for me when using mercator (WGS 84, used in google maps) for my country (Brazil), never tested using other projections and hemispheres. You may have to make adjustments for other situations, but if you nail these basic principles you will be fine.
For example, given a projection and path:
var projection = d3.geo.mercator()
.scale(1);
var path = d3.geo.path()
.projection(projection);
The bounds method from path returns the bounding box in pixels. Use it to find the correct scale, comparing the size in pixels with the size in map units (0.95 gives you a 5% margin over the best fit for width or height). Basic geometry here, calculating the rectangle width/height given diagonally opposed corners:
var b = path.bounds(feature),
s = 0.9 / Math.max(
(b[1][0] - b[0][0]) / width,
(b[1][1] - b[0][1]) / height
);
projection.scale(s);
Use the d3.geo.bounds method to find the bounding box in map units:
b = d3.geo.bounds(feature);
Set the center of the projection to the center of the bounding box:
projection.center([(b[1][0]+b[0][0])/2, (b[1][1]+b[0][1])/2]);
Use the translate method to move the center of the map to the center of the canvas:
projection.translate([width/2, height/2]);
By now you should have the feature in the center of the map zoomed with a 5% margin.
There is a center() method you can use that accepts a lat/lon pair.
From what I understand, translate() is only used for literally moving the pixels of the map. I am not sure how to determine what scale is.
In addition to Center a map in d3 given a geoJSON object, note that you may prefer fitExtent() over fitSize() if you want to specify a padding around the bounds of your object. fitSize() automatically sets this padding to 0.
I was looking around on the Internet for a fuss-free way to center my map, and got inspired by Jan van der Laan and mbostock's answer. Here's an easier way using jQuery if you are using a container for the svg. I created a border of 95% for padding/borders etc.
var width = $("#container").width() * 0.95,
height = $("#container").width() * 0.95 / 1.9 //using height() doesn't work since there's nothing inside
var projection = d3.geo.mercator().translate([width / 2, height / 2]).scale(width);
var path = d3.geo.path().projection(projection);
var svg = d3.select("#container").append("svg").attr("width", width).attr("height", height);
If you looking for exact scaling, this answer won't work for you. But if like me, you wish to display a map that centralizes in a container, this should be enough. I was trying to display the mercator map and found that this method was useful in centralizing my map, and I could easily cut off the Antarctic portion since I didn't need it.
To pan/zoom the map you should look at overlaying the SVG on Leaflet. That will be a lot easier than transforming the SVG. See this example http://bost.ocks.org/mike/leaflet/ and then How to change the map center in leaflet
With mbostocks' answer, and Herb Caudill's comment, I started running into issues with Alaska since I was using a mercator projection. I should note that for my own purposes, I am trying to project and center US States. I found that I had to marry the two answers with Jan van der Laan answer with following exception for polygons that overlap hemispheres (polygons that end up with a absolute value for East - West that is greater than 1):
set up a simple projection in mercator:
projection = d3.geo.mercator().scale(1).translate([0,0]);
create the path:
path = d3.geo.path().projection(projection);
3.set up my bounds:
var bounds = path.bounds(topoJson),
dx = Math.abs(bounds[1][0] - bounds[0][0]),
dy = Math.abs(bounds[1][1] - bounds[0][1]),
x = (bounds[1][0] + bounds[0][0]),
y = (bounds[1][1] + bounds[0][1]);
4.Add exception for Alaska and states that overlap the hemispheres:
if(dx > 1){
var center = d3.geo.centroid(topojson.feature(json, json.objects[topoObj]));
scale = height / dy * 0.85;
console.log(scale);
projection = projection
.scale(scale)
.center(center)
.translate([ width/2, height/2]);
}else{
scale = 0.85 / Math.max( dx / width, dy / height );
offset = [ (width - scale * x)/2 , (height - scale * y)/2];
// new projection
projection = projection
.scale(scale)
.translate(offset);
}
I hope this helps.
For people who want to adjust verticaly et horizontaly, here is the solution :
var width = 300;
var height = 400;
var vis = d3.select("#vis").append("svg")
.attr("width", width).attr("height", height)
d3.json("nld.json", function(json) {
// create a first guess for the projection
var center = d3.geo.centroid(json)
var scale = 150;
var offset = [width/2, height/2];
var projection = d3.geo.mercator().scale(scale).center(center)
.translate(offset);
// create the path
var path = d3.geo.path().projection(projection);
// using the path determine the bounds of the current map and use
// these to determine better values for the scale and translation
var bounds = path.bounds(json);
var hscale = scale*width / (bounds[1][0] - bounds[0][0]);
var vscale = scale*height / (bounds[1][1] - bounds[0][1]);
var scale = (hscale < vscale) ? hscale : vscale;
var offset = [width - (bounds[0][0] + bounds[1][0])/2,
height - (bounds[0][1] + bounds[1][1])/2];
// new projection
projection = d3.geo.mercator().center(center)
.scale(scale).translate(offset);
path = path.projection(projection);
// adjust projection
var bounds = path.bounds(json);
offset[0] = offset[0] + (width - bounds[1][0] - bounds[0][0]) / 2;
offset[1] = offset[1] + (height - bounds[1][1] - bounds[0][1]) / 2;
projection = d3.geo.mercator().center(center)
.scale(scale).translate(offset);
path = path.projection(projection);
// add a rectangle to see the bound of the svg
vis.append("rect").attr('width', width).attr('height', height)
.style('stroke', 'black').style('fill', 'none');
vis.selectAll("path").data(json.features).enter().append("path")
.attr("d", path)
.style("fill", "red")
.style("stroke-width", "1")
.style("stroke", "black")
});
How I centered a Topojson, where I needed to pull out the feature:
var projection = d3.geo.albersUsa();
var path = d3.geo.path()
.projection(projection);
var tracts = topojson.feature(mapdata, mapdata.objects.tx_counties);
projection
.scale(1)
.translate([0, 0]);
var b = path.bounds(tracts),
s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
projection
.scale(s)
.translate(t);
svg.append("path")
.datum(topojson.feature(mapdata, mapdata.objects.tx_counties))
.attr("d", path)

Resources