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.
Related
I have an embedded SVG in an HTML document. An (SVG) circle is animated using <animate>. I was trying to find a way to put some kind of event listener on that circle only when it moves horizontally.
Upon being moved (horizontally), I'd like to find the x-coordinates of the circle shape and set a third (outside) rect shape width to the relative position of the circle. This third rect would be like a progress bar tracking the horizontal progress of the circle.
Does the SVG circle (by the way, the circle is inside an SVG g-group) being moved by trigger some kind of event I can set a listener so that then I can change the width attribute of the sort of progress bar?
I have thought that if either the <animate> or the element moved/changed triggers some kind of event I could try to catch it and then change the width on the bar.
I have found that it is not much good use an "independent" animate on the rect as the pace of growth is very different when the circle moves upwards. I am not using the canvas element because I am trying to keep the scalability and the shapes semantics. (I would rather prefer a javascript solution but I would be grateful for other approaches.)
EDIT after answer: The anser have ben very much to the piint and (I think) helpful. I am very new to SVG and I may have misinterpreted something. Fot that reason I am including code.
I have tried to implement your recommendations and I seem to have been unsuccessful. .cx.animVal.value applied to the circle does not seem to get me what I need. I will include a chopped version of my code which should move a ball along a path which itself is being moved horizontally; two rects (inBar and outBar) should be tracking the horizontal displacement growing horizontally more or less at the same rate as the ball. In order to make sure setInterval works and the position is correctly gathered, a line has been added to list oBall..animVal and oball..baseVal. In FF 21.0, there is no change for animVal along the displacement. Have I understood your suggestions correctly? here follow the code (including headers etc. as I am a noob in SVG in particular):
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" dir="ltr">
<head><title>Motion</title>
<script>function beginAnim(anim,sPos){anim.beginElement();}</script>
</head>
<body>
<div id="here">
<button onclick="beginAnim(document.getElementById('anim'),'out');">START</button>
</div>
<div style="height:350px;">
<svg width="100%" height="100%" version="1.1" xmlns="http://www.w3.org/2000/svg">
<script type="text/ecmascript">
<![CDATA[
function trckngBars(){
oDiv=document.getElementById('here');
var oBall=document.getElementById('ball');
var oBar=[document.getElementById('inBar'),document.getElementById('outBar')];
idTimer=self.setInterval(function(){updtBars(oBall,oBar);},100);
}
function updtBars(oBall,oBar){
var xCoor=String(oBall.cx.animVal.value);
oDiv.innerHTML+='==>'+oBall.cx.animVal.value+'..'+oBall.cx.baseVal.value;
oBar[0].setAttribute("width",xCoor);
oBar[1].setAttribute("width",xCoor);
}
// ]]>
</script>
<defs>
<path id="throw" d="M0,0 q 80,-55 200,20" style="fill: none; stroke: blue;" />
</defs>
<g>
<g>
<rect x="2" y="50" width="400" height="110" style="fill: yellow; stroke: black;"></rect>
</g>
<g>
<!-- show the path along which the circle will move -->
<use id="throw_path" visibility="hidden" xlink:href="#throw" x="50" y="130" />
<circle id="ball" cx="50" cy="130" r="10" style="fill: red; stroke: black;">
<animateMotion id="ball_anim" begin="anim.begin+1s" dur="6s" fill="freeze" onbegin="trckngBars();" onend="window.clearInterval(idTimer);">
<mpath xlink:href="#throw" />
</animateMotion>
</circle>
<rect id="inBar" x="50" y="205" width="20" height="30" style="fill: purple;stroke-linecap: butt;">
<!-- <animate attributeType="XML" attributeName="width" from="0" to="200" begin="ball_anim.begin" dur="6s" fill="freeze" /> -->
</rect>
</g>
<animateTransform id="anim" attributeType="XML" attributeName="transform" type="translate" from="0" to="200" begin="indefinite" dur="10s" fill="freeze" />
</g>
<rect id="outBar" x="50" y="235" width="10" height="30" style="fill: orange;stroke-linecap: butt;">
<!-- <animate attributeType="XML" attributeName="width" from="0" to="400" begin="anim.begin+1s" dur="10s" fill="freeze" /> -->
</rect>
</svg>
</div>
</body>
</html>
If the code is run, it seems that animVal for the moving #ball remains at the same x-coordinat (50) while clearly it is moving.
An event is fired when animations begin, end or repeat but not (as you want) whenever there is a change of animation value.
As animations are deterministic though you can just start the rect shape animation so many seconds after the circle animation starts.
var cx = myCircle.cx.animVal.value;
will give you the animated value if you need it, provided that's the attribute you're animating.
You're using animateMotion rather than animating the cx and cy attributes on their own though. I'm think the only way to get the circle position post that transform is to call getBBox.
#Robert Thank you very much for your help. Your answer has been a good plunge into SVG and SMIL (and let me add cold). I have not been able to use getBBox, but inspecting the specification on paths ([link] http://www.w3.org/TR/SVG11/paths.html) and animateMotion (same site), it apears that can be achieved as SMIL animations are deterministic as suggested in your answer.
An animation has very few event triggers and by design seem as much concerned with the base state of the animation target as it is with the current position (theseem to be referred as "base values" and "presentation values"). (All the following works in javascript run by FF 21.) We can poll the current time of the animation applying getCurrentTime on the animateMotion object. I am assuming that the animation does it at constant velocity, so with that, we determine how much the object has moved along the path and obtain the length traversed (as we can get the total length of the whole path with method getTotalLength).
Then knowing the length, we can determine the current position on the path (using method getPointAtLength). Note, that the values returned, both time and position are relative to the container object, and thus they are scalable and/or require transformation).
For a (simple) working example, the javascript code in the Question sample code can be replaced by the following. It appears to work with the very few tests I have made:
function trckngBars(){
/* Upon beginning an animation (onbegin event), the required objects are gathered
and an interval is set */
var oBall=[document.getElementById('throw'),document.getElementById('ball_anim')];
var oBar=document.getElementById('inBar');
/* idTimer is set as a global variable so that it can be accessed from anywhere
to clear the interval*/
idTimer=self.setInterval(function(){updtBars(oBall,oBar);},50);
}
function updtBars(oBall,oBar){
/* This function, whose purpose is only to illustrate path method getPointLength
and animateMotion method getCurentTime, is quick and dirty. Note that oBall[0] is
the path and oBall[1] is the animate(Motion) */
//Calculates the amount of time passed as a ratio to the total time of the animation
var t_ratio=((oBall[1].getCurrentTime()-oBall[1].getStartTime())/oBall[1].getSimpleDuration());
// As mentioned, it assumes that animateMotion performs uniform motion along path
var l=oBall[0].getTotalLenth()*t_ratio;
// Gets (relative referred as user in documentation) horizontal coordinate
var xCoor=oBall[0].getPointAtLength(l).x;
oBar.setAttribute("width",xCoor);
}
function endTAnim(){
/* This function can be triggered _onend_ of an animation to clear the interval
and leave bars with the exact last dimensions */
window.clearInterval(idTimer);
var oBar=[document.getElementById('inBar'),document.getElementById('outBar')];
oBar[0].setAttribute("width",200); //hardcoded for convenience
}
Thus the simplest method I have been able to find requires the animation object (to obtain the time) and the path object (to "predict" the position) and it does not involve the actual element being moved by the animation. (It is somewhat simplifiedfrom the initial question to avoid discussing different coordinate systems when composed animations are used - this might be better discussed ia a stand-alone way.)
Though I have not noticed any lag (as the actual SVG is not much more complicated), I would be interested in knowing computationally cheaper methods as I was considering using this approach to find and draw a distance segment between two SMIL animated objects.
Of course all this relies on the assumption of a uniform movement aong the path, if that were not so and in larger images one might notice and offset I would also be grateful for any pointers on that (short of better do the animation directly in javascript/programming language and so you have total control). Thank you for all te edits you did avoiding getting into a quagmire - the only thing I knew about SVG three days ago is that it was XML.
A while ago I ran into the same problem you are describing. I wanted to be able to stop animations halfway, based on events triggered by the user and keep elements at their reached position. Unable to do so with SMIL I decided to forge my own animation system for svg.js, a small javascript library I have been working on:
http://documentup.com/wout/svg.js#animating-elements
It might be useful for what you are trying to achieve.
I have multiple existing SVG circles exported from CorelDraw and would like each to have unique text appear in a tooltip on hover.
Inside g element where are circles I added text elements. I located each text next to corresponding circle and with corresponding text.
<g id="cities" class="gradici">
<circle class="first" r="7" />
<circle class="second" r="7 />
</g>
var Citytooltip = svg.selectAll("g.gradici").selectAll("text")
.data(naziviGradova)
.enter()
.append("text")
.style("visibility", "hidden")
.attr("x", function(d,i){return graddx[i]})
.attr("y",function(d,i){return graddy[i]})
.text(function(d) {return d;})
.attr("font-size", "10px")
.attr("fill", "#black");
I menage to get when I hover over any circle that all text is visible/hidden next to all circles.
var city= svg.selectAll("#cities circle");
city
.on("mouseover", (function(){Citytooltip.style("visibility",
"visible");}))
.on("mouseout", (function(){Citytooltip.style("visibility",
"hidden");}));
But I am straggling how to get text to be visible/hidden just over the circle I am hovering. I suppose I should somehow iterate trough city but I am stuck how to do that. Any ideas?
Add a title, description or metadata elements as content for a circle element in order for the user agent to provide tooltips (depending on the user agent):
<g id="cities" class="gradici">
<desc>A group of circles</desc>
<circle class="first" r="7">
<desc>First circle</desc>
</circle>
<circle class="second" r="7>
<desc>Second circle</desc>
</circle>
</g>
This is specified by SVG 1.1.
For modern desktop and mobile Web browsers, the provided element descriptions are typically rendered as you describe and expect -- as tooltips appearing when the user "hovers their pointer device" over the circle element that e.g. contains the desc element.
My advice would be to not reinvent the wheel with elaborate and complicated script-based solutions that always carry the risk of breaking for some of your users, not when something like the above is part of SVG already and suffices for you.
If user agent tooltips aren't going to cut it, one will have to implement some of the functionality oneself. I decide to still rely on the declarative desc elements and we can trivially use these even with rendering tooltips ourselves.
In the following SVG document, a tooltip definition is used as a template, and whenever the "mouse" pointer (anything that can generate "mouse*" events, really) enters an element, we extract the document fragment (Range) that is the contents of its desc element and copy these contents into the "contents" group/graphics of the tooltip. On top of that, we calculate the position where the tooltip should be shown -- at the tip of the mouse pointer -- and we resize the background "panel" so that it actually resembles what most people accept as tooltips.
You can add your own styling and even animation to further refine desired result.
More explanation is in the comments in the code below:
<?xml version="2.0" encoding="utf-8" ?>
<!DOCTYPE svg SYSTEM "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100">
<style>
#rich-tooltip {
pointer-events: none;
}
#rich-tooltip .panel {
fill: silver;
}
</style>
<defs>
<!-- the actual template, will be removed from the context and always shown as appended at the end of the document body, so that it is rendered above everything else. -->
<g id="rich-tooltip">
<rect class="panel" /><!-- just some decorative background -->
<g class="contents" /><!-- contents of an element's description will be added as child nodes of this element -->
</g>
</defs>
<circle cx="50" cy="50" r="25" fill="yellow">
<desc><circle cx="10" cy="10" r="5" /><text dominant-baseline="hanging" fill="red">First circle</text></desc>
</circle>
<circle cx="70" cy="50" r="40" fill="green">
<desc><circle cx="10" cy="10" r="5" /><text dominant-baseline="hanging" fill="red">Second circle</text></desc>
</circle>
<script>
const tooltip = document.getElementById("rich-tooltip");
tooltip.remove(); /// Initial state of the tooltip is "not shown" (removed from DOM tree)
var timeout; /// We only show the tooltip once the pointer settles and some time passes
const tooltip_delay = 1000; /// Delay before showing the tooltip once pointer settles
var last_tooltip_ev; /// Auxilary variable to be able to calculate movement after showing the tooltip, so we don't remove it immediately but only once the pointer actually moved substantially, this is just a nice touch, not otherwise crucial
const remove_tooltip_move_threshold = 10; /// How many user units (pixels, normall) the cursor may move before tooltip is hidden
function on_mouse_move_event(ev) {
if(document.contains(tooltip)) { /// Is the tooltip being shown?
if(last_tooltip_ev) {
if(((x, y) => Math.sqrt(x * x + y * y))(ev.clientX - last_tooltip_ev.clientX, ev.clientY - last_tooltip_ev.clientY) >= remove_tooltip_move_threshold) { /// has the pointer moved far enough from where the tooltip was originally shown?
tooltip.remove(); /// Hide the tooltip
}
}
} else {
if(timeout) clearTimeout(timeout);
timeout = setTimeout(show_tooltip, tooltip_delay, ev);
}
}
function show_tooltip(ev) {
const desc = ev.target.querySelector(":scope > desc");
if(!desc) { /// Does the element that is under the pointer even have a description?
tooltip.remove(); /// Hide the tooltip (ignoring the fact it may not be showing in the first place)
return;
}
document.documentElement.appendChild(tooltip);
const desc_range = document.createRange();
desc_range.selectNodeContents(desc); /// Select all children of the description element, as `desc_range`
const contents = tooltip.querySelector(".contents");
const contents_range = document.createRange();
contents_range.selectNodeContents(contents); /// Select all children of the tooltip contents element, as `contents_range`
contents_range.extractContents(); /// Empty tooltip contents
contents.appendChild(desc_range.cloneContents()); /// Fill contents with previously selected description. We _copy_ the description -- the original should legitimately stay where it was
const panel = tooltip.querySelector("rect.panel");
panel.setAttribute("width", contents.getBBox().width);
panel.setAttribute("height", contents.getBBox().height); /// "Stretch" the panel so that it covers the tooltip contents
const pt = document.documentElement.createSVGPoint();
pt.x = ev.clientX;
pt.y = ev.clientY;
const view_pt = pt.matrixTransform(document.documentElement.getScreenCTM().inverse()); /// We need to convert mouse pointer coordinates to the SVG viewbox coordinates
tooltip.setAttribute("transform", `translate(${view_pt.x} ${view_pt.y})`); /// Move the tooltip to appropriate position
last_tooltip_ev = ev; /// Save the event to be able to calculate distance later (above)
}
addEventListener("mouseover", function(ev) { /// When the pointer gets over an element...
ev.target.addEventListener("mousemove", on_mouse_move_event); /// We will be tracking pointer movements to trigger timeout for showing the tooltip
ev.target.addEventListener("mouseout", function() { /// Once the pointer gets anywhere but the element itself -- like over its children or other elements...
ev.target.removeEventListener("mouseout", on_mouse_move_event); /// Cancel the whole mousemove business, the behavior will be setup by whatever element the mouse pointer gets over next anyway
}, { once: true }); /// Once, for this element, everything else will be setup by another call for "mouseover" listener
});
</script>
</svg>
The code would be simpler without timeout triggering etc, but chances are a well thought out and user friendly tooltip implementation would be using delays and compensate for stray pointer movements, so I thought I'd make sense to keep some skeletal framework in place for these and also demonstrate how one would go about implementing them.
This is anyway fairly optimal in the sense that you only use one set of listeners at each time -- you don't need to assign listeners to each and all elements you want to track. If an element has a description, this script will make sure a tooltip is shown, is all. Temporarily, we do assign mouseout listener to an element but it's typically only one listener assigned to only one element at any point in time -- as soon as the pointer gets out of an element, the listener is removed (and something else reassigns another instance of it, but that's perfectly fine).
I menage to solve my problem, so I will leave the answer if someone get stuck like me.
var titleCreate = svg.selectAll("g.gradici circle").append("title").text("tooltip");
for (var i =0; i<naziviGradova.length; i++){
var textNazivaGradova = naziviGradova[i];
var title = svg.getElementsByTagName("title");
title[i].innerHTML = textNazivaGradova;
}
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).
I have the following SVG graphic:
<svg version="1.1" id="diagram" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="375px" height="150px">
<path d="M45,11.5H33.333c0.735-1.159,1.167-2.528,1.167-4C34.5,3.364,31.136,0,27,0s-7.5,3.364-7.5,7.5c0,1.472,0.432,2.841,1.167,4H9l-9,32h54L45,11.5z M22.5,7.5C22.5,5.019,24.519,3,27,3s4.5,2.019,4.5,4.5c0,1.752-1.017,3.257-2.481,4h-4.037 C23.517,10.757,22.5,9.252,22.5,7.5z" id="control"/>
</svg>
I want to programmatically change the scale of this object, but I want it to scale from the center point.
I've tried wrapping it around a <g> tag, like so
<g transform="translate(0,0)">
<path x="0" y="0" id="control" transform="scale(2)">...</path>
</g>
But this doesn't seem to work. It seems that scaling a path requires manipulation of the path's matrix, which seems horrifically difficult. Annoyingly, it's easy to scale using additive="sum" property but in this instance, I am not using a transform animation.
Can anyone help me out?
Edit: Managed to get this working nicely, for anyone who is stuck on the same thing, here is a nice way of doing it programmatically:
var elem = document.getElementById("control");
var bBox = elem.getBBox();
var scaleX = 2;
var scaleY = 2;
$(elem).attr("transform", `scale(${scaleX}, ${scaleY}) translate(${-bBox.width/2},${-bBox.height/2})`);
If you know the coordinates of the center point, then you can combine a translate and scale in one transformation. The translation is calculated as: (1 - scale) * currentPosition.
If the center is (10, 20) and you are scaling by 3 then translate by (1 - 3)*10, (1 - 3)*20 = (-20, -40):
<g transform="translate(-20, -40) scale(3)">
<path d="M45,11.5H33.333c0.735-1.159,1.167-2.528,1.167-4C34.5,3.364,31.136,0,27,0s-7.5,3.364-7.5,7.5c0,1.472,0.432,2.841,1.167,4H9l-9,32h54L45,11.5z M22.5,7.5C22.5,5.019,24.519,3,27,3s4.5,2.019,4.5,4.5c0,1.752-1.017,3.257-2.481,4h-4.037 C23.517,10.757,22.5,9.252,22.5,7.5z" id="control"/>
</g>
The transformations are applied in reverse order from the one they are declared, so in the example, above, the scale is performed first and then the translate. Scaling affects the coordinates so the translation here is in scaled coordinates.
You can calculate the center point programmatically using element.getBBox().
You can alter the origin to center:
.scaled-path-svg {
svg {
path {
transform-origin: center;
transform: scale(1.1);
}
}
}
The answer provided by aetheria earlier is great. There is another thing to take care of as well -- stroke-width, so that the outline stays of the same width while the object scales. Usage:
stroke-width: (1/scaling-factor)
So, if your scaling is by say 2, then:
stroke-width: (0.5)
NOTE: You shouldn't missout the transform: translate(...) scale(2) as mentioned by aetheria.
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.