Labeling force-directed nodes in d3: speed vs visibility - javascript

Yet another question about adding labels to a d3 force graph...
I am labeling a graph with nodes that are inside individual groups, and I have been appending the labels inside these groups like so:
<svg>
<g class="nodes-with-labels">
<g class="individual-node">
<circle></circle>
<text>Node Label</text>
</g>
...
</g>
</svg>
This adds minimal extra elements to the graph and allows my graph's tick() function to just call one transform operation. I put up a demo fiddle here (without any movement/tick() function):
https://jsfiddle.net/52cLjxt4/1/
Unfortunately, the labels end up behind many of the nodes because they are in groups that are drawn before other groups that contain nodes. This problem can be solved by putting nodes and labels into separate parent groups, like in this example:
https://jsfiddle.net/hhwawm84/1/
<svg>
<g class="nodes">
<g class="individual-node">
<circle></circle>
</g>
...
</g>
<g class="labels">
<g class="individual-label">
<text>Node Label</text>
</g>
...
</g>
</svg>
However, this appears to be significantly slower: it creates more elements and requires two transform statements instead of one in the tick() statement, since it's moving the labels around separately.
Speed is a real concern for my project. Is there a better approach here that might avoid creating so many extra groups and doubling the transform statements?

You don't need to each label and circle in an g - just set the transform attribute directly on each element. It might also be worth profiling setting the cx/cy and x/y attributes instead of transform too.
If you don't need the graph to animate, precomputing the ticks and setting the transforms could help with performance:
for (var i = 0; i < 120; ++i) simulation.tick();
If that's still too slow, try using canvas (faster because it doesn't have a scene graph) or css transforms on html elements (faster because they are gpu accelerated).

Related

How to use SVG with React and generate icons

Is there any place where I can upload an icon and get an SVG string vector?
Also, what are those tags in the code below?<g> <path> and viewBox, data-original, xmlns, d tags are?
Lately, Is the performance worth using SVG in place of regular icons?
<svg viewBox="0 0 512 512">
<g xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="M0 0h128v128H0zm0 0M192 0h128v128H192zm0 0M384 0h128v128H384zm0 0M0 192h128v128H0zm0 0"
data-original="#bfc9d1"
/>
</g>
</svg>
Here's a very good guide to using SVG with react. https://www.sanity.io/guides/import-svg-files-in-react
There are many online convertors you can use to create svg's
e.g https://www.pngtosvg.com/
The SVG <g> element is used to group SVG shapes together.
The SVG <path> element indicates that the vector to draw is a path. The could alternatively be a polyline or a shape e.g circle.
The <viewBox> attribute is a list of four values: min-x, min-y, width and height
The xmlns attribute is XML Namespace which is needed to use the correct DTD - Doctype Declaration
The <d> attribute defines the path that will be drawn.
From my experience SVG performs significantly faster when using inline SVG's.
The main blocking element for page loading is the numberous amount of files that load sequentially. Using inline svg loads all the images within the page file.
The major benefit of SVG's are the scalability of vector over raster when zooming or viewing at differant resolutions.

Transition destroys SVG rotation

I am working on a JavaScript project, and as part of it, trying to rotate an svg path element around a given point.
Here is a simplified example of the js file:
var angle=0;
d3.select("#canvas")
.append("path")
.attr("id", "sector")
.attr("fill", "red")
.attr("d", "M150,150 L150,50 A100,100 0 0,1 236.6,200 Z")
.on("click", function(){
console.log("click");
angle=(angle+120)%360;
d3.select("#sector")
.transition().duration(2500) //removing this line leads to a nice transform attribute in the resulting html
.attr("transform","rotate("+angle+",150,150)");
})
And the html is just:
<svg id="canvas" width="300" height="300">
<circle cx="150" cy="150" r="100" stroke="blue" fill="none"/>
</svg>
Here you can find it on JSfiddle.
As the comment in the above excerpt suggests, it all works fine with the 3-argument version of the rotate function, where I can specify the x and y coordinates of the point which I want to use as the center of rotation. The resulting path element gets a transform attribute with the value of "rotate(120,150,150)" Unless I want to use a transition.
When I insert the line about the transition, the transformation gets some weird extra things added, it looks like "translate(354.9038105676658, 95.09618943233417) rotate(119.99999999999999) skewX(-3.1805546814635176e-15) scale(0.9999999999999999,0.9999999999999999)"
I guess in the background the non-(0,0)-centered rotation gets replaced with some translations and a (0,0)-centered rotation, just as you can do it in geometry. The position and orientation after the transition is fine indeed. However, during the transformation the element is moving on a funny path, in the example the sector leaves the circle.
Is there a way I can suppress the transition doing all these transformations and just apply a single non-(0,0)-based rotation?
Are there any other workarounds?
Changing the transform-origin for the path element attribute did not seem to work, but maybe I was doing it wrong.
I am looking for a CSS-free solution. It is an architectural decision which I cannot overrule in the project.
As you see, D3 is already involved in the project, but I would like to use as few additional external libraries as possible.
Thanks in advance!
create your arc with its center at (0,0). Then translate it to the center of the circle.
Then the d3 transition will work nicely as follows:
d3.select("#sector")
.transition().duration(2500)
.attr("transform","translate(150,150)rotate("+angle+")");

How to get position of empty SVG group?

I'm working with d3, and I have trouble with positioning and empty groups.
I have this svg:
<svg id="mysvg" height="200" width="1350" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(50, 6)">
<g class="a">
<g transform="translate(0,161.3919677734375)" style="opacity: 1;">
<line y2="0" x2="-6"></line>
</g>
</g>
<g class="b">
</g>
</g>
</svg>
I want to dynamically add a line to group with class b, and I want to add it so the coordinates of the first point of the line will coincide with the coordinates of the second point of the line inside group a.
Since coordinates of SVG objects are relative to their containers, to get the relative coordinates I get first the absolute position of the line inside g.a and the absolute position of g.b, using getBoundingClientRect()
The problem is coordinates of g.b, if it's empty, are completely messed up. I have to create a bogus object to get them properly:
d3.select("#mysvg .b").append("circle").attr("r", 0).attr("fill", "transparent")
.attr("cx", 0).attr("cy", 0);
Furthermore, if I create the circle with a radius greater than zero, the position of its group b will shift.
You can use basic DOM interfaces to transform between both coordinate systems. Interface InterfaceSVGLocatable defines method getTransformToElement() which
Returns the transformation matrix from the user coordinate system on the current element (after application of the ‘transform’ attribute, if any) to the user coordinate system on parameter element (after application of its ‘transform’ attribute, if any).
It is worth noting, that support for this was dropped from Chrome 48+ (Issue 524432). However, there is a rather slim polyfill available:
// Polyfill needed for Chrome 48+
SVGElement.prototype.getTransformToElement =
SVGElement.prototype.getTransformToElement || function(elem) {
return elem.getScreenCTM().inverse().multiply(this.getScreenCTM());
};
Because it is just this one simple line, it might even be easier to directly use it within your code.
You may than use a helper SVGPoint to get the transformation matrix needed to transform from elements in your group a to elements in group b:
// Create a helper point
var point = document.getElementById("mysvg").createSVGPoint();
point.x = line.getAttribute("x2");
point.y = line.getAttribute("y2");
// Calculate starting point of new line based on transformation between coordinate systems.
point = point.matrixTransform(line.getTransformToElement(groupB));
Have a look at this working example, which draws a red line in group b starting at the end of the line in group a to coordinates (100, 100):
// Polyfill needed for Chrome >48
SVGElement.prototype.getTransformToElement = SVGElement.prototype.getTransformToElement || function(elem) {
return elem.getScreenCTM().inverse().multiply(this.getScreenCTM());
};
var groupB = document.querySelector("g.b");
var line = document.querySelector(".a line");
// Create a helper point
var point = document.getElementById("mysvg").createSVGPoint();
point.x = line.getAttribute("x2");
point.y = line.getAttribute("y2");
// Calculate starting point of new line based on transformation between coordinate systems.
point = point.matrixTransform(line.getTransformToElement(groupB));
// D3 applied to simplify matters a little.
// New line is drawn from end of line in group a to (100,100).
d3.select(groupB).append("line")
.attr({
"x1": point.x,
"y1": point.y,
"x2": 100,
"y2": 100
});
line {
stroke: black;
}
.b line{
stroke:red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg id="mysvg" height="200" width="1350" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(50, 6)">
<g class="a">
<g transform="translate(0,161.3919677734375)" style="opacity: 1;">
<line y2="0" x2="-40"></line>
</g>
</g>
<g class="b">
</g>
</g>
</svg>
Because questions were raised in the comments regarding the performance of this approach compared to the one proposed by the question itself, I have set up a little jsPerf test case to compare both. I would suspect this answer's code to significantly outperform the original one because manipulating the DOM is always an expensive operation. The matrix calculations on the other hand will only have to use values from the DOM without applying any modifications. The results clearly back this assumption with the insertion of a circle being at least 95% slower in FF, IE and Chrome.

How to add a line to an SVG chart (drawn by Google Visualization)

I am drawing a bubble chart using Google Visualization and would like to add a line to the chart after it is rendered by Google. I can see the circle elements that make up the chart and address them via jQuery (for instance, to change a circle's color) and am trying to add a line element as a sibling to the circles.
After I execute
$('svg').find('circle:first').attr('fill', 'blue');
$('svg').find('circle:first').after(
"<line x1='153' y1='383' x2='381' y2='236' stroke='black' stroke-width='2'></line>"
);
The first circle turns blue, but no line appears. I can inspect the DOM and see:
<circle cx="153.6111617460369" cy="383.625" r="30" stroke="#cccccc" stroke-width="1" fill-opacity="0.8" fill="blue"></circle>
<line x1="153" y1="383" x2="381" y2="236" stroke="black" stroke-width="2"></line>
<circle cx="381.7818593144754" cy="236.68327383367495" r="19" stroke="#cccccc" stroke-width="1" fill-opacity="0.8" fill="#deb887"></circle>
Is there something I need to do beyond adding the line element to make to the svg element to cause the line to appear?
I don't know if the jQuery library allows you to modify SVG elements (and the elements inside them). I think it only supports standard HTML elements.
I would however look into this library: http://raphaeljs.com/ as it appears to solve your issue exactly!
If there is no interaction the google chart, you can use this method...
jquery's append not working with svg element?
Else you may need to try it using d3...
Svg line not displayed on google charts?

enclosing the nodes of a d3 force directed graph in a circle or a polygon or a cloud

I have built a d3 force directed graph with grouped nodes. I want to enclose the groups inside cloud like structure. How can I do this?
Js Fiddle link for the graph: http://jsfiddle.net/Cfq9J/5/
My result should look similar to this image:
This is a tricky problem, and I'm not wholly sure you can do it in a performative way. You can see my static implementation here: http://jsfiddle.net/nrabinowitz/yPfJH/
and the dynamic implementation here, though it's quite slow and jittery: http://jsfiddle.net/nrabinowitz/9a7yy/
Notes on the implementation:
This works by masking each circle with all of the other circles in its group. You might be able to speed this up with collision detection.
Because each circle is both rendered and used as a mask, there's heavy use of use elements to reference the circle for each node. The actual circle is defined in a def element, a non-rendered definition for reuse. When this is run, each node will be rendered like this:
<g class="node">
<defs>
<circle id="circlelanguages" r="46" transform="translate(388,458)" />
</defs>
<mask id="masklanguages">
<!-- show the circle itself, as a base -->
<use xlink:href="#circlelanguages"
fill="white"
stroke-width="2"
stroke="white"></use>
<!-- now hide all the other circles in the group -->
<use class="other" xlink:href="#circleenglish" fill="black"></use>
<use class="other" xlink:href="#circlereligion" fill="black">
<!-- ... -->
</mask>
<!-- now render the circle, with its custom mask -->
<use xlink:href="#circlelanguages"
mask="url(#masklanguages)"
style="fill: #ffffff; stroke: #1f77b4; " />
</g>
I put node circles, links, and text each in a different g container, to layer them appropriately.
You'd be better off including a data variable in your node data, rather than font size - I had to convert the fontSize property to an integer to use it for the circle radius. Even then, because the width of the text isn't tied to the data value, you'll get some text that's bigger than the circle beneath it.
Not sure why the circle for the first node isn't placed correctly in the static version - it works in the dynamic one. Mystery.

Categories

Resources