Update path of existing SVG element - svg

I am trying to draw a border on svg element using code.
elem.attr({
'stroke-width' : 2,
'stroke' : '#3fa9f5'
});
elem is already drawn and I dont have control how its drawn . When I see its path ie elem.d , Z is not present at end. Because of that I am not able to draw border at one end.
elem.d="M 323.5 8 L 323.5 40 409.5 40 409.5 8"
Can I dynamically add Z to the above element? Adding Z to elem.d string is not working.
Code for adding Z to elem.d
if(elem.d !== undefined){
if(elem.d.indexOf("Z") === -1){
elem.d += " Z";
}
}

There's no elem.d property. The interface is SVGAnimatedPathData
So it's elem[0].pathSegList.appendItem to append the Z. And createSVGPathSegClosePath to create it which means putting it all together looks like this.
elem[0].pathSegList.appendItem(elem[0].createSVGPathSegClosePath());
The [0] unwraps the jquery wrapper on the element (if elem is a raw DOM element object then the [0] is not necessary)
If you want to test whether the last segment is a close path then you want something like this...
var pathSegList = elem[0].pathSegList;
if (pathSegList.numberOfItems > 0) {
var lastSeg = pathSegList.getItem(pathSegList.numberOfItems - 1);
if (lastSeg.pathSegType != lastSeg.PATHSEG_CLOSEPATH) {
// do whatever
}
}
Alternatively you could do with with attr (jquery) or get/setAttribute (DOM)
elem.attr(d) will get the attribute as a string as will elem[0].getAttribute("d") The SVG DOM above will give better perfomance though.

Related

Fabricjs selection or hover based on mouse location on object bounding rectangle for inner objects

I want to achieve selection based on the bounding rectangle but with a different approach.
Scenario: If I draw object inside object, like first text, then rectangle over it, then ellipse and then triangle. Now I should be able to select the text or rectangle or ellipse OR the reverse order anyhow.
As I start hovering the triangle's bounding rect, the selection or active object should be triangle, but as I move my mouse over ellipse's bounding rect, the current object should be shown as ellipse and so on, irrespective of the order I have added the objects on canvas.
I tried with perPixelTargetFind and following solution Fabricjs - selection only via border, both the solutions are not working meeting my requirement.
I am using FabricJS version 3.6.3
Thanks in advance.
First you need to set perPixelTargetFind: true and targetFindTolerance:5.
Now you will face the issue for selection.
Issue: If you mousedown and drag on empty space, the object was getting selected.
Solution: Found a way to do that. I debugged through the fabric's mechanism to get objects on current mouse pointer location. There is a function _collectObjects which checks for intersectsWithRect (intersect with boundingRect points of the current object), isContainedWithinRect (do the points come inside the boundingRect), containsPoint(current mouse pointer points come in the current object location). So you need to override the _collectObjects function and remove containsPoint check. That will work.
Overridden function:
_collectObjects: function(e) {
var group = [],
currentObject,
x1 = this._groupSelector.ex,
y1 = this._groupSelector.ey,
x2 = x1 + this._groupSelector.left,
y2 = y1 + this._groupSelector.top,
selectionX1Y1 = new fabric.Point(min(x1, x2), min(y1, y2)),
selectionX2Y2 = new fabric.Point(max(x1, x2), max(y1, y2)),
allowIntersect = !this.selectionFullyContained,
isClick = x1 === x2 && y1 === y2;
// we iterate reverse order to collect top first in case of click.
for (var i = this._objects.length; i--; ) {
currentObject = this._objects[i];
if (!currentObject || !currentObject.selectable || !currentObject.visible) {
continue;
}
if ((allowIntersect && currentObject.intersectsWithRect(selectionX1Y1, selectionX2Y2)) ||
currentObject.isContainedWithinRect(selectionX1Y1, selectionX2Y2))
) {
group.push(currentObject);
// only add one object if it's a click
if (isClick) {
break;
}
}
}
if (group.length > 1) {
group = group.filter(function(object) {
return !object.onSelect({ e: e });
});
}
return group;
}

How do you associate DOM elements with tween.js?

How do you relate TWEEN.js to a DOM element to do some complicated (or simple) animation effects on this DOM element?
I got a demo on the Internet, you need to create a sophisticated animation (using three.js) and DOM (to show or hide the elements associated with a TWEEN.js to DOM elements, DOM elements) has been written in the inside of the HTML (just slow to show or hide the effect).
The implementation of the clickMeOk method has achieved animation effects, but I hope to perform another effect at the same time - the display or hiding of text descriptions (shown or hidden with animation)
var isMeTweening = false;
function clickMeOk() {
if (isMeTweening)
return;
isMeTweening = true;
var scale = mesh6.scale.x < 1 ? 1 : 0.001;
new TWEEN.Tween(mesh6.scale)
.to({ x: scale, y: scale, z: scale }, 2000)
.easing(TWEEN.Easing.Quartic.InOut)
.onComplete(function() {
isMeTweening = false;
}).start();
var opacity = mesh6.material.opacity > 0 ? 0 : 0.5;
new TWEEN.Tween(mesh6.material).to({ opacity: opacity }, 1800).easing(TWEEN.Easing.Quartic.InOut).start();
//Here you want to add DOM element animation (display or hide)
}
Thanks !
I think You should use CSS to make animations on DOM Elements.
https://www.w3schools.com/css/css3_animations.asp

D3, retrieve the x position of a <g> element and apply multiple transform

With D3.js I want to transform the x of a group many times (eg. by clicking a button a causing a transform for every click), starting from the current position of the group. Problem is I can't retrieve the x of the group and, even if I find it, I can't find any built-in function that lets me change the x of a group starting from its current x. Am I missing something important? It's very strange I can't get the x of an element added to an SVG "stage".
My code is the following (edited for Lars): I created the nodes the way you suggested me yesterday (cloning them).
var m=[5,10,15,20,25,30];
var count=0;
d3.select('svg').on("mouseup", function(d, i){
count+=0.5;
var n = d3.selectAll(".myGroup");
n.each(function(d,i) {
if(i!=0){
// here I need to know the current x of the current n element but I
// can't get it
// this causes error "Uncaught TypeError: undefined is not a function"
// var currentx = d3.transform(this.attr("transform")).translate[0];
this.setAttribute("transform", "translate("+((100/m[i])*count)+",10)");
}
});
});
Getting and changing the position of a g element is relatively straightforward. For example like this:
var g = d3.select("#myG");
// get x position
var currentx = d3.transform(g.attr("transform")).translate[0];
// set x position
g.attr("transform", "translate(" + (currentx + 100) + ",0)");
Edit:
If you have a raw DOM element, select it with D3 first:
var currentx = d3.transform(d3.select(this).attr("transform")).translate[0];

How to avoid the overlapping of text elements on the TreeMap when child elements are opened in D3.js?

I created a Tree in D3.js based on Mike Bostock's Node-link Tree. The problem I have and that I also see in Mike's Tree is that the text label overlap/underlap the circle nodes when there isn't enough space rather than extend the links to leave some space.
As a new user I'm not allowed to upload images, so here is a link to Mike's Tree where you can see the labels of the preceding nodes overlapping the following nodes.
I tried various things to fix the problem by detecting the pixel length of the text with:
d3.select('.nodeText').node().getComputedTextLength();
However this only works after I rendered the page when I need the length of the longest text item before I render.
Getting the longest text item before I render with:
nodes = tree.nodes(root).reverse();
var longest = nodes.reduce(function (a, b) {
return a.label.length > b.label.length ? a : b;
});
node = vis.selectAll('g.node').data(nodes, function(d, i){
return d.id || (d.id = ++i);
});
nodes.forEach(function(d) {
d.y = (longest.label.length + 200);
});
only returns the string length, while using
d.y = (d.depth * 200);
makes every link a static length and doesn't resize as beautiful when new nodes get opened or closed.
Is there a way to avoid this overlapping? If so, what would be the best way to do this and to keep the dynamic structure of the tree?
There are 3 possible solutions that I can come up with but aren't that straightforward:
Detecting label length and using an ellipsis where it overruns child nodes. (which would make the labels less readable)
scaling the layout dynamically by detecting the label length and telling the links to adjust accordingly. (which would be best but seems really difficult
scale the svg element and use a scroll bar when the labels start to run over. (not sure this is possible as I have been working on the assumption that the SVG needs to have a set height and width).
So the following approach can give different levels of the layout different "heights". You have to take care that with a radial layout you risk not having enough spread for small circles to fan your text without overlaps, but let's ignore that for now.
The key is to realize that the tree layout simply maps things to an arbitrary space of width and height and that the diagonal projection maps width (x) to angle and height (y) to radius. Moreover the radius is a simple function of the depth of the tree.
So here is a way to reassign the depths based on the text lengths:
First of all, I use the following (jQuery) to compute maximum text sizes for:
var computeMaxTextSize = function(data, fontSize, fontName){
var maxH = 0, maxW = 0;
var div = document.createElement('div');
document.body.appendChild(div);
$(div).css({
position: 'absolute',
left: -1000,
top: -1000,
display: 'none',
margin:0,
padding:0
});
$(div).css("font", fontSize + 'px '+fontName);
data.forEach(function(d) {
$(div).html(d);
maxH = Math.max(maxH, $(div).outerHeight());
maxW = Math.max(maxW, $(div).outerWidth());
});
$(div).remove();
return {maxH: maxH, maxW: maxW};
}
Now I will recursively build an array with an array of strings per level:
var allStrings = [[]];
var childStrings = function(level, n) {
var a = allStrings[level];
a.push(n.name);
if(n.children && n.children.length > 0) {
if(!allStrings[level+1]) {
allStrings[level+1] = [];
}
n.children.forEach(function(d) {
childStrings(level + 1, d);
});
}
};
childStrings(0, root);
And then compute the maximum text length per level.
var maxLevelSizes = [];
allStrings.forEach(function(d, i) {
maxLevelSizes.push(computeMaxTextSize(allStrings[i], '10', 'sans-serif'));
});
Then I compute the total text width for all the levels (adding spacing for the little circle icons and some padding to make it look nice). This will be the radius of the final layout. Note that I will use this same padding amount again later on.
var padding = 25; // Width of the blue circle plus some spacing
var totalRadius = d3.sum(maxLevelSizes, function(d) { return d.maxW + padding});
var diameter = totalRadius * 2; // was 960;
var tree = d3.layout.tree()
.size([360, totalRadius])
.separation(function(a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; });
Now we can call the layout as usual. There is one last piece: to figure out the radius for the different levels we will need a cumulative sum of the radii of the previous levels. Once we have that we simply assign the new radii to the computed nodes.
// Compute cummulative sums - these will be the ring radii
var newDepths = maxLevelSizes.reduce(function(prev, curr, index) {
prev.push(prev[index] + curr.maxW + padding);
return prev;
},[0]);
var nodes = tree.nodes(root);
// Assign new radius based on depth
nodes.forEach(function(d) {
d.y = newDepths[d.depth];
});
Eh voila! This is maybe not the cleanest solution, and perhaps does not address every concern, but it should get you started. Have fun!

SVG: Calculating bounding box without displaying object

I need to get a text bounding box to adjust my layout before rendering anything. With some experimenting, I found that I had to actually render the text before 'getBBox' (or 'getComputedTextLength') will return a non-zero value:
var group = svgDocument.createElementNS(svgns, "g");
for(i=0; i <= nYblocks; ++i) {
str = svgDocument.createTextNode(strings[i]);
obj = tnode.cloneNode(true);
obj.setAttributeNS(null, "y", y1);
obj.appendChild(str);
group.appendChild(obj);
y1 += yBlockPx;
}
svgDocument.documentElement.appendChild(group); // **REQUIRED**
bb = vgroup.getBBox();
Problem: is there a good way to render the text so that it doesn't actually display? Should I just adjust the colours or opacity, or is there something clever I can do to render somewhere else, perhaps in a different tree?
Thanks -
Al
I think the easiest option is to draw it with the visibility set to hidden:
obj.setAttributeNS(null, "visibility", "hidden");

Resources