Resetting D3s transform attributes - javascript

After creating some simulations I was going to remove the transforms on reset (where i can change the simulation). That way the viewport is not looking in a stale location.
I was manually resetting it and it looked good but when i carried out either a drag or a zoom, it would jump back to its old location. I noticed the issue was related to the d3.event.transform properties being maintained. I was thinking that the best course of action would be to reset the properties back to x:0, y:0, scale: 1, but i didnt see much with regard to the transform attribute.
How do i reset this?
The closest i have been able to do would be akin to:
const mySvg; // Native element
const g = d3.select(mySvg).select(":first-child"); // pointing to g, where the transform attribute is applied.
g.attr("transform", "")
which reset it, just for a moment until a tick, or drag or zoom occured which it would snap back to d3.event.transform. How is this resettable?
// This is my zoomable behavior i inject in TS
applyZoomableBehavior(svgElement, containerElement) {
const svg = d3.select(svgElement),
container = d3.select(containerElement),
zoomed = () => {
const trans = d3.event.transform;
container.attr('transform', `translate(${trans.x}, ${trans.y}) scale(${trans.k})`);
},
zoom = d3.zoom().on('zoom', zoomed);
svg.call(zoom);
}
Given that it looks like this is related to event handling, by the source: https://github.com/d3/d3-selection/blob/v1.4.1/README.md#handling-events
I am under the impression that i could submit an event for transform, but I dont really see anything giving the right information.
My goal is that I am going to create a resetViewport function which does just this.
HTML:
<svg #svg>
<g>
<g *ngFor="let node of nodes"></g>
<g *ngFor="let link of links"></g>
</g>
</svg
Code:
resetViewport(svgElement){
const g = d3.select(svgElement).select(":first-child");
// `g` points to the outter g element, the child of svg.
}

I believe the answer to the problem would be involving d3.zoomIdentity
If i want to create this reset function, it would be defined simply as:
resetViewport(svgElement){
const svg = d3.select(svgElement);// svgElement is a nativeElement.
// Need to update the current viewport.
const g = svg.select(":first-child")
g.attr('transform', 'translate(0,0) scale(1.0)');
// The below resets the scale and positioning of the viewport for subsequent move / zoom calls.
const zoom = d3.zoom();
svg.call(zoom.transform, d3.zoomIdentity);
}
So when the reset is called it uses the identity matrix to set the location.

Related

Setting transform translate of <g> using Javascript

What I set for transforming <g> is
const svg = d3.select('target-svg')
const g = d3.select('target-g')
svg.call(d3.zoom().on('zoom', () => {
g.attr('transform', d3.event.transform)
})
Now, I am trying to set transform attribute of <g> svg tag using following sample javascript code.
g.attr('transform', 'translate(100, 100) scale(1)')
The issue is that the <g> tag is transleted fine but when zoom in and out using mouse wheel, the <g> is translated to origin position.
For example, if the origin coordinate of translate of <g> is (0, 0) and I translate it to (100, 100), the <g>'s (x, y) is located (100, 100). But when I zoom it, the <g> is go back to (0, 0).
How can I stop it?
A D3 zoom behavior does not track an element's transform attribute - afterall, a zoom behavior might not alter it (zooming on canvas, changing color or shape on zoom, granularity of data, applying a zoom to different elements rather than the one triggering events, etc).
When you apply a transform with d3.event.transform you are applying the current zoom transform, which starts with a translate of [0,0] and a scale of 1. In other words, unless you specify otherwise, the default initial zoom event with a d3 zoom behavior will always be relative to this initial state. This is why the zoom is not relative to the manually set element's transform value.
To unify a manually set transform attribute and the zoom behavior's transform, use the zoom to manage both. You can manually set the transform attribute of the g with the zoom behavior:
var transform = d3.zoomIdentity.translate([100,100]).scale(1);
svg.call(zoom.transform, transform);
The first line manipulates a d3 zoom identity to create a new transform. The second line applies it, calling a zoom event in the process, which triggers a zoom.on event.
With this approach the zoom manages both the use of manual transform and event based transforms, so that the zoom is never out of date or "unaware" of what transform is currently applied to a selection.
Altogether, this might look like:
var svg = d3.select('body')
.append('svg')
.attr('width',500)
.attr('height',400)
var g = svg.append('g');
var rect = g.append('rect')
.attr('x', 750)
.attr('y', 100)
.attr('height',50)
.attr('width',50);
var zoom = d3.zoom()
.on('zoom', function() {
g.attr('transform',d3.event.transform);
})
var initialTransform = d3.zoomIdentity
.translate(-700,-40)
svg.call(zoom);
svg.call(zoom.transform,initialTransform);
<script src="https://d3js.org/d3.v5.min.js"></script>

zooming an image results in issues with pageX/pageY to SVG conversions

I am trying to move a SVG circle that sits in the center of an HTML image. If you mouse down on the image and drag the circle it works great.
However, if I zoom the image (tap on + button in the codepen), pageX and pageY to SVG co-ordinate translation messes up.
How should I be handling this correctly ? (My complete code handles both touch and mouse events for SVG - I've simplified it to just mouse for this example)
My codepen: http://codepen.io/pliablepixels/pen/EZxyRN
Here is how I am getting co-ordinates (please see codepen for a runnable example):
// map to SVG view so I can move the circle
function recompute(ax,ay)
{
// alert ("Here");
var svg=document.getElementById('zsvg');
var pt = svg.createSVGPoint();
pt.x = ax;
pt.y = ay;
var svgP = pt.matrixTransform(svg.getScreenCTM().inverse());
$scope.cx = Math.round(svgP.x);
$scope.cy = Math.round(svgP.y);
}
function moveit(event)
{
if (!isDrag) return;
var status = "Dragging with X="+event.pageX+" Y="+event.pageY;
$timeout (function(){$scope.status = status; recompute(event.pageX, event.pageY)});
}
The relevant SVG:
<svg id = "zsvg" class="zonelayer" viewBox="0 0 400 200" width="400" height="300" >
<circle id="c1" ng-attr-cx="{{cx}}" ng-attr-cy="{{cy}}" r="20" stroke="blue" fill="purple" />
</svg>
Firstly, you would normally use clientX and clientY rather than pageX and pageY.
Secondly, the Ionic(?) zoomTo() function you are using is applying a 3D transform to the container div. Ie.
style="transform: translate3d(-791.5px, -173px, 0px) scale(2);"
I don't expect getScreenCTM() handles 3D transforms. Even ones that are effectively 2D because they do nothing in the Z axis.
You'll need to either:
do the zoom a different way. IOW handle it yourself so you can do it in a getScreenCTM()-friendly way. Or multiply the zoom factor in directly. or
find a way of getting the details of the transform that Ionic has applied and updating your transformed mouse coords appropriately.
Update:
The definition of getScreenCTM() has changed in SVG 2. In SVG 1.1 it was fairly loosely defined. In SVG 2 the definition has been updated to explicitly define how the result is calculated. Although it does not specify how 3D transforms on ancestor elements should be handled.
I have experimented a little on Chrome and Firefox. It appears that Chrome has implemented the SVG2 definition, but it has a bug. It is not returning the correct transform matrix. However Firefox has not yet been updated. It is not including any transforms on ancestor elements.
I now believe the bug in Chrome is the reason your sample is not working there. However if you want to be cross-browser, my advice to handle zoom yourself - and adjust the transform (or coords) - still holds.

Mouse click transformation to svg after pan and zoom

Im using snap.svg an snap.svg.zpd libraries. Same issue I have if I use snap.svg and jQuery panzoom library combination.
Code sample you can find here.
var mySvg = $("#plan")[0];
var snap = Snap("#plan");
//create an image
var imagePlan = snap.image("http://upload.wikimedia.org/wikipedia/commons/4/42/Cathedral_schematic_plan_fr_vectorial.svg", 10, 10, 900, 500);
var group = snap.group(imagePlan);
snap.zpd();
var pt = mySvg.createSVGPoint(); // create the point;
imagePlan.click(function(evt)
{
console.log(evt);
pt.x = evt.x;
pt.y = evt.y;
console.log(mySvg.getScreenCTM().inverse());
//When click, create a rect
var transformed = pt.matrixTransform(mySvg.getScreenCTM().inverse());
var rect1 = snap.rect(transformed.x, transformed.y, 40, 40);
group.add(rect1);
});
Problem is...if you click on initial svg it will add rectangle to the mouse position. If you pan/zoom image and then add rectangle it will be shiffted.
It looks like problem is in method mySvg.getScreenCTM().inverse(). Matrix returned is always same one, panning and zooming does not change it. It always use matrix from initialy rendered svg. However, if I inspect svg element, I can see that pann/zoom change transform matrix directly on element (image below).
Does anybody know how to fix this. My requirement is to be able to drag and drop elements outside svg into svg on any zoom scale or pan context, so I need transformation from mouse click point to svg offset coordinates. If you know any other approach or any other library combination that could done this, it would be ok for me.
Thanks in advance.
Problem is, the transform isn't in mySvg. Its on the 'g' group element thats inside the svg. Zpd will create a group to operate on as far as I know, so you want to look at that.
To hightlight this, take a look at
console.log(mySvg.firstElementChild.getScreenCTM().inverse());
In this case its the g element (there's more direct ways of accessing it, depending on whether you want to just work in js, or snap, or svg.js).
jsfiddle
Its not quite clear from your description where you want the rect (within the svg, separate or whatt) to go and at what scale etc though, and if you want it to be part of the zoom/panning, or static or whatever. So I'm not sure whether you need this or not.
I'm guessing you want something like this
var tpt = pt.matrixTransform( mySvg.firstElementChild.getScreenCTM().inverse() )
var rect1 = snap.rect(tpt.x, tpt.y, 40, 40);

How to draw non-scalable text in SVG with Javascript?

I'm using d3 library to create a svg graphic. The problem I have is when I resize the window. The whole graphic resizes meaning that texts (legend and axis) resize as well, to the point where it's unreadable. I need it to keep the same size when resizing.
I've been searching online and I found this solution:
var resizeTracker;
// Counteracts all transforms applied above an element.
// Apply a translation to the element to have it remain at a local position
var unscale = function (el) {
var svg = el.ownerSVGElement;
var xf = el.scaleIndependentXForm;
if (!xf) {
// Keep a single transform matrix in the stack for fighting transformations
xf = el.scaleIndependentXForm = svg.createSVGTransform();
// Be sure to apply this transform after existing transforms (translate)
el.transform.baseVal.appendItem(xf);
}
var m = svg.getTransformToElement(el.parentNode);
m.e = m.f = 0; // Ignore (preserve) any translations done up to this point
xf.setMatrix(m);
};
[].forEach.call($("text"), unscale);
$(window).resize(function () {
if (resizeTracker) clearTimeout(resizeTracker);
resizeTracker = setTimeout(function () { [].forEach.call($("text"), unscale); }, 0);
});
And added it to my code, but it's not working. I debugged it and at this part of the code:
var xf = el.scaleIndependentXForm;
It always returns the same matrix: 1 0 0 1 0 0 and the text keeps resizing as does the rest of the svg elements instead of keeping static.
Could anyone help me, please?
Thanks in advance.
The same thing was happening to me with an SVG generated by SnapSVG until I noted that the example page on which this does work wraps its 'main' SVG tag in another SVG tag before using el.ownerSVGElement.ownerSVGElement rather than el.ownerSVGElement.
Wrapping my SVG in an 'empty' wrapper SVG (note style overflow:visible;) I had much better results!
Edit: oh, wait. Internet Explorer still isn't happy. Seems the author of the solution is aware...

Infinite zooming in force layout d3.js

There is an example where we can click on a circle and see inner circles.
Also there are different examples of the force layout.
Is it possible to have a force layout and each node of it will/can be a circle with inner force layout?
So it will work as infinite zoom (with additional data loading) for these circles.
Any ideas/examples are welcome.
I would approach the problem like this: Build a force-directed layout, starting with one of the tutorials (maybe this one since it build something that uses circle packing for initialization). Add D3's zoom behavior.
var force = d3.layout.force()
// force layout settings
var zoom = d3.behavior.zoom()
// etc.
So far, so good. Except that the force layout likes to hang out around [width/2, height/2], but it makes the zooming easier if you center around [0, 0]. Fight with geometric zooming for a little while until you realize that this problem really demands semantic zooming. Implement semantic zooming. Go get a coffee.
Figure out a relationship between the size of your circles and the zoom level that will let you tell when the next level needs to be uncovered. Something like this:
// expand when this percent of the screen is covered
var coverageThreshold = 0.6;
// the circles should be scaled to this size
var maxRadius = 20;
// the size of the visualization
var width = 960;
// which means this is the magic scale factor
var scaleThreshold = maxRadius / (coverageThreshold * width)
// note: the above is probably wrong
Now, implement a spatial data filter. As you're zooming down, you basically want to hide any data points that have zoomed out of view so that you don't waste gpu time computing their representation. Also, figure out an algorithm that will determine which node the user is zooming in on. This very well might use a Voronoi tessalation. Learn way more than you thought you needed to about geometry.
One more math thing we need to work out. We'll have the child nodes take the place of the parent node, so we need to scale their size based on the total size of the parent. This is going to be annoying and require some tweaking to get right, unless you know the right algorithm... I don't.
// size of the parent node
var parentRadius = someNumberPossiblyCalculated;
// area of the parent node
var parentArea = 2 * Math.PI * parentRadius;
// percent of the the parent's area that will be covered by children
// (here be dragons)
var childrenCoverageRatio = 0.8;
// total area covered by children
var childrenArea = parentArea * childrenCoverageArea;
// the total of the radiuses of the children
var childTotal = parent.children
.map(radiusFn)
.reduce(function(a, b) { return a + b; });
// the child radius function
// use this to generate the child elements with d3
// (optimize that divide in production!)
var childRadius = function(d) {
return maxRadius * radiusFn(d) / childTotal;
};
// note: the above is probably wrong
Ok, now we have the pieces in place to make the magic sauce. In the zoom handler, check d3.event.scale against your reference point. If the user has zoomed in past it, perform the following steps very quickly:
hide the parent elements that are off-screen
remove the parent node that is being zoomed into
add the child nodes of that parent to the layout at the x and y location of the parent
explicitly run force.tick() a handful of times so the children move apart a bit
potentially use the circle-packing layout to make this process cleaner
Ok, so now we have a nice little force layout with zoom. As you zoom in you'll hit some threshold, hopefully computed automatically by the visualization code. When you do, the node you're zooming in on "explodes" into all it's constituent nodes.
Now figure out how to structure your code so that you can "reset" things, to allow you to continue zooming in and have it happen again. That could be recursive, but it might be cleaner to just shrink the scales by a few orders of magnitude and simultaneously expand the SVG elements by the inverse factor.
Now zooming out. First of all, you'll want a distinct zoom threshold for the reverse process, a hysteresis effect in the control which will help prevent a jumpy visualization if someone's riding the mousewheel. You zoom in and it expands, then you have to zoom back just a little bit further before it contracts again.
Okay, when you hit the zoom out threshold you just drop the child elements and add the parent back at the centroid of the children's locations.
var parent.x = d3.mean(parent.children, function(d) { return d.x; });
var parent.y = d3.mean(parent.children, function(d) { return d.y; });
Also, as you're zooming out start showing those nodes that you hid while zooming in.
As #Lars mentioned, this would probably take a little while.

Categories