D3 clipping issues on zoom - d3.js

I would like to use zoom behaviour in a simple d3 graph. The thing is that whenever I zoom in, the "main" group elements take up the whole svg space, which causes other existing elements such as axes and texts to get overlapped. I have read that clipping can be used to solve this problem but I didn't manage to get it working properly.
The following image (zoom in was applied) shows the problem:
Related example with what I have tried so far can be found here.

I was finally able to solve this problem. Key points were:
Use proper svg element for clipping, normally rect does the job with corresponding width/height (same as your "drawing" area).
Transform all elements drawn within the clip path (region), and not the parent group.
In code (omitting irrelevant parts), the result is:
// Scales, axis, etc.
...
// Zoom behaviour & event handler
let zoomed = function () {
let e = d3.event;
let tx = Math.min(0, Math.max(e.translate[0], width - width*e.scale));
let ty = Math.min(0, Math.max(e.translate[1], height - height*e.scale));
zoom.translate([tx,ty]);
main.selectAll('.circle').attr('transform', 'translate(' + [tx,ty] + ')scale(' + e.scale + ')');
svg.select('.x.axis').call(xAxis);
svg.select('.y.axis').call(yAxis);
}
let zoom = d3.behavior.zoom()
.x(x)
.y(y)
.scaleExtent([1,8])
.on('zoom', zoomed);
const svg = d3.select('body').append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.attr('pointer-events', 'all')
.call(zoom);
const g = svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// Set clip region, rect with same width/height as "drawing" area, where we will be able to zoom in
g.append('defs')
.append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('x', 0)
.attr('y', 0)
.attr('width', width)
.attr('height', height);
const main = g.append('g')
.attr('class', 'main')
.attr('clip-path', 'url(#clip)');
let circles = main.selectAll('.circle').data(data).enter();

Related

D3 Pie Chart rotate only the arcs

My chart needs a label in the center of the donut that should not rotate when clicked.
https://codepen.io/scratchy303/pen/dyXMzrz
Can I append the label in a sibling "g"roup and rotate the arcs group?
Can I translate just the arcs rather than rotating the entire SVG?
What's the simplest solution?
var svg = d3.select("#pieChart").append("svg")
.attr("width", '100%')
.attr("height", '100%')
.attr('viewBox', '0 0 ' + Math.min(width, height) + ' ' + Math.min(width, height))
.attr('preserveAspectRatio', 'xMinYMin')
.append("g")
.attr("transform", "translate(" + radius + "," + height / 2 + ")")
.style("filter", "url(#drop-shadow)");
svg.append("g")
.append("text")
.attr("text-anchor", "middle")
.text("Donut Name");
I found a solution by counter-rotating my text. My biggest hurdle was simply traversing my SVG in D3.
//start by selecting the parent "g"roup and then you can select the text
d3.select(i.parentNode).select(".donut-label")
.transition()
.duration(1000)
.attr("transform", "rotate(" + (-angle) + ")");

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/

d3 Tree - How to auto adjust node spacing when zooming

I have a d3 tree with zoom and pan functionality. The tree can have a great number of child nodes for any parent. This can result in vertically squashed up nodes.
I would like the nodes to automatically adjust their spacing when zooming out, especially the vertical spacing.
The appending d3 svg and the zoom part of the d3 code:
var svg = d3.select("#tree_container")
.append("svg")
.attr("width", width + margin.right + margin.left)
.attr("height", height + margin.top + margin.bottom)
.call( d3.behavior.zoom().on( "zoom", function () {
svg.attr( "transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")" )
} ) )
.append("g");
Fiddle
Would it be enough to set the node sizes? Then d3 will automatically adjust positions of the nodes.
var radius = 10;
var tree = d3.layout.tree()
.size([height, width])
.nodeSize([radius*2+3]);
...
function update(source) {
...
nodeUpdate.select("circle")
.attr("r", radius)
As Fiddle

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.

Updating line graph

I'm looking for the best way to update my line graph. The incoming data is not 'new', it's just switching between many different data sets. My issue is that .append('path') simply isn't working, and I'm not sure why. It may have to do with my grouping: I like to put margins around my graphs so axis and labels aren't clipped. To do this, my HTML looks like the following...
<svg w, h id='svg-container'>
<g translate(w - margin, h - margin) id='chart-container'>
<g id='x-axis'>
<g id='y-axis'>
In my code I would like to select chart-container and append a <path d='...' id='line'>. Later, when updating the graph, I would hope to be able to simply select('chart-container').select('path') and update that selection.
Initial setup of the chart:
var svgContainer = d3.select(element[0]).append('svg')
.attr({
width: width + margin.left + margin.right,
height: height + margin.top + margin.bottom
})
.classed('svg-container', true);
var chartContainer = svgContainer.append('g')
.classed('chart-container', true)
.attr('transform', "translate(" + margin.left + "," + margin.top + ")");
chartContainer.append('path')
.data(activeData)
.attr('d', lineFunc)
.attr('stroke', 'blue')
.attr('stroke-width',
.attr('fill', 'none');
Update the chart later on:
svgContainer = d3.select('#line-container').select('.svg-container')
.attr({
width: width + margin.left + margin.right,
height: height + margin.top + margin.bottom
});
chartContainer = svgContainer.select('.chart-container')
.attr('transform', "translate(" + margin.left + "," + margin.top + ")");
chartContainer.select('path')
.data(activeData)
.attr('d', lineFunc)
.attr('stroke', 'blue')
.attr('stroke-width',
.attr('fill', 'none');
Unfortunately this doesn't work, and I'm not sure why. The initial chartContainer.append('path') seem to be working (sometimes), and the chartContainer.select('path') doesn't actually return the correct item (othertimes). Might d3 be traversing my axis groups and returning one of their paths? Am I going about this the wrong way?
You don't show us your complete code, but from your description it sounds as if you may be selecting the wrong path. One way around this is to assign a special class to the path you want and select accordingly.
chartContainer.append("path")
.attr("class", "chart");
chartContainer.select("path.chart");
The other problem is that you can't really use .data() to update the data bound to the DOM element like you're using it. Use .datum() instead.
chartContainer.select('path')
.datum(activeData);

Resources