D3.js - create new node in visible area when zoomed - javascript

I am currently working on a force-directed d3 graph utilizing the zoom function.
The user has the option to create new nodes on the graph. This is utilized via a double click event on the graph to get the coordinates at which the new node is created.
It is possible, and during the user's daily work quite common, that the double clicks on the graph, then zooms to another area and/or changes the scaling, and then finished the creation of the node (setting attributes). In this case, the node is suppose to be created in the visible area of the graph.
For this scenario, I wrote a function that takes the to be created node and the svg element and mutated the node's coordinates to center it in the visible area, if necessary:
moveNodeIntoViewPort(node: NodeInterface, svgElement: any): NodeInterface {
let svg = svgElement._groups[0][0];
let minX: number = parseInt(svg.attributes.viewBox.nodeValue.split(" ")[0]);
let minY: number = parseInt(svg.attributes.viewBox.nodeValue.split(" ")[1]);
let width: number = parseInt(
svg.attributes.viewBox.nodeValue.split(" ")[1].split(" ")[0]
);
let height: number = parseInt(
svg.attributes.viewBox.nodeValue.split(" ")[1].split(" ")[1]
);
let zoomX: number = parseInt(svg.__zoom.x) * -1;
let zoomY: number = parseInt(svg.__zoom.y) * -1;
let zoomK: number = parseFloat(svg.__zoom.k.toFixed(2));
node.x =
node.x < minX + zoomX || node.x > minX + width + zoomX
? (minX + zoomX + width / 2) / zoomK
: node.x;
node.y =
node.y < minY + zoomY || node.y > minY + height + zoomY
? (minY + zoomY + height / 2) / zoomK
: node.y;
return node;
}
Although this code works as intended, I am not satisfied with the code itself and am wondering, if D3 offers any better solution out of the box. I could not find any information in the official D3 documentation about how to:
check, if a node is visible in the zoomed area
how to get the current center coordinates of a zoomed graph
Any input is greatly appreciated.

After some research (and with some help), we managed to realize the same result with following code using a DOMMatrix:
moveNodeIntoViewPort(node: NodeInterface, svgElement: any): NodeInterface {
let transformationMatrix = this.viewportRef._groups[0][0].getScreenCTM();
const nodePoint = new DOMPoint(node.x, node.y).matrixTransform(
transformationMatrix
);
let currentViewport = svgElement._groups[0][0].getBoundingClientRect();
let centerPoint = new DOMPoint(
currentViewport.left + currentViewport.width / 2,
currentViewport.top + currentViewport.height / 2
).matrixTransform(transformationMatrix.inverse());
if (
nodePoint.x < currentViewport.left ||
nodePoint.x > currentViewport.right ||
nodePoint.y < currentViewport.top ||
nodePoint.y > currentViewport.bottom
) {
node.x = centerPoint.x;
node.y = centerPoint.y;
}
return node;
}
viewportRef is the d3 selection of the <g class="viewport> child of the <svg> element.
Although not quite shorter than the original function, as it is now, we can prevent using string manipulation and may extract part of the function to be used in other processes.

Related

D3js v5 Zooming to Bounding box on geoMercator().fitSize()

I use this as reference: https://bl.ocks.org/iamkevinv/0a24e9126cd2fa6b283c6f2d774b69a2
Adjusted some syntax to fit for version 5
Scale works, Translate looks like it works too because if I change the value, it zooms on different place..
But the problem is it doesn't zoom on the correct place I clicked.
I think this doesn't get to the place correctly because I use d3.geoMercator().fitSize([width, height], geoJSONFeatures) instead:
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 = Math.max(1, Math.min(8, 0.9 / Math.max(dx / width, dy / height))),
translate = [width / 2 - scale * x, height / 2 - scale * y];
Already tried to change the values to fit mine but failed, I can't get it.
Here is my projection:
var width = 500;
var height = 600;
d3.json("/regions50mtopo.json")
.then((geoJSON) => {
var geoJSONFeatures = topojson.feature(geoJSON, geoJSON.objects["Regions.50m"]);
// My Projection
var projection = d3.geoMercator().fitSize([width, height], geoJSONFeatures);
...
Any help, guide or reference?
Note: I'm mapping different country and fitSize(...) solves the
problem easily to fit on my svg that's why I can't use the same as in
the reference link I provided.
Found an answer: https://bl.ocks.org/veltman/77679636739ea2fc6f0be1b4473cf03a
centered = centered !== d && d;
var paths = svg.selectAll("path")
.classed("active", d => d === centered);
// Starting translate/scale
var t0 = projection.translate(),
s0 = projection.scale();
// Re-fit to destination
projection.fitSize([960, 500], centered || states);
// Create interpolators
var interpolateTranslate = d3.interpolate(t0, projection.translate()),
interpolateScale = d3.interpolate(s0, projection.scale());
var interpolator = function(t) {
projection.scale(interpolateScale(t))
.translate(interpolateTranslate(t));
paths.attr("d", path);
};
d3.transition()
.duration(750)
.tween("projection", function() {
return interpolator;
});
Exactly what I'm looking for. It works now as expected.
But maybe somebody also have suggestions on how to optimise it, because as the author said too, it feels slow and "laggy" when zooming in/out.

Using After Effects Expression To Script A Simple Shadow Behavior

I'm working on an AE project where around 50 Emojis should have a drop shadow on the floor.To make things easier I tried to add an expression that auto shrinks and grows the shadows based on the distance of the emoji to the floor.
Here is what I've tried
Drop Shadow Approach
You can see that the shadow grows and shrinks but in the wrong direction. So when emoji comes closer to the floor it shrinks and when the distance is more it grows. I need the opposite of the current behavior.
How do I achieve that?
This is the expression I've used for the scale property of the shadow layer. Shadow layer is separate from the emoji layer. So I have a composition with only 2 layers.
var y = thisComp.layer("smile").position[1];
var dist = Math.sqrt( Math.pow((this.position[0]-this.position[0]), 2) + Math.pow((this.position[1]-y), 2) );
newValue = dist ;
xScale = newValue;
yScale = newValue;
[xScale,yScale]
Thanks for your time.
The basic concept here is mapping values from one range to another. You want to say that (for example) as the distance changes between 0 and 100, the scale should change proportionally between 1 and 0.
function map ( x, oldMin, oldMax, newMin, newMax ) {
return newMin + ( x - oldMin ) / ( oldMax - oldMin ) * ( newMax - newMin );
}
var minDistance = 0;
var maxDistance = 100;
var maxScale = 1;
var minScale = 0;
xScale = yScale = map( dist, minDistance, maxDistance, maxScale, minScale );

dynamically calculate div position in

In looking to build a table seating/planning app. currently got a round table that i can dynamically add chairs around using code i found on stackoverflow. function is calculating the x,y absolut position of each seat around the table.
https://www.dropbox.com/s/4kvb0vre0dwtc0i/table.mov?dl=0
the video shows what i got so far.
for anyone interested the code for that is
export const calcRoundDimensions = (totalChairs, chairSize) => {
var chairspos = [];
var squareSize = chairSize * 2;
for (var i = 0; i < totalChairs; i++) {
var top = String(squareSize/2 + (-squareSize/2-chairSize) * Math.cos((360 / totalChairs / 180) * (i + 0) * Math.PI)) + 'px';
var left = String(squareSize/2 + (squareSize/2+chairSize) * (true ? Math.sin((360 / totalChairs / 180) * (i + 0) * Math.PI) : -Math.sin((360 / totalChairs / 180) * (i + 0) * Math.PI))) + 'px';
chairspos.push({top:top, left:left});
}
return {chairpos:chairspos, size:squareSize+chairSize};
}
I'm looking to do the same with a square one (also a rectangular one, but first thing's first).
square table would be fixed width (150px). I'm looking too be able to add chairs clockwise and have them centred on the side. (will have a 12 or 16 chair limit but this shouldn't matter i guess).
rectangle table would have one chair at either end then be able to add 5 or six chair on each of the longer ends.
searched the site but wasn't able to find much direction.
any help would be appreciated!
Not super clear on what you need but the following will get you positions of elements compared to the absolute top and left of the page:
const box = element.getBoundingClientRect();
const elementPosition = {
x:box.x+window.scrollX,
y:box.y+window.scrollY
};

d3 Force: Calculating position of text on links where links between nodes are arcs

One way to apply links to text is to use the force.links() array for the text elements and centre the text on the midpoint of the link.
I have some nodes with bidirectional links, which I've rendered as paths that bend at their midpoint to ensure it's clear that there's two links between the two nodes.
For these bidirectional links, I want to move the text so that it sits correctly over the bending path.
To do this, I've attempted to calculate the intersection(s) of a circle centred on the centre point of the link and a line running perpendicular to the link that also passes through its centre. I think in principal this makes sense, and it seems to be half working, but I'm not sure how to define which coordinate returned through calculating the intersections to apply to which label, and how to stop them jumping between the curved links when I move the nodes around (see jsfiddle - https://jsfiddle.net/sL3au5fz/6/).
The function for calculating coordinates of text on arcing paths is as follows:
function calcLinkTextCoords(d,coord, calling) {
var x_coord, y_coord;
//find centre point of coords
var cp = [(d.target.x + d.source.x)/2, (d.target.y + d.source.y)/2];
// find perpendicular gradient of line running through coords
var pg = -1 / ((d.target.y - d.source.y)/(d.target.x - d.source.x));
// define radius of circle (distance from centre point text will appear)
var radius = Math.sqrt(Math.pow(d.target.x - d.source.x,2) + Math.pow(d.target.y - d.source.y,2)) / 5 ;
// find x coord where circle with radius 20 centred on d midpoint meets perpendicular line to d.
if (d.target.y < d.source.y) {
x_coord = cp[0] + (radius / Math.sqrt(1 + Math.pow(pg,2)));
} else {
x_coord = cp[0] - (radius / Math.sqrt(1 + Math.pow(pg,2)));
};
// find y coord where x coord is x_text and y coord falls on perpendicular line to d running through midpoint of d
var y_coord = pg * (x_coord - cp[0]) + cp[1];
return (coord == "x" ? x_coord : y_coord);
};
Any help either to fix the above or propose another way to achieve this would be appreciated.
Incidentally I've tried using textPath to line my text up with my links but I don't find that method to be performant when displaying upward of 30-40 nodes and links.
Update: Amended above function and now works as intended. Updated fiddle here:https://jsfiddle.net/o82c2s4x/6/
You can calculate the projection of the chord to x and y axis and add it to the source node coordinates:
function calcLinkTextCoords(d,coord) {
//find chord length
var dx = (d.target.x - d.source.x);
var dy = (d.target.y - d.source.y);
var chord = Math.sqrt(dx*dx + dy*dy);
//Saggita
// since radius is equal to chord
var sag = chord - Math.sqrt(chord*chord - Math.pow(chord/2,2));
//Find the angles
var t1 = Math.atan2(sag, chord/2);
var t2 = Math.atan2(dy,dx);
var teta = t1+t2;
var h = Math.sqrt(sag*sag + Math.pow(chord/2,2));
return ({x: d.source.x + h*Math.cos(teta),y: d.source.y + h*Math.sin(teta)});
};
Here is the updated JsFiddle

Adjusting scale of a group to ensure shapes inside are as big as possible in d3 js

I'm using d3 tree layout similar to this example: http://bl.ocks.org/mbostock/4339083
I implemented a search box that when typing, centers your screen on a virtual "average" position of all the appropriate nodes.
I want to adjust the scale, so that selected nodes will be
All Visible
As zoomed in as possible.
If the search match is exactly 1, simulate the clicking on the node, else center to this virtual position.
if (matches[0].length === 1) {
click(matches.datum(), 0, 0, false);
}
else {
var position = GetAveragePosition(matches);
centerToPosition(position.x, position.y, 1);
}
This is what the centerToPosition function looks like:
function centerToPosition(x0, y0, newScale) {
if (typeof newScale == "undefined") {
scale = zoomListener.scale();
}
else {
scale = newScale;
}
var x = y0 * -1; //not sure why this is.. but it is
var y = x0 * -1;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
So how can I calculate the new scale? I tried different variations by taking the extents of the data points
var xExtent = d3.extent(matches.data(), function (d) {
return d.x0;
});
var yExtent = d3.extent(matches.data(), function (d) {
return d.y0;
});
Also tried looking at the transform properties of the group before centering the screen.
var components = d3.transform(svgGroup.attr("transform"));
I'll try to add a js fiddle soon!
EDIT: Here it is: http://jsfiddle.net/7SJqC/
Interesting project.
The method of determining the appropriate scale to fit a collection of points is fairly straightforward, although it took me quite a while to figure out why it wasn't working for me -- I hadn't clued in to the fact that (since you were drawing the tree horizontally) "x" from the tree layout represented vertical position, and "y" represented horizontal position, so I was getting apparently arbitrary results.
With that cleared up, to figure out the zoom you simply need to find the height and width (in data-coordinates) of the area you want to display, and compare that with the height and width of the viewport (or whatever your original max and min dimensions are).
ScaleFactor = oldDomain / newDomain
Generally, you don't want to distort the image with different horizontal and vertical scales, so you figure out the scale factor separately for width and height and take the minimum (so the entire area will fit in the viewport).
You can use the d3 array functions to figure out the extent of positions in each direction, and then find the middle of the extent adding max and min and dividing by two.
var matches = d3.selectAll(".selected");
/*...*/
if ( matches.empty() ) {
centerToPosition(0, 0, 1); //reset
}
else if (matches.size() === 1) {
click(matches.datum(), 0, 0, false);
}
else {
var xExtent = d3.extent(matches.data(), function (d) {
return d.x0;
});
var yExtent = d3.extent(matches.data(), function (d) {
return d.y0;
});
//note: the "x" values are used to set VERTICAL position,
//while the "y" values are setting the HORIZONTAL position
var potentialXZoom = viewerHeight/(xExtent[1] - xExtent[0] + 20);
var potentialYZoom = viewerWidth/(yExtent[1] - yExtent[0] + 150);
//The "20" and "150" are for height and width of the labels
//You could (should) replace with calculated values
//or values stored in variables
centerToPosition( (xExtent[0] + xExtent[1])/2,
(yExtent[0] + yExtent[1])/2,
Math.min(potentialXZoom, potentialYZoom)
);
}
http://jsfiddle.net/7SJqC/2/

Categories

Resources