What Transformation values to calculate for scale and translate if you want to zoom - d3.js

Im looking at this example which shows how one can use the zoom functionality to zoom in a specified domain range
https://bl.ocks.org/mbostock/431a331294d2b5ddd33f947cf4c81319
Im confused about this part:
var d0 = new Date(2003, 0, 1),
d1 = new Date(2004, 0, 1);
// Gratuitous intro zoom!
svg.call(zoom).transition()
.duration(1500)
.call(zoom.transform, d3.zoomIdentity
.scale(width / (x(d1) - x(d0))) // I think this is to caulcuate k which is the zoom factor
.translate(-x(d0), 0)); // but what is this?
I'm having trouble understanding the calculations that are done. Correct me if my assumptions are wrong
d3.zoomIdentity This is a transformation that does nothing when applied.
.scale(width / (x(d1) - x(d0))) This is to calculate how much scale to apply by calculating the ratio between the width and the pixel difference between the two data points d0 and d1
.translate(-x(d0), 0)) I don't understand this part. Why is x(d0) negated and how does the x coordinate of d(0) relate to how much translation need to be applied?

The translate value is aligning the graph so that x(d0) is the leftmost x value visible in the plot area. This ensures the visible portion of the plot area extends from d0 through d1 (the visible subdomain). If our full domain for the x scale has a minimum of 0, then x(0) will be shifted left (negative shift) x(d0) pixels.
I'll use a snippet to demonstrate:
var svg = d3.select("svg"),
margin = {top: 10, right: 50, bottom: 70, left: 200},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
// Scale for Data:
var x = d3.scaleLinear()
.range([0, width])
.domain([0,20]);
// Scale for Zoom:
var xZoom = d3.scaleLinear()
.range([0,width])
.domain([0,width]);
var xAxis = d3.axisBottom(x).ticks(5);
var xZoomAxis = d3.axisBottom(xZoom);
var zoom = d3.zoom()
.scaleExtent([1, 32])
.translateExtent([[0, 0], [width, height]])
.extent([[0, 0], [width, height]])
.on("zoom", zoomed);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// plot area
g.append("rect")
.attr("width",width)
.attr("height",height)
.attr("fill","url(#stripes)");
g.append("text")
.attr("x",width/2)
.attr("y",height/2)
.style("text-anchor","middle")
.text("plot area");
g.append("line")
.attr("y1",0)
.attr("y2",height)
.attr("stroke-width",1)
.attr("stroke","black");
// zoomed plot area:
var rect = g.append("rect")
.attr("width",width)
.attr("height",height)
.attr("fill","lightgrey")
.attr("opacity",0.4);
// Axis for plot:
g.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Axis for zoom:
g.append("g")
.attr("class", "axis axis-zoom-x")
.attr("transform", "translate(0,"+(height+30)+")")
.call(xZoomAxis);
var text = g.append("text")
.attr("y", height+60)
.attr("text-anchor","middle")
.text("zoom units")
.attr("x",width/2);
// Gratuitous intro zoom:
var d1 = 18;
var d0 = 8;
svg.call(zoom).transition()
.duration(2000)
.call(zoom.transform, d3.zoomIdentity
.scale(width / (x(d1) - x(d0)))
.translate(-x(d0), 0));
function zoomed() {
var t = d3.event.transform, xt = t.rescaleX(x);
xZoom.range([xt(0),xt(20)]);
g.select(".axis--x").call(xAxis.scale(xt));
g.select(".axis-zoom-x").call(xZoomAxis.scale(xZoom));
rect.attr("x", xt(0));
rect.attr("width", xt(20) - xt(0));
text.attr("x", xt(10));
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="400" height="180">
<defs>
<pattern id="stripes" patternUnits="userSpaceOnUse" width="8" height="8" patternTransform="rotate(45 0 0)">
<rect width="3" height="8" fill="orange"></rect>
</pattern>
</defs>
</svg>
Snippet Explanation:
Plot area: orange stripes
Full scaled extent of data: grey box.
Left hand side of plot area is x=0 (pixels) for the parent g that holds everything.
As we zoom in the bounds of our data exceeds the plot area. We want to show a specific subdomain of our data. We achieve part of that with the scale (as you correctly deduce) but the other portion is with the translate: we push values less than the lowest value of our x subdomain to the left. By pushing the entire graph left by an amount equal to x(d0), x(d0) appears as the leftmost coordinate of the plot area.

Related

Scaling D3 chart inside svg while keeping margins the same width?

Working on rendering charts using D3 that has to scale to different resolutions.
I would like to keep everything except the chart path itself at static size which includes all text, line thickness and margins.
I think I have figured out how to handle everything except the margins and cant find anyone who has the same issue so hopefully someone can help.
I've written a short test script that hopefully explains how I have my margins and everything else setup. The red are margins and the grey rectangle is where I draw my chart. Added a text element to show how I scale texts, line thickness etc.
What I want to achieve is that the margin have a static width as I scale the window. If I scale the svg and make it 100px wider I want the red rectangle to become 100px wider while the red margins stay the same.
I had been thinking about increasing the middle rectangle using the scale variable to have it increase "faster" but trying to use something like transform(scale on the rectangle in the resized() function produced unexpected results.
var margin = {top: 100, right: 150, bottom: 100, left: 150}
var outerWidth = 1600,
outerHeight = 900;
var width = outerWidth - margin.right - margin.left,
height = outerHeight - margin.top - margin.bottom;
svg = d3.select(".plot-div").append("svg")
.attr("class", "plot-svg")
.style("background-color", "red")
.attr("width", "100%")
.attr("viewBox", "0 0 " + outerWidth + " " + outerHeight);
g = svg.append("g")
.attr("class", "plot-space")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", "grey");
text = d3.select(".plot-space").append("text")
.text("Text with the background")
.attr("y", 50)
.attr("x", 50)
.attr("font-size", 16)
.attr("font-family", "monospace")
.attr("fill", "white");
window.addEventListener("resize", resized);
function resized(){
var current_width = svg.node().getBoundingClientRect().width;
var scale = outerWidth / current_width;
text.attr("transform", "scale(" + scale + " " + scale + ")");
}
resized();
jsfiddle for you to play around with.
https://jsfiddle.net/2tuompye/4/

d3js: Unable to place horizontally bars within the axis

I'm learning to make charts in d3 from scratch without taking someone else code and modifying it. I can successfully create a x & y axis vertical bar chart. But when it comes to transform the same chart to horizontal bar chart I end up in a mess. Here is my code so far:
var data = [{
name: "China",
value: 1330141295
}, {
name: "India",
value: 1173108018
}, {
name: "Indonesia",
value: 242968342
}, {
name: "Russia",
value: 139390205
}];
//set margins
var margin = {
top: 30,
right: 30,
bottom: 30,
left: 40
};
var width = 960 - margin.left - margin.right;
var height = 600 - margin.top - margin.bottom;
//set scales & ranges
var yScale = d3.scaleBand().range([0, height]).padding(0.1)
var xScale = d3.scaleLinear().range([0, width])
//draw the svg
var svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom).append("g")
.attr("transform", "translate(" + margin.left * 2 + "," + margin.top + ")")
//load the data
data.forEach(function(d) {
d.population = +d.population;
});
//set domains
xScale.domain([0, d3.max(data, function(d) {
return d.population
})])
yScale.domain(data.map(d => d.name))
//add x & y Axis
svg.append("g")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(d3.axisBottom(xScale));
svg.append("g")
.call(d3.axisLeft(yScale))
.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("y", d => yScale(d.name))
.attr("height", d => yScale(d.name))
.attr("x", d => width - xScale(d.population))
.attr("width", yScale.bandwidth())
Thank you very much.
You need to change a lot of things in your code.
TL;DR
change value to population in the array
scales are used to convert values to proportional pixel values
height is the vertical size of an element. you should use yScale(d.name)
width is the horizontal size of an element. you should use xScale(d.population)
y is the vertical position of an element. you should use yScale.bandwidth()
x is the vertical position of an element. you should use 0
use selectAll("rect") on a new appended g or the svg element not the same g element that has the axises on it
add fill attribute so that your rects have color
You have the population field labelled value but you're calling population through out the code to use it. So replace value with population in your data objects.
Next you need to change the way you're setting up the rects. use selectAll on the svg element directly or append another g to the svg element and add the rects on that element. Right now your code attempts to add the rects to the same g element as the x axis.
Make sure you are setting the attributes of the rects correctly. Height is the size in pixels of the rect element not the position. y is the position of the rects vertically from the top downwards. this means the height attribute should use yScale(d.name) and width should use xScale(d.population) because they are the width and length of the rectangles, or rect elements. x and y are the coordinate positions of the element from the top left corner of the svg element. Use them to determine the starting position of the top left pixel of your rects. You should have y as yScale.bandwidth() and x as 0.

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 Geo Tile Baselayer Offset

I'm using d3.geo.tile() and have used it successfully before but this time the tile layer doesn't seem to draw at the same scale and translate as the point layer. The below code creates a map that pans and draws just fine, but draws the circles, which should be in the Mediterranean, in Africa. If I zoom in, it scales the tiles and circles just fine, it's as if my xy coordinates are off, but they aren't.
I get the feeling that it's actually drawing the base layer without offsetting and scaling it properly because it should be centering on the coordinates 12,42, but it's a great big mystery to me since this exact same code works fine in a different application.
If someone can spot some problem, or just a hint, that would help.
function createNewMap(){
width = 1200, height = 800;
var tile = d3.geo.tile()
.size([1200, 800]);
var projection = d3.geo.mercator()
.scale((1 << 12) / 2 / Math.PI)
.translate([width / 2, height / 2]);
var center = projection([12, 42]);
var zoom = d3.behavior.zoom()
.scale(projection.scale() * 2 * Math.PI)
.scaleExtent([1 << 10, 1 << 17])
.translate([width - center[0], height - center[1]])
.on("zoom", zoomed);
projection
.scale(1 / 2 / Math.PI)
.translate([0, 0]);
var svg = d3.select("#newMapId").append("svg")
.attr("width", width)
.attr("height", height)
.call(zoom);
var raster = svg.append("g");
var vector = svg.append("g");
vector.selectAll("g").data(dataModule.polisData).enter().append("g")
.attr("class", "sites")
.attr("transform", function(d) {return "translate(" + (projection([d.xcoord,d.ycoord])[0]) + "," + (projection([d.xcoord,d.ycoord])[1]) + ")scale("+(projection.scale())+")"})
.append("circle")
.attr("class", "sitecirc");
zoomed();
function zoomed() {
var tiles = tile
.scale(zoom.scale())
.translate(zoom.translate())
();
var image = raster
.attr("transform", "scale(" + tiles.scale + ")translate(" + tiles.translate + ")")
.selectAll("image")
.data(tiles, function(d) { return d; });
image.exit()
.remove();
image.enter().append("image")
.attr("xlink:href", function(d) { return "http://" + ["a", "b", "c", "d"][Math.random() * 4 | 0] + ".tiles.mapbox.com/v3/elijahmeeks.map-zm593ocx/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.attr("width", 1)
.attr("height", 1)
.attr("x", function(d) { return d[0]; })
.attr("y", function(d) { return d[1]; });
vector
.attr("transform", "translate(" + zoom.translate() + ")scale(" + zoom.scale() + ")");
d3.selectAll(".sitecirc")
.attr("r", 10 / zoom.scale());
}
Your code appears to be based on my example that changes the SVG transform on zoom. Changing the transform is a nice technique when you have complex geometry that you just want to scale and translate when you pan or zoom — it’s typically faster than reprojecting everything — but it’s also more complex than changing the projection on zoom.
The code doesn’t change very much if you want to change the projection on zoom. In essence:
projection
.scale(zoom.scale() / 2 / Math.PI)
.translate(zoom.translate());
And then re-run your d3.geo.path to re-render. As shown in bl.ocks.org/9535021:
Also, fixing the projection and changing the transform can cause precision problems if you zoom in a lot. Another reason to only use that technique when it offers substantial performance gains by avoid reprojection. And here reprojecting is super-cheap because it’s just a handful of points.

make a circle progress bar, but animate arc.endAngle (angle >180 degree) stops working in d3.js

I am running D3.js to draw a progress bar in circle shape, which you will see the demo on jsfiddle , the progress bar has a transition animation.
The main code is
var width = 960,
height = 500,
twoPi = 2 * Math.PI,
progress = 0,
total = 1308573, // must be hard-coded if server doesn't report Content-Length
formatPercent = d3.format(".0%");
var arc = d3.svg.arc()
.startAngle(0)
.innerRadius(0)
.outerRadius(240);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var meter = svg.append("g")
.attr("class", "progress-meter");
meter.append("path")
.attr("class", "background")
.attr("d", arc.endAngle(twoPi));
var foreground = meter.append("path")
.attr("class", "foreground");
foreground.attr("d", arc.endAngle(twoPi * 0))
foreground.transition().duration(1500).attr("d", arc.endAngle( twoPi * 2/3 ));
var text = meter.append("text")
.attr("text-anchor", "middle")
.attr("dy", ".35em");
to make the progress bar move, we only need to change to the arc.endAngle(), which is on the line.
foreground.transition().duration(1500).attr("d", arc.endAngle( twoPi * 2/3 ));
if the angle is less than 180, ( endangle < twoPi*1/2), then the animation works fine, but when the angle is larger than 180, so means endangle >= twoPi*1/2. then the animation would not show, and if you look at the console, you will find many errors on d3.js
Error: Problem parsing d="M1.1633760361312584e-14,-190A190,190 0 1.481481481481482e-7,1 -0.000022772330200401806,-189.9999883969182L0,0Z" meeting.html:1
2
Error: Problem parsing d="M1.1633760361312584e-14,-190A190,190 0 2.56e-7,1 -0.00003935058659476369,-189.99997994987467L0,0Z"
so what is the exact problem for this, how to solve it
It doesn't work because you can't use the standard transition for radial paths. By default, it simply interpolates the numbers without knowing what they represent, so in your case, you end up with some really small numbers (e.g. 1.1633760361312584e-14) which Javascript represents in exponential notation which is not valid for SVG paths.
The solution is to use a custom tween function that knows how to interpolate arcs:
function arcTween() {
var i = d3.interpolate(0, twoPi * 2/3);
return function(t) {
return arc.endAngle(i(t))();
};
}
Complete example here. You may also be interested in this example, which shows how to do it with data bound to the paths.

Resources