D3js - Reposition tree graph in viewbox - javascript

I have used viewbox to enable my tree graph to be able to resized based on the browser window size. However, I could not position the tree to be at the center of the view box. I have tried to apply transform-translate attribute to the graph, the position remain unchanged. What attribute/changes do I have to apply to position the tree graph? Below, the first image is the current output. Image two shows the desired output. I have attached the initialisation code for the tree graph.
var margin = { top: 40, right: 120, bottom: 20, left: 120 };
var width = 700 - margin.right - margin.left;
var height = 500 - margin.top - margin.bottom;
var i = 0, duration = 750;
var tree = d3.layout.tree()
.nodeSize([110,110])
var diagonal = d3.svg.diagonal()
.projection(function (d) { return [d.x, d.y]; });
var svg = d3.select(treeContainerDom)
.append("div")
.classed("svg-container", true) //container class to make it responsive
.append("svg")
//responsive SVG needs these 2 attributes and no width and height attr
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "-50 0 1000 1000")
//.attr("viewBox", "0 0 "+width+" "+height)
//class to make it responsive
.classed("svg-content-responsive", true)
.call(zm = d3.behavior.zoom().scaleExtent([0.5,2]).on("zoom", redraw)).append("g");

Related

Setting ticks on a time scale during zoom

The snippet below creates a single x axis with starting ticks of 10. During zoom I'm updating ticks on the rescaled axis with:
.ticks(startTicks * Math.floor(event.transform.k))
With .scaleExtent([1, 50]) I can get down from years to 3-hourly blocks fairly smoothly (besides a little label overlap here and there).
But, when I request the number of ticks applied on the scale (xScale.ticks().length) I get a different number to the one I just assigned.
Also, when I get the labels (xScale.ticks().map(xScale.tickFormat())) they differ from the ones rendered as I get deeper into the zoom.
Reading here:
An optional count argument requests more or fewer ticks. The number of
ticks returned, however, is not necessarily equal to the requested
count. Ticks are restricted to nicely-rounded values (multiples of 1,
2, 5 and powers of 10), and the scale’s domain can not always be
subdivided in exactly count such intervals. See d3.ticks for more
details.
I understand I might not get the number of ticks I request, but it's counter-intuitive that:
I request more and more ticks (per k) - between 10 and 500
Then the returned ticks fluctuates between 5 and 19.
Why is this ? Is there a better or 'standard' way to update ticks whilst zooming for scaleTime or scaleUtc ?
var margin = {top: 0, right: 25, bottom: 20, left: 25}
var width = 600 - margin.left - margin.right;
var height = 40 - margin.top - margin.bottom;
// x domain
var x = d3.timeDays(new Date(2020, 00, 01), new Date(2025, 00, 01));
// start with 10 ticks
var startTicks = 10;
// zoom function
var zoom = d3.zoom()
.on("zoom", (event) => {
var t = event.transform;
xScale
.domain(t.rescaleX(xScale2).domain())
.range([0, width].map(d => t.applyX(d)));
var zoomedRangeWidth = xScale.range()[1] - xScale.range()[0];
var zrw = zoomedRangeWidth.toFixed(4);
var kAppliedToWidth = kw = t.k * width;
var kw = kAppliedToWidth.toFixed(4);
var zoomTicks = zt = startTicks * Math.floor(t.k);
svg.select(".x-axis")
.call(d3.axisBottom(xScale)
.ticks(zt)
);
var realTicks = rt = xScale.ticks().length;
console.log(`zrw: ${zrw}, kw: ${kw}, zt: ${zt}, rt: ${rt}`);
console.log(`labels: ${xScale.ticks().map(xScale.tickFormat())}`);
})
.scaleExtent([1, 50]);
// x scale
var xScale = d3.scaleTime()
.domain(d3.extent(x))
.range([0, width]);
// x scale copy
var xScale2 = xScale.copy();
// svg
var svg = d3.select("#scale")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// clippath
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", 0)
.attr("width", width)
.attr("height", height);
// x-axis
svg.append("g")
.attr("class", "x-axis")
.attr("clip-path", "url(#clip)")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale)
.ticks(startTicks));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.3.1/d3.min.js"></script>
<div id="scale"></div>
The issue is in how the xScale is being updated on zoom.
The current approach in the example is:
xScale
.domain(t.rescaleX(xScale2).domain())
.range([0, width].map(d => t.applyX(d)));
This is doing two things:
Creating a rescaled copy of xScale2, but only to get its domain.
Extending the range of the xScale depending on the transform.
Because of step 2, the scale range is growing outside of the screen. When you request 500 ticks but only see 10, it is because there are 490 out of the viewport.
The solution is that continuous scales don't need to have the range updated on zoom, because the rescaleX method is enough for the transformation process.
The appropriate way to rescale a continuous scale on zoom is:
xScale = t.rescaleX(xScale2)
Which changes only the domain and keeps the range intact.
Consider this example to illustrate why only changing the domain is enough: If a scale maps from a domain [0,1] to a range [0, 100], and it is transformed with rescaleX, the new scale will now map from another domain (say, [0.4, 0.6]) to the same range [0, 100]. This is the zoom concept: it was showing data from 0 to 1 in a 100 width viewport, but now it is showing data from 0.4 to 0.6 in the same viewport; it "zoomed in" to 0.4 and 0.6.
The incorrect format returned from xScale.tickFormat() was a consequence of the range extension, but also of a mismatch between the displayed ticks and the computed ticks. The method only return the same ticks that are displayed if it also consideres the same amount of ticks, which is informed in the first parameter (in your example, it would be xScale.tickFormat(zt)). Since it had no arguments, it defaults to 10, and the 10 ticks computed in the time scale could be different or be in a different time granularity than the zt ticks that are displayed.
In summary, the snippet needs three changes:
Change 1: Update only the domain directly with rescaleX.
Change 2: Fix zoom ticks to a number, such as 10.
Change 3: Consider the number of ticks when using the tickFormat method.
The snippet below is updated with those changes:
var margin = {top: 0, right: 25, bottom: 20, left: 25}
var width = 600 - margin.left - margin.right;
var height = 40 - margin.top - margin.bottom;
// x domain
var x = d3.timeDays(new Date(2020, 00, 01), new Date(2025, 00, 01));
// start with 10 ticks
var startTicks = 10;
// zoom function
var zoom = d3.zoom()
.on("zoom", (event) => {
var t = event.transform;
// Change 1: Update only the domain directly with rescaleX
xScale = t.rescaleX(xScale2);
var zoomedRangeWidth = xScale.range()[1] - xScale.range()[0];
var zrw = zoomedRangeWidth.toFixed(4);
var kAppliedToWidth = kw = t.k * width;
var kw = kAppliedToWidth.toFixed(4);
// Change 2: Fix zoom ticks to a number, such as 10
var zoomTicks = zt = 10
svg.select(".x-axis")
.call(d3.axisBottom(xScale)
.ticks(zt)
);
var realTicks = rt = xScale.ticks().length;
console.log(`zrw: ${zrw}, kw: ${kw}, zt: ${zt}, rt: ${rt}`);
// Change 3: Consider zt when using the tickFormat method
console.log(`labels: ${xScale.ticks().map(xScale.tickFormat(zt))}`);
})
.scaleExtent([1, 50]);
// x scale
var xScale = d3.scaleTime()
.domain(d3.extent(x))
.range([0, width]);
// x scale copy
var xScale2 = xScale.copy();
// svg
var svg = d3.select("#scale")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.call(zoom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// clippath
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("x", 0)
.attr("width", width)
.attr("height", height);
// x-axis
svg.append("g")
.attr("class", "x-axis")
.attr("clip-path", "url(#clip)")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale)
.ticks(startTicks));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.3.1/d3.min.js"></script>
<div id="scale"></div>

How to make SVG responsive in D3 with viewbox?

I have noticed that in various examples, an SVG is responsive (changes size in response to changes in window size) and sometimes it is not responsive when using viewbox / preserveAspectRatio.
Here is a very simple example. I am using viewbox and preseverAspectiRatio on the SVG element like every other example, but yet, it is not responsive, why?
<html>
<meta charset="utf-8">
<body>
<div id ="chart"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script>
var svgContainer = d3.select("#chart")
.append("svg")
.attr("width", 200)
.attr("height", 200)
.attr("viewBox", "0 0 100 100")
.attr("preserveAspectRatio", "xMinYMin meet")
//Draw the Circle
var circle = svgContainer.append("circle")
.attr("cx", 50)
.attr("cy", 50)
.attr("r", 50);
</script>
</body>
</html>
Currently your svg is not resize because you have specified a fixed with for the svg container of 200x200.
var svgContainer = d3.select("#chart")
.append("svg")
.attr("width", 200) // setting fixed width
.attr("height", 200) // setting fixed width
One solution is to change these to percentages, which will rescale to the size of its parent.
var svgContainer = d3.select("#chart")
.append("svg")
.attr("width", '100%') // percent width
.attr("height", 100%) // percent height
Another possible solution is to use the Window#resize event, and change the size of the svg based on the changes.
window.addEventListener('resize', function () {
// select svg and resize accordingly
});
I should add that in chrome you can use ResizeObserver to watch for changes to the svg parents size, and resize the svg accordingly from that.
const resizeOb = new ResizeObserver((entries: any[]) => {
for (const entry of entries) {
const cr = entry.contentRect;
const width = cr.width; // parent width
const height = cr.height; // parent height
// resize svg
}
});
this.resizeOb.observe(svgParentElement);

Setting Initial D3 Transform Properties

I've created a map of Washington D.C. roads by converting a shapefile to JSON, and then mapping it. I have set up a zoom property to allow users to zoom in and out. It's here:
https://bl.ocks.org/KingOfCramers/raw/c8d575fb1322590012323a7953908d5f/56bd68ec204046076114413ef777706726bdb50a/
However, I'm having problems because when the user "zooms" in on the projected map, it jumps back to the middle of the screen. This must have something to do with the coordinates the zoom event initially receives. How do I change those settings so that, upon interaction, the map begins to zoom from its current position?
Here's the full code:
var transLat = -100;
var transLon = -350;
var transScale = 1.6;
var width = 960;
var height = 600;
var margin = {top: 0, bottom: 0, left: 0, right: 0}
d3.queue()
.defer(d3.json, "simpleroads.json")
.defer(d3.csv, "locations.csv")
.await(ready)
function ready(error, world, locations) {
var projection = d3.geoIdentity().reflectY(true).fitSize([width,height],world)
var path = d3.geoPath().projection(projection) // Geopath generator
var zoomExtent = d3.zoom().scaleExtent([1.6, 3]);
function zoom() {
g.attr("transform", d3.event.transform)
}
console.log(locations)
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.bottom + margin.top)
.style("background-color","lightgrey")
.call(zoomExtent
.on("zoom", zoom))
var g = svg.append("g")
.attr("class", "mapInformation")
.attr("transform", `translate(${transLat},${transLon}) scale(${transScale})`)
var paths = g.selectAll("path")
.data(world.features)
.enter()
.append("path")
.attr("d", path)
}
You have to pass the initial transform to the zoom function. In your case:
.call(zoomExtent.transform,
d3.zoomIdentity.translate(transLat, transLon).scale(transScale));
Here is the updated blockbuilder: http://blockbuilder.org/anonymous/b1d7dcc08c3fbee934fd415d7127dd14

Internet Explorer Scaling not working properly

I have a map created with d3. I set it to have the width of the parent div (with id map) and height with a ratio of 5/9 to to the width. The viewBox has been set to "0 0 width height".
Here is the setup:
var width = $("#map").width(),
height = width * 500 / 900,
active = d3.select(null);
var projection = d3.geo.albersUsa()
.scale(width)
.translate([width / 2, height / 2]);
var path = d3.geo.path()
.projection(projection);
projection = d3.geo.albersUsa()
.scale(width)
.translate([width / 2, height / 2]);
path = d3.geo.path()
.projection(projection);
svg = d3.select("#map").append("svg")
.attr("width", "100%")
.attr("height", "100%")
.attr("viewBox", "0 0 " + width + " " + height)
g = svg.append("g");
Here is how it looks like in Chrome:
But it looks like this in IE:
The scaling is completely messed up. I believe it has to do with the viewBox. Could someone explain how to solve this issue?
Thanks!
The problem you are having is that height: "100%" is handled differently by Chrome and IE. You have smartly set your height variable to be based on your width and aspect ratio, but to take advantage of this, you'll need to use these values when setting the dimensions of your svg as well as the viewBox:
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", "0 0 " + width + " " + height);

Strange behavior of Bootstrap Accordion Expand Height with D3 Graph

I have a circle packing layout of D3 within a bootstrap accordion (where all the tabs are expanded on load). I have tried to allow for a responsive layout by using the following:
var vis = d3.select("#Vis_container").insert("svg:svg", "h2")
.attr("width", '100%')
.attr("height", '100%')
.attr('viewBox', '0 0 ' + w + ' ' + h)
.attr('preserveAspectRatio', 'xMinYMin')
.append("svg:g")
.attr("transform", "translate(" + 0 + "," + 0 + ")");
If you collapse the tab that contains the D3 layout and expand it again, the svg's height is fine, but the container it is in does not resize correctly i.e. not enough height is provided for the entire layout to show.
Note: I used the container's width as its height as well, due to the height not being available on load.
var w = $('#Vis_container').width(),
h = $('#Vis_container').width()
Can anyone help me to correct the height of the container / accordion when the tab is expanded again?
See the fiddle here: http://jsfiddle.net/Sim1/sLMC2/7/
I am offering this as an alternative. Here is the FIDDLE.
Basically, I gave an initial (and hopefully reasonable) value for width and height, and then added code to re-size after that, if desired. May this will help.
var vis = d3.select("#Vis_container")
.insert("svg:svg", "h2")
.attr("width", h)
.attr("height", w)
...
var chart = $("#Vis_container>svg"),
aspect = chart.width() / chart.height(),
container = chart.parent();
$(window).on("resize", function() {
var targetWidth = container.width();
chart.attr("width", targetWidth);
chart.attr("height", Math.round(targetWidth / aspect));
}).trigger("resize");

Categories