D3 scatter plot - visible points outside the chart after zoom - d3.js

I'm new to D3 and I've a question related to adding zoom into a chart.
I built a scatter plot with zoom/pan and everything is working except for the fact that when I use the zoom on the chart, I can see some points outside the chart "area" and I really want to avoid that.
The zoom behavior looks like this:
var zoom = d3.behavior.zoom()
.x(xScale)
.y(yScale)
.scaleExtent([1, 10])
.on("zoom", zoomed);
And the zoomed function like this:
function zoomed() {
var panX = d3.event.translate[0];
var panY = d3.event.translate[1];
var scale = d3.event.scale;
panX = panX > 10 ? 10 : panX;
var maxX = -(scale-1)*width-10;
panX = panX < maxX ? maxX : panX;
panY = panY > 10 ? 10 : panY;
var maxY = -(scale-1)*height-10;
panY = panY < maxY ? maxY : panY;
zoom.translate([panX, panY]);
main.select(".x.axis").call(xAxis);
main.select(".y.axis").call(yAxis);
main.selectAll("circle")
.attr("cx", function (d,i) { return xScale(d[0]); } )
.attr("cy", function (d) { return yScale(d[1]); } )
.attr("r", 5);
}
You can see a working example (and all the code) here
Is there a way I can define the "area" of scope for the zoom, so any point outside this area is not visible? or can I add something into the zoomed function to fix this?
Thank you very much.

One option would be to restrict the g region of the element in which your points are drawn using a clip path
See http://jsfiddle.net/henbox/duwyay3y/1/ for the code.
First define your clip path:
var clip = main.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("id", "clip-rect")
.attr("x", "0")
.attr("y", "0")
.attr('width', width)
.attr('height', height);
then add to the g element
main.append("g").attr("clip-path", "url(#clip)")
.selectAll("circle")
...

Related

D3js brush area on zoom not centered

I'm working on zooming and brushing on a barchart in D3js. I'm following this example: https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172.
I'm having issue's when zooming with the brush area. It's not centered correctly (most of the times). When it's not centering correct it's centered too much centered to the left (see screenshot).
This is the code for brushing and zooming
// Zoom
var zoom = d3.zoom()
.scaleExtent([.9, 20])
.translateExtent([[0, 0], [graph.width(), graph.height()]])
.on('zoom', zoomUpdateChart);
svg.call(zoom);
function zoomUpdateChart() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type == "brush") return;
// recover the new scale
var transform = d3.event.transform;
var zoomLevel = d3.event.transform.k;
graph._xScale.domain(transform.rescaleX(graph._xScaleZoom).domain());
resetZoomBtn.attr("opacity", 1)
.on("click", resetZoom);
graph.TooltipRemove(tooltip);
// regenerating the box axis
axis.remove();
axis = graph.RenderBoxAxis(g, graph._xScale);
bars.attr("x", function(d) { return graph._xScale(d.x); })
.attr("width", barWidth * zoomLevel );
zoomSection.select(".brush").call(brush.move, graph._xScale.range().map(transform.invertX, transform));
}
function resetZoom() {
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity)
.on("end", () => {
resetZoomBtn.attr("opacity", 0.2)
.on("click", null);
});
graph.TooltipRemove(tooltip);
}
// Brush
var brush = d3.brushX()
.extent([[0, 0], [graph._width, graph._heightZoom]])
.on("brush", brushed);
zoomSection.append("g")
.attr("class", "brush")
.call(brush)
.call(brush.move, graph._xScale.range());
function brushed() {
if (d3.event.sourceEvent && d3.event.sourceEvent.type == "zoom") return;
const selection = d3.event.selection || graph._xScaleZoom.range();
graph._xScale.domain(selection.map(graph._xScaleZoom.invert, graph._xScaleZoom));
const selectionWidth = selection[1] - selection[0];
const scale = (selectionWidth / graph._width);
const zoomLevel = 1 / scale;
resetZoomBtn.attr("opacity", 1)
.on("click", resetZoom);
graph.TooltipRemove(tooltip);
// regenerating the box axis
axis.remove();
axis = graph.RenderBoxAxis(g, graph._xScale);
bars.attr("x", function(d) { return graph._xScale(d.x) })
.attr("width", barWidth * zoomLevel)
svg.call(zoom.transform, d3.zoomIdentity
.scale(graph._width / selectionWidth)
.translate(-selection[0], 0));
}
I've done some research myself. The problem is within setting the range for the new brush position.
zoomSection.select(".brush").call(brush.move, graph._xScale.range().map(transform.invertX, transform));
It's set to [-166.07655066619142, 298.5037864592652]. The first index should be a positive number but is not calculated correctly. I've looked over it a couple of hours but haven't found the solution.
Some more details: The page loads more graphs of different types like areachart and line charts.
After much searching I found the anwser. I wasn't setting .extent() on zoom. The correct code is:
var zoom = d3.zoom()
.scaleExtent([0.9, 20])
.extent([[0, 0], [graph._width, graph._height]])
.translateExtent([[0, 0], [graph._width, graph._height]])
.on('zoom', zoomUpdateChart);

reversing order of d3.zoom scale and translate

If you click the red button in this example:
https://bl.ocks.org/interwebjill/fe782e6f195b17f6fe6798a24c390d90
you can see that the chart translates so that the circle is in the center and then zooms in to a specified level (reclicking on the button zooms back out). Translating and then zooming in this way leaves a gap on the left that I would rather not have. How might I change the code so that the chart zooms first and then translates to center so that I don't have this gap in the chart?
I have tried reversing the order of the scale and translate in both the zoom definition and the zoomToExtent function but there is no different in effect.
The ultimate source of the problem is d3.interpolateZoom. This interpolator has scale interpolate faster than translate - even though they mostly both are transitioning at the same time. The pattern implemented with d3.interpolateZoom is based on this paper.
Because scale and translate both interpolate differently in d3.interpolateZoom, you get a gap in the side of your chart as the scale decreases/increases more rapidly than the translate values.
d3.interpolateZoom is used when you call the zoom on a transition.
However, if you apply a transform directly on a transition using .attr(), the d3 transition will use d3.interpolateString, which will search the start and end strings for corresponding numbers and use d3.interpolateNumber on those. This will apply the same interpolation to both scale and translate.
Using both methods we can compare the discrepancy between d3.interpolateZoom and d3.interpolateString. Below the black rectangle uses d3.interpolateString while the orange rectangle uses d3.interpolateZoom. Click on a rectangle to start the transition:
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 300);
var g1 = svg.append("g"), g2 = svg.append("g");
var zoom1 = d3.zoom().on("zoom", function() {
g1.attr("transform", d3.event.transform);
});
var zoom2 = d3.zoom().on("zoom", function() {
g2.attr("transform", d3.event.transform);
});
g1.call(zoom1.transform, d3.zoomIdentity
.translate(150, 100)
.scale(2));
g2.call(zoom2.transform, d3.zoomIdentity
.translate(150,100)
.scale(2));
g1.append("rect")
.attr("x", 20)
.attr("y", 20)
.attr("width", 50)
.attr("height", 50);
g2.append("rect")
.attr("x", 22)
.attr("y", 22)
.attr("width", 46)
.attr("height",46)
.attr("fill","orange");
d3.selectAll("rect").on("click", function() {
g1.transition()
.duration(6000)
.attr("transform", d3.zoomIdentity)
.on("end", function() {
d3.select(this).call(zoom1.transform, d3.zoomIdentity);
})
g2.transition()
.duration(6000)
.call(zoom2.transform, d3.zoomIdentity)
});
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>
Where the first rectangle transitions the transform with .attr(), we need to call the zoom afterwards to ensure the zoom has the current transform, we don't need to in this example, but if you wanted to use the zoom after the transform you need to do this
Comparing these two we get:
(Y axis indicates percentage remaining in transition from start attribute to end attribute)
You want scale and translate to move simultaneously at the same rate when transitioning. We can do this if we use a tweening function. Unlike above we can't just use transition().attr("transform",newTransfrom) because you are also drawing canvas and updating the axis. So we'll need to create our own tweening function that can use the current transform and scale, apply it to the axis, canvas, and markers.
For example, rather than calling the zoom (which will use d3.interpolateZoom):
function zoomToExtent(d0, d1) {
zoomRect.call(zoom).transition()
.duration(1500)
.call(zoom.transform, d3.zoomIdentity
.translate(-xSVG(d0), 0)
.scale(width / (xSVG(d1) - xSVG(d0))));
}
Instead, we can use a tweening function which controls the element's transform and applies the same interpolator to scale and translate:
function zoomToExtent(d0, d1) {
//get transition start and end values:
var startScale = d3.zoomTransform(zoomRect.node()).k;
var startTranslate = d3.zoomTransform(zoomRect.node()).x;
var endTranslate = -xSVG(d0);
var endScale = width / (xSVG(d1) - xSVG(d0));
zoomRect.call(zoom).transition()
.duration(1500)
.tween("transform", function() {
var interpolateScale = d3.interpolateNumber(startScale,endScale);
var interpolateTranslate = d3.interpolateNumber(startTranslate,endTranslate);
return function(t) {
var t = d3.zoomIdentity.translate(interpolateTranslate(t),0).scale(interpolateScale(t));
zoomed(t);
}
})
.on("end", function() { // update the zoom identity on end:
d3.select(this).call(zoom.transform, d3.zoomIdentity
.translate(endTranslate, 0)
.scale(endScale));
})
}
You may notice I'm passing a transform value to the zoomed function, since there is no d3.event.transform for this, we need to modify the zoomed function to use the passed parameter if available, otherwise to fall back on the event transform:
function zoomed(transform) {
var t = transform || d3.event.transform;
...
Altogether, that might look something like this.
For another comparison between the two transitioning methods, I've created a gridded comparison that can be toggled between the two zoom identities:
var svg = d3.select("body").append("svg")
.attr("width", 510)
.attr("height", 310);
var g1 = svg.append("g");
var g2 = svg.append("g");
var rectangles1 = g1.selectAll()
.data(d3.range(750))
.enter()
.append("rect")
.attr("x", function(d) { return d%25*20; })
.attr("y", function(d) { return Math.floor(d/25)*20; })
.attr("width", 20)
.attr("height", 20)
.attr("fill","#ccc")
.attr("stroke","white")
.attr("stroke-width", 2);
var rectangles2 = g2.selectAll()
.data(d3.range(750))
.enter()
.append("rect")
.attr("x", function(d) { return d%25*20; })
.attr("y", function(d) { return Math.floor(d/25)*20; })
.attr("width", 20)
.attr("height", 20)
.attr("fill","none")
.attr("stroke","#444")
.attr("stroke-width", 1);
var startZoom = d3.zoomIdentity
.translate(-250,-200)
.scale(4);
var endZoom = d3.zoomIdentity
.translate(-100,-100)
.scale(5);
var zoom1 = d3.zoom().on("zoom", function() { g1.attr("transform", d3.event.transform); });
var zoom2 = d3.zoom().on("zoom", function() { g2.attr("transform", d3.event.transform); });
g1.call(zoom1.transform, startZoom);
g2.call(zoom2.transform, startZoom);
var toggle = true;
svg.on("click", function() {
toggle = !toggle;
g1.transition()
.duration(5000)
.call(zoom1.transform, toggle ? startZoom: endZoom)
g2.transition()
.duration(5000)
.attr("transform", toggle ? startZoom: endZoom)
.on("end", function() {
d3.select(this).call(zoom2.transform, toggle ? startZoom: endZoom);
})
})
rect {
opacity: 0.5;
}
<script type="text/javascript" src="https://d3js.org/d3.v5.js"></script>

D3.js: How to keep marker and text on same position when zooming in and out

I have a D3.js map based on this: https://bl.ocks.org/mbostock/2374239.
I have added a custom marker and a text on the feature that a user zooms in to a county. However, the marker and text do not stay on the same position as the county when I zoom in or out. My zoom function is as follows:
function zoomToCounty(stname, cntyname) {
d3.json("/topo/us-wgs84.json", function (us) {
var t = projection.translate(); // the projection's default translation
var s = projection.scale() // the projection's default scale
//initialize marker
d3.selectAll(".mark").remove();
d3.selectAll("text").remove();
//reset active to inactive size and color
d3.select("#svgMap2").select("g").select(".active")
.style("stroke-width", "0.5px")
.style("stroke", "#808080");
d3.selectAll(".county")
.classed("active", function (d) {
if (d.properties.StateName === stname && d.properties.County === cntyname) {
var zoom = d3.zoom()
.scaleExtent([1, 6])
.on("zoom", zoomed3);
svg.select("rect")
.call(zoom);
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = 0.9 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
//get centroid
var center = path.centroid(d);
//create marker
d3.select("#svgMap2").select("g")
.append("image")
.attr("xlink:href", "/images/marker2.png")
.attr("width", 14)
.attr("height", 20)
.attr("class", "mark")
.attr("transform", function (d) {
return "translate(" + center + ")";
});
//add text
d3.select("#svgMap2").select("g")
.append("text")
.style("fill", "#000")
.attr("x", x)
.attr("y", y)
.attr("dy", ".05em") //set offset y position
.attr("text-anchor", "middle") //set anchor y justification
.text(cntyname);
svg.transition()
.duration(750)
.call(zoom.transform, d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale));
return true;
}
})
}); //end d3.json
Working website can be found at: http://realtimeceap.brc.tamus.edu/
Thanks in advance.
01-28-2018 Status: I'm still unable to fix this one. I just need help on how to keep my image marker and text on the same coordinates as the selected feature when I zoom in/out using the mouse wheel. Initial zoom is at the middle of the svg with scale = 8. How do I make the marker "STICK" to specified coordinates once a user moves the wheel? HELP!
Solved this problem by:
1. I called the zoom function on the "g" element then
2. I created a function for when user moves the wheel; called this function from the "rect" element.
View the working codes at: http://realtimeceap.brc.tamus.edu

Fill Transition on a circle based on time

This is a link which shows filling up a circle with 2 colors.
http://jsfiddle.net/9ChXk/
This is the code:
var r = 100;
var svg = d3.select("body").append("svg");
var grad = svg.append("defs").append("linearGradient").attr("id", "grad")
.attr("x1", "0%").attr("x2", "0%").attr("y1", "100%").attr("y2", "0%");
grad.append("stop").attr("offset", "50%").style("stop-color", "lightblue");
grad.append("stop").attr("offset", "50%").style("stop-color", "white");
svg.append("circle")
.attr("cx", r)
.attr("cy", r)
.attr("r", r)
.attr("stroke", "blue")
.attr("fill", "url(#grad)");
I want the circle to keep changing its fill based on time...something like this link->
http://invision-web.net/ticket-status/
how can it be done in d3.js? Please help me out!
Changing the 'y1' attribute of grad does the trick. You can even add transitions for a smooth effect:
grad.transition().duration(1000).attr("y1","20%")
JsFiddle: http://jsfiddle.net/chrisJamesC/9ChXk/18/
EDIT: To have a chain of transitions:
var data = ["10%","40%","80%","20%","60%","90%","40%","50%","80%","20%"];
var data2 = [10,40,80,20,60,90,40,50,80,20]
var position = 0;
window.setInterval(function() {
grad.transition().duration(1000).attr("y1",((100-data2[position])*2).toString()+'%');
position = (position+1)%data2.length;
},1000)
JsFiddle: http://jsfiddle.net/chrisJamesC/9ChXk/20/

scaling geo coordinates in D3.js

I like to draw a circle in each coordinate. Using only width, height, and coordinates, how do I scale the coordinates? I'm very new to D3.js and I guess I'm doing something wrong with the projection part.
var width = 200,
height = 200;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var coordinates = [ [43.08803611,-79.06312222],
[43.09453889,-79.05636667] ];
var group = svg.append('g');
var projection = d3.geo.mercator()
.translate([width,height]);
var projectedCoordinates = [];
for (var i=0; i<coordinates.length; i++) {
projectedCoordinates[i] = projection(coordinates[i]);
}
group.selectAll("circle")
.data(projectedCoordinates)
.enter()
.append("circle")
.attr("r",4)
.attr("cx", function(d){ return d[0]; })
.attr("cy", function(d){ return d[1]; });
You are almost there, the only problem is that your projection is projecting the coordinates outside your drawing area. You can use the .center() function to tell the projection where the center is and .scale() to "zoom in". You should also only translate the projection by half the width and height of the container, otherwise the center will be in the bottom right corner.
The following example values should enable you to see the points:
var projection = d3.geo.mercator()
.center([43.09, -79.06])
.scale(50000)
.translate([width/2,height/2]);

Resources