How to Apply D3.Js Svg Clipping After Zooming

How to apply d3.js svg clipping after zooming

I had the same problem and spent the last couple of hours trying to figure out a solution. Apparently, the clip-path operates on the object prior to transformation. So I tried to reverse-transform the clip object when performing the zoom transformation, and this worked !

It is something in the spirit of:

var clip_orig_x = 100, clip_orig_y = 100;
function zoomed() {
var t = d3.event.translate;
var s = d3.event.scale;

// standard zoom transformation:
container.attr("transform", "translate(" + t +")scale(" + s + ")");

// the trick: reverse transform the clip object!
clip.attr("transform", "scale(" + 1/s + ")")
.attr("x", clip_orig_x - t[0])
.attr("y", clip_orig_y - t[1]);
}

where clip is the rectangle in the clipPath. Because of interactions between zooming and translation, you need to set "x" and "y" explicitly instead of using transform.

I am sure experienced d3 programmers out there will come up with a better solution, but this works !

d3 v4: Clip path not updating after pan and zoom

Apparently I hadn't added enough groups. The easy solution was to add another append("g") to the svgGroup I was creating, for a last line looking like this:

var svgGroup = treeContainer.append("g")
.attr("clip-path","url(#clip)")
.append("g");

D3 clipping issues on zoom

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();

D3: keeping position of SVG's relative while panning and zooming

Here is a modified version of your codepen which fixes the movements of the marker during drag events while keeping the marker outside the floorplan svg container:

https://codepen.io/xavierguihot/pen/OvyRPY?editors=0010


To bring back into context, an easy solution would have been to include the marker element inside the floorplan container (in order for the marker to get the same zoom events as the floorplan), but here we want the marker to be in its own svg container.

And it is not trivial!


Appart from including ids in html tags (in order to select these elements from the html), only the javascript part has ben modified.

Let's dig a little bit on the steps necessary to get to this point:

First: Let's modify the zoomed function to apply to the marker as well:

Initially this was the zoom function:

function zoomed() {
svg.attr("transform", d3.event.transform);
}

And the modified version:

function zoomed() {

// Before zooming the floor, let's find the previous scale of the floor:
var curFloor = document.getElementById('floorplan');
var curFloorScale = 1;
if (curFloor.getAttribute("transform")) {
var curFloorTransf = getTransformation(curFloor.getAttribute("transform"));
curFloorScale = curFloorTransf.scaleX;
}

// Let's apply the zoom
svg.attr("transform", d3.event.transform);

// And let's now find the new scale of the floor:
var newFloorTransf = getTransformation(curFloor.getAttribute("transform"));
var newFloorScale = newFloorTransf.scaleX;

// This way we get the diff of scale applied to the floor, which we'll apply to the marker:
var dscale = newFloorScale - curFloorScale;

// Then let's find the current x, y coordinates of the marker:
var marker = document.getElementById('Layer_1');
var currentTransf = getTransformation(marker.getAttribute("transform"));
var currentx = currentTransf.translateX;
var currenty = currentTransf.translateY;

// And the position of the mouse:
var center = d3.mouse(marker);

// In order to find out the distance between the mouse and the marker:
// (43 is based on the size of the marker)
var dx = currentx - center[0] + 43;
var dy = currenty - center[1];

// Which allows us to find out the exact place of the new x, y coordinates of the marker after the zoom:
// 38.5 and 39.8 comes from the ratio between the size of the floor container and the marker container.
// "/2" comes (I think) from the fact that the floor container is initially translated at the center of the screen:
var newx = currentx + dx * dscale / (38.5/2);
var newy = currenty + dy * dscale / (39.8/2);

// And we can finally apply the translation/scale of the marker!:
d3.selectAll(".marker").attr("transform", "translate(" + newx + "," + newy + ") scale(" + d3.event.transform.k + ")");
}

This heavily uses the getTransformation function which allows to retrieve the current transform details of an element.

Then: But now, after having zoomed, when we drag the marker, it takes back its original size:

This means we have to tweak the marker's dragg function to keep its current scale when applying the drag transform:

Here was the initial drag function:

function dragged(d) {
var x = d3.event.x;
var y = d3.event.y;
d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
}

And its modified version:

function draggedMarker(d) {

var x = d3.event.x;
var y = d3.event.y;

// As we want to keep the same current scale of the marker during the transform, let's find out the current scale of the marker:
var marker = document.getElementById('Layer_1');
var curScale = 1;
if (marker.getAttribute("transform")) {
curScale = getTransformation(marker.getAttribute("transform")).scaleX;
}

// We can thus apply the translate And keep the current scale:
d3.select(this).attr("transform", "translate(" + x + "," + y + "), scale(" + curScale + ")");
}

Finally: When dragging the floor we also have to drag the marker accordingly:

We thus have to override the default dragging of the floor in order to include the same dragg event to the marker.

Here is the drag function applied to the floor:

function draggedFloor(d) {

// Overriding the floor drag to do the exact same thing as the default drag behaviour^^:

var dx = d3.event.dx;
var dy = d3.event.dy;

var curFloor = document.getElementById('svg-floor');
var curScale = 1;
var curx = 0;
var cury = 0;
if (curFloor.getAttribute("transform")) {
curScale = getTransformation(curFloor.getAttribute("transform")).scaleX;
curx = getTransformation(curFloor.getAttribute("transform")).translateX;
cury = getTransformation(curFloor.getAttribute("transform")).translateY;
}

d3.select(this).attr("transform", "translate(" + (curx + dx) + "," + (cury + dy) + ")");

// We had to override the floor drag in order to include in the same method the drag of the marker:

var marker = document.getElementById('Layer_1');
var currentTransf = getTransformation(marker.getAttribute("transform"));

var currentx = currentTransf.translateX;
var currenty = currentTransf.translateY;
var currentScale = currentTransf.scaleX;

d3.selectAll(".marker").attr("transform", "translate(" + (currentx + dx) + "," + (currenty + dy) + ") scale(" + currentScale + ")");
}

D3.js-how to clip area outside rectangle while zooming in a grouped bar chart

Here's an updated jsfiddle

It comes down to a few modifications. First, you need to know about SVG clip-path which is a way to define an svg shape that masks or crops another shape (your chart, in this case). As demonstrated in the linked documentation, a clip-path requires a <defs> element with a <clipPath> inside it. In your case, that <clipPath> needs to contain a <rect> whose bounds are set to cover the visible area.

To create that <defs>, I added:

var mask = svg.append("defs")
.append("clipPath")
.attr("id", "mask")
.style("pointer-events", "none")
.append("rect")
.attr({
x: 0,
y: 0,
width: width,
height: height + margin.bottom,
})

Ultimately, to use the above mask to crop the chart, it required calling

.attr("clip-path", "url(#mask)")

on the thing that is being masked.

The way you had things set up, there was no single SVG <g> that contained both of the things that needed to be mask (i.e. the chart and the x-axis). So I added that <g>, and reshuffled things so that the axis and chart (allStates) are added to it:

var masked = svg.append("g")
.attr("clip-path", "url(#mask)")

masked.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);

var allStates = masked
.append("g")
.attr("class", "allStates");

And that's all.

D3: Zooming/Panning Line Graph in SVG is not working in Canvas

There is one major source of grief that causes your line to disappear, and it is only triggered on the zoom:

function zoomed() {
t = d3.event.transform;
x.domain(t.rescaleX(x2).domain()); // here
...
}

Rescaling won't work on x2 as you haven't defined its domain. x2 is your reference scale that is used to set x on each zoom, it should be the same as x to start with. However, the default domain for a d3.timeScale() is from January 1, 2000 to January 2, 2000 (see API docs), which won't work out for your data as your data does not overlap this period.

You need to set the domain of x2 as well as x. If you do so after you set the initial domain for x with: x2.domain(x.domain()), you should get a chart that updates (jsbin) as you now have a domain that overlaps your data.

However, now, the issue is that you need to clip your line, which you do in your svg example but not the canvas. To do so you could use something like:

function draw() {
xAxis();
yAxis();

// save context without clip apth
context.save();

// create a clip path:
context.beginPath()
context.rect(0, 0, width, height);
context.clip();

// draw line in clip path
context.beginPath()
line(data);

context.lineWidth = 1.5;
context.strokeStyle = "steelblue";
context.stroke();

// restore context without clip path
context.restore();
}

See this jsbin

And as we shouldn't let the axes overwrite themselves: Here's a jsbin that erases the previous axes (with a commented out code block that redefines the y domain based on what values are contained in the selected x domain).

For good measure, here's a snippet of the last jsbin (scaled down for snippet view):

var data = getData().map(function (d) {        return d;    });
var canvas = document.querySelector("canvas"), context = canvas.getContext("2d");
var margin = { top: 20, right: 20, bottom: 30, left: 50 }, width = canvas.width - margin.left - margin.right, height = canvas.height - margin.top - margin.bottom;
var parseTime = d3.timeParse("%d-%b-%y");
// setup scales var x = d3.scaleTime() .range([0, width]); var x2 = d3.scaleTime().range([0, width]); var y = d3.scaleLinear() .range([height, 0]);
// setup domain x.domain(d3.extent(data, function (d) { return moment(d.Ind, 'YYYYMM'); })); y.domain(d3.extent(data, function (d) { return d.KSum; })); x2.domain(x.domain());

// get day range var dayDiff = daydiff(x.domain()[0],x.domain()[1]);
// line generator var line = d3.line() .x(function (d) { return x(moment(d.Ind, 'YYYYMM')); }) .y(function (d) { return y(d.KSum); }) .curve(d3.curveMonotoneX) .context(context);
// zoom var zoom = d3.zoom() .scaleExtent([1, dayDiff]) .translateExtent([[0, 0], [width, height]]) .extent([[0, 0], [width, height]]) .on("zoom", zoomed); d3.select("canvas").call(zoom)
context.translate(margin.left, margin.top);
draw();//
function draw() { // remove everything: context.clearRect(-margin.left, -margin.top, canvas.width, canvas.height); /* // Calculate the y axis domain across the selected x domain: newYDomain = d3.extent(data, function(d) { if ( (x(moment(d.Ind, 'YYYYMM')) > 0) && (x(moment(d.Ind, 'YYYYMM')) < width) ) { return d.KSum; } }); // Don't update the y axis if there are no points to set a new domain, just keep the old domain. if ((newYDomain[0] !== undefined) && (newYDomain[0] != newYDomain[1])) { y.domain(newYDomain); } //*/
// draw axes: xAxis(); yAxis(); // save context without clip apth context.save(); // create a clip path: context.beginPath() context.rect(0, 0, width, height); context.clip();
// draw line in clip path context.beginPath() line(data); context.lineWidth = 1.5; context.strokeStyle = "steelblue"; context.stroke(); // restore context without clip path context.restore();
}
function zoomed() { t = d3.event.transform; x.domain(t.rescaleX(x2).domain()); draw(); }
function xAxis() { var tickCount = 10, tickSize = 6, ticks = x.ticks(tickCount), tickFormat = x.tickFormat();
context.beginPath(); ticks.forEach(function (d) { context.moveTo(x(d), height); context.lineTo(x(d), height + tickSize); }); context.strokeStyle = "black"; context.stroke();
context.textAlign = "center"; context.textBaseline = "top"; ticks.forEach(function (d) { context.fillText(tickFormat(d), x(d), height + tickSize); }); }
function yAxis() { var tickCount = 10, tickSize = 6, tickPadding = 3, ticks = y.ticks(tickCount), tickFormat = y.tickFormat(tickCount);
context.beginPath(); ticks.forEach(function (d) { context.moveTo(0, y(d)); context.lineTo(-6, y(d)); }); context.strokeStyle = "black"; context.stroke();
context.beginPath(); context.moveTo(-tickSize, 0); context.lineTo(0.5, 0); context.lineTo(0.5, height); context.lineTo(-tickSize, height); context.strokeStyle = "black"; context.stroke();
context.textAlign = "right"; context.textBaseline = "middle"; ticks.forEach(function (d) { context.fillText(tickFormat(d), -tickSize - tickPadding, y(d)); });
context.save(); context.rotate(-Math.PI / 2); context.textAlign = "right"; context.textBaseline = "top"; context.font = "bold 10px sans-serif"; context.fillText("Price (US$)", -10, 10); context.restore(); }
function getDate(d) { return new Date(d.Ind); }
function daydiff(first, second) { return Math.round((second - first) / (1000 * 60 * 60 * 24)); }
function getData() { return [ { "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542", "Ind": 201501, "TMin": 30.43, "TMax": 77.4, "KMin": 0.041, "KMax": 1.364, "KSum": 625.08 }, { "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542", "Ind": 201502, "TMin": 35.3, "TMax": 81.34, "KMin": 0.036, "KMax": 1.401, "KSum": 542.57 }, { "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542", "Ind": 201503, "TMin": 32.58, "TMax": 81.32, "KMin": 0.036, "KMax": 1.325, "KSum": 577.83 }, { "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542", "Ind": 201504, "TMin": 54.54, "TMax": 86.55, "KMin": 0.036, "KMax": 1.587, "KSum": 814.62 }, { "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542", "Ind": 201505, "TMin": 61.35, "TMax": 88.61, "KMin": 0.036, "KMax": 1.988, "KSum": 2429.56 }, { "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542", "Ind": 201506, "TMin": 69.5, "TMax": 92.42, "KMin": 0.037, "KMax": 1.995, "KSum": 2484.93 }, { "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542", "Ind": 201507, "TMin": 71.95, "TMax": 98.62, "KMin": 0.037, "KMax": 1.864, "KSum": 2062.05 }, { "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542", "Ind": 201508, "TMin": 76.13, "TMax": 99.59, "KMin": 0.045, "KMax": 1.977, "KSum": 900.05 }, { "BriteID": "BI-43dd32fe-ecbc-48d4-a8dc-e1f66110a542", "Ind": 201509, "TMin": 70, "TMax": 91.8, "KMin": 0.034, "KMax": 1.458, "KSum": 401.39 }]; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.18.1/moment.js"></script><canvas width="500" height="200"></canvas>

D3.js zoom with nested svg breaks viewport in Internet Explorer

I'm sorry this question didn't get enough attention when you first posted it: it's actually a simple fix. Just set the overflow property on the outer SVG to hidden.

So why does your code work as you intend on the other browsers?

It's because they set this property by default. The initial value for overflow in CSS is visible, but the SVG specs require that any element that can take a viewBox attribute has overflow:hidden in the browser's default stylesheet, except for the root SVG element. The other browsers interpret this exception as if it only applied to an <svg> element that is the root of an .svg document. Internet Explorer also applies treats the top-level inline SVG in an HTML document as if was a root (and therefore had overflow: visible).

The following snippet demonstrates the different behaviors. It uses a circle inside a nested SVG inside an inline SVG. The circle is too big for the nested SVG, so if overflow is hidden on the nested SVG (as it is by default in all browsers) the circle will be cropped to a square. The nested SVG is offset, partly outside the outer SVG. If overflow is hidden on the outer SVG, the nested SVG will be cropped to a rectangle; if overflow is visible you'll see the square sticking outside of the frame.

The first SVG uses default overflow on the outer SVG (different for IE) while the others explicitly set overflow: hidden or overflow: visible.

svg {    border: solid gray;    height: 100px;    width: 100px;    margin: 50px;}circle {    fill: royalBlue;}
<svg>    <svg x="-50" y="-50" width="100" height="100" >        <circle r="100" cx="50" cy="50"/>    </svg></svg><svg style="overflow: hidden">    <svg x="-50" y="-50" width="100" height="100" >        <circle r="100" cx="50" cy="50"/>    </svg></svg><svg style="overflow: visible">    <svg x="-50" y="-50" width="100" height="100" >        <circle r="100" cx="50" cy="50"/>    </svg></svg>

Svg clip-path within rectangle does not work

You want something like this:

http://jsfiddle.net/dsummersl/EqLBJ/1/

Specifically:

  • use 'clip' instead of 'clip-rect'
  • put the content you wish to clip inside a 'g' element, and specify the 'clip-path' attribute and the transforms for the 'g' element.

How do I draw gridlines in d3.js with zoom and pan

For anyone looking, I have solved this problem. I have updated the javascript in the original post, and updated the jsfiddle. If you are copying this code to your local machine where you are using d3.js 7.4.4 or higher then you need to change the lines that say d3.event.transform.... to just e.transform.



Related Topics



Leave a reply



Submit