I have some svg on html page. I want to to bind some data on them, and add decoration element based on that data. What I think I have to do is:
// pseudo-code
selection = select all existing svg piece i want to decorate
var datas = selection.each( function(d,i) { // each is right? maybe selection.datum() is better?
var data = this.(do something with the svg piece)
return data;
});
// add elements with the "default" chain selection.data(datas).enter().append()
What I noticed is that selection.each does not return something where i can find the data returned. I think this is the way, but i can't figure out what i have to do for see the binded data.
So i have to do some dirty workaround like:
var datas = [];
selection.each( function(d,i) { // each is right? maybe selection.datum() is better?
var data = this.(do something with the svg piece)
datas.push(data);
});
Why? how can i do something similar without pushing manually data into an array AND binding data inside some existing svg element?
Here is a jsFiddle example.
Or, if you prefer, the code:
html:
<div id="container">
<svg>
<rect id="0" x="0" y="50" width="30" height="30"/>
<rect id="1" x="50" y="50" width="30" height="30"/>
<rect id="2" x="100" y="50" width="30" height="30"/>
<rect id="3" x="150" y="50" width="30" height="30"/>
<rect id="4" x="200" y="50" width="30" height="30"/>
<rect id="5" x="250" y="50" width="30" height="30"/>
</svg>
</div>
js:
var svg = d3.select("#container svg");
var districts = svg.selectAll("rect");
var district_data = [];
var _c = districts.each(function(d, i) {
var bbox = this.getBBox();
var centroid = [
bbox.x + bbox.width/2,
bbox.y + bbox.height/2
];
var ret = {centroid:centroid, position:bbox.x};
district_data.push( ret );
return ret;
});
// now, i'm expecting that _c should be something
// similar to district_data
console.log(_c);
svg
.selectAll("circle")
.data(district_data) // i think i should use `_c` instead of manually created `district_data` but does not work
.enter()
.append("circle")
.attr("class", "district_circle")
.attr("cx", function(d){ return d.centroid[0]})
.attr("cy", function(d){ return d.centroid[1]})
.attr("r", 10)
.attr("fill", function(d){ return "rgb("+d.position+",0,0)"});
First, it's incorrect to expect an each() method to return an array of data. It's just a way to iterate over the selection. What it's returning (the thing that's getting assigned to _c) is a d3 selection object –– the same selection object on which you're calling each(). I.e. _c == districts evaluates to true. AFAIK, the d3 selection object doesn't provide anything that you could use to gather values the way you describe wanting to do.
Generally, you'd want to use a .map() function to gather those values and assign them to _c, but unfortunately that doesn't seem to be possible here, because, again, districts is a d3 selection, not a flat array. And if you tried calling map() on it, I don't think it would actually iterate over each element in the selection, and you'll also get a js error, because the this object doesn't get assigned to the SVG element on which you need to call getBBox().
Bottom line, I think the approach you took is the correct one: iterate with each() and build up the array by pushing into it.
I can suggest one other way to do this more concisely, but it requires modifying the structure of the existing SVG:
Instead of of having the rects be siblings, nest each one inside a g. Like:
<div id="container">
<svg>
<g>
<rect id="0" x="0" y="50" width="30" height="30"/>
</g>
<g>
<rect id="1" x="50" y="50" width="30" height="30"/>
</g>
...
Then in JS (untested):
svg.selectAll('g')
.each(function(d, i) { // Note, d is actually undefined, bc no data-binding was done
var rect = d3.select(this).select('rect');
var bbox = this.getBBox();
var centroid = [
bbox.x + bbox.width/2,
bbox.y + bbox.height/2
];
// Now create the circle, appending it to the group,
// as a sibling of its corresponding rect
var circle = d3.select(this).append('circle')
.attr("class", "district_circle")
.attr("cx", centroid[0])
.attr("cy", centroid[1])
.attr("r", 10)
.attr("fill", "rgb("+bbox.x+",0,0)");
});
This is still not totally great form, because the positioning is applied to each circle and rect, whereas, ideally, the positioning would be applied at the group level –– and that's not too hard to achieve. But now we're getting picky.
Related
Suppose I have created a svg with d3.js like that:
svg = d3.select('.svgContainer').append("svg")
.attr("width", '100%')
.attr("height", '100%')
.call(d3.zoom().on("zoom", function () {
svg.attr("transform", d3.event.transform)
}))
.append("g");
and I now want to add different groups to it (dynamically and from different files). For example that one:
<svg>
<g id="myGroup">
<rect width="100" height="100" fill="blue" />
</g>
</svg>
I know, that I could add the whole svg like that (suppose the file is called test.svg):
d3.xml("test.svg").then(function(xml) {
var svgNode = xml.getElementsByTagName("svg")[0];
svg.node().appendChild(svgNode);
});
However, this procuces the following DOM:
svg
-svg
--myGroup
But because I need to transform my Group with respect to the main svg, I need the following DOM structure:
svg
-myGroup
--(eventually more dynamically added groups)
I have tried the following and got the correct DOM, but the group does not show up inside of my svg:
d3.xml("test.svg").then(function(xml) {
var svgGroup = xml.getElementById("myGroup");
svg.node().append(svgGroup);
});
EDIT: I found out, this should already work, the problem was I had several critical <defs> inside of my SVG (for example for gradients). I lost them, when I only appended the group. I ended up, wrapping my <g> around the <defs> and everything works fine now.
Just as a complement to the accepted answer: if you want to a pure D3 solution, you can use selection.append with a function. In your case:
d3.xml(svgfile).then(function(xml) {
var svgNode = d3.select(xml).select("#MyGroup");
svg.append(() => svgNode.node()) ;
});
Or even shorter:
d3.xml(svgfile).then(function(xml) {
svg.append(() => d3.select(xml).select("#MyGroup").node()) ;
});
Here is a demo using the Wikipedia SVG linked in the other answer:
var svgfile = "https://simple.wikipedia.org/static/images/mobile/copyright/wikipedia-wordmark-en.svg";
var svg = d3.select('body').append("svg")
.attr("width", '120')
.attr("height", '20');
d3.xml(svgfile).then(function(xml) {
var svgNode = d3.select(xml).select("#Wikipedia");
svg.append(() => svgNode.node());
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
You are almost there. I have illustrated both ways to add elements in this codepen. The problem with the first approach is that svgNode is not a node, but a nodeList. You can add each node in the list with a loop, as you can see in the code:
...
var toAdd = svgNode.childNodes;
for (var i = 0; i < svgNode.childElementCount; i++){
svg.node().appendChild(toAdd[i]);
}
...
Regarding the second approach, I cannot say that there is anything wrong. I have imported a path from a different svg file and it is displayed in the lower part. The only detail that I changed is that I set the size of the svg element explicitly, not as 100%. Is it possible that it was not rendered because it was outside the visible part?
I am using the VivaGraph.js library to render a graph in SVG. I am trying to display an image cropped to a circle, for which I am using a clipPath element - as recommended in this post.
However, when I create a new SVG element of type that has a capital letter in it, e.g. clipPath in my case, the element that is inserted into the DOM is lowercase, i.e. clippath, even though the string I pass in to the constructor is camelCase. Since SVG is case sensitive, this element is ignored. Everything else seems to be okay.
I also tried to change the order in which I append the child elements, in hopes of changing the 'z-index', but it didn't have an impact on this.
I am using the following code inside of the function that creates the visual representation of the node in the graph (the 'addNode' callback) to create the node:
var clipPhotoId = 'clipPhoto';
var clipPath = Viva.Graph.svg('clipPath').attr('id', clipPhotoId);
var ui = Viva.Graph.svg('g');
var photo = Viva.Graph.svg('image').attr('width', 20).attr('height', 20).link(url).attr('clip-path', 'url(#' + clipPhotoId + ')');
var photoShape = Viva.Graph.svg('circle').attr('r', 10).attr('cx', 10).attr('cy', 10);
clipPath.append(photoShape);
ui.append(clipPath);
ui.append(photo);
return ui;
Thank you!
There is a bit of tweaking needed on top of the post you provided.
General idea to solve your issue is this one:
We create a VivaGraph svg graphics (which will create an svg element in the dom)
Into this svg graphic we create only once a clip path with relative coordinates
When we create a node we refer to the clip path
Code is:
var graph = Viva.Graph.graph();
graph.addNode('a', { img : 'a.jpg' });
graph.addNode('b', { img : 'b.jpg' });
graph.addLink('a', 'b');
var graphics = Viva.Graph.View.svgGraphics();
// Create the clipPath node
var clipPath = Viva.Graph.svg('clipPath').attr('id', 'clipCircle').attr('clipPathUnits', 'objectBoundingBox');
var circle = Viva.Graph.svg('circle').attr('r', .5).attr('cx', .5).attr('cy', .5);
clipPath.appendChild(circle);
// Add the clipPath to the svg root
graphics.getSvgRoot().appendChild(clipPath);
graphics.node(function(node) {
return Viva.Graph.svg('image')
.attr('width', 30)
.attr('height', 30)
// I refer to the same clip path for each node
.attr('clip-path', 'url(#clipCircle)')
.link(node.data.img);
})
.placeNode(function(nodeUI, pos){
nodeUI.attr('x', pos.x - 15).attr('y', pos.y - 15);
});
var renderer = Viva.Graph.View.renderer(graph, { graphics : graphics });
renderer.run();
The result in the dom will be like this:
<svg>
<g buffered-rendering="dynamic" transform="matrix(1, 0, 0,1,720,230.5)">
<line stroke="#999" x1="-77.49251279562495" y1="-44.795726056131116" x2="6.447213894549255" y2="-56.29464520347651"></line>
<image width="30" height="30" clip-path="url(#clipCircle)" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="a.jpg" x="-92.49251279562495" y="-59.795726056131116"></image>
<image width="30" height="30" clip-path="url(#clipCircle)" xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="b.jpg" x="-8.552786105450746" y="-71.2946452034765"></image>
</g>
<clipPath id="clipCircle" clipPathUnits="objectBoundingBox">
<circle r="0.5" cx="0.5" cy="0.5"></circle>
</clipPath>
</svg>
Notice the clipPathUnits="objectBoundingBox", since it's the main trick for this solution.
I have an SVG group with a rect inside of it, and would like the rect to act as a border for the group...
<g>
<rect></rect>
</g>
but the group is dynamic and its content changes. I am attempting to resize the rect in my update function as such
.attr("x", function(d) { return this.parentNode.getBBox().x })
.attr("y", function(d) { return this.parentNode.getBBox().y })
.attr("width", function(d) { return this.parentNode.getBBox().width })
.attr("height", function(d) { return this.parentNode.getBBox().height })
But what seems to happen is that it expands relatively fine, but then cannot shrink properly since the group's bounding box width is now the same as the expanded rect's width (the rect's width is the group's width, but the group's width is now the rect's width).
Is there any way to get a rectangle inside an SVG group to properly resize and act as a border?
There's more than one way to solve this.
Use the outline property (2014-08-05 status: works in Chrome and Opera)
<svg xmlns="http://www.w3.org/2000/svg" width="500px" height="500px">
<g style="outline: thick solid black; outline-offset: 10px;">
<circle cx="50" cy="60" r="20" fill="yellow"/>
<rect x="80" y="80" width="200" height="100" fill="blue"/>
</g>
</svg>
See live example.
Use a filter to generate the border (2014-08-05 status: works in Firefox, but Chrome/Opera has a bug on feMorphology, but it should be possible to work around that by using other filter primitives).
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
<defs>
<filter id="border" x="-5%" y="-5%" width="110%" height="110%">
<feFlood flood-color="black" result="outer"/>
<feMorphology operator="erode" radius="2" in="outer" result="inner"/>
<feComposite in="inner" in2="outer" operator="xor"/>
<feComposite in2="SourceGraphic"/>
</filter>
</defs>
<g filter="url(#border)">
<circle cx="50" cy="60" r="20" fill="yellow"/>
<rect x="80" y="80" width="200" height="100" fill="blue"/>
</g>
</svg>
See live example.
Both of the above will automatically update to whatever size the group has, without the need for DOM modifications.
Yes, you can find the new bounding box by selecting all child elements of the group that are not the bounding rect itself, and then calculating the overall bounding box based on the individual bounding boxes of the children.
Lets say your bounding rect had a class of bounding-rect, you could do the following:
function updateRect() {
// SELECT ALL CHILD NODES EXCEPT THE BOUNDING RECT
var allChildNodes = theGroup.selectAll(':not(.bounding-rect)')[0]
// `x` AND `y` ARE SIMPLY THE MIN VALUES OF ALL CHILD BBOXES
var x = d3.min(allChildNodes, function(d) {return d.getBBox().x;}),
y = d3.min(allChildNodes, function(d) {return d.getBBox().y;}),
// WIDTH AND HEIGHT REQUIRE A BIT OF CALCULATION
width = d3.max(allChildNodes, function(d) {
var bb = d.getBBox();
return (bb.x + bb.width) - x;
}),
height = d3.max(allChildNodes, function(d) {
var bb = d.getBBox();
return (bb.y + bb.height) - y;
});
// UPDATE THE ATTRS FOR THE RECT
svg.select('.bounding-rect')
.attr('x', x)
.attr('y', y)
.attr('width', width)
.attr('height', height);
}
This would set the x and y values of the overall bounding box to be the minimum x and y values in the childrens' bounding boxes. Then the overall width is calculated by finding the maximum right boundary bb.x + bb.width and subtracting the overall box's x. The overall height is then calculated in the same way as the width.
HERE is an example of this.
The simplest, cross-browser compatible way is to implement a border is to use a rect exactly as I did, but place it outside of the group, as mentioned by #Duopixel in his comment. As it is still positioned by the bounding box, it will have the correct width, height, x, and y.
<rect></rect>
<g></g>
I want to access the parent element of current element
Here is the structure of HTML
svg
g id=invisibleG
g
circle
g
circle
g
circle
Basically I want to add text inside the circles when I hover over them.
So I want something like this on hover of any particular circle
svg
g id=invisibleG
g
circle --> radius is increased and text presented inside that
text
g
circle
g
circle
On hover I can select current element through d3.select(this),How can I get root element(g in my case)?
You can use d3.select(this.parentNode) to select parent element of current element. And for selecting root element you can use d3.select("#invisibleG").
To get the root element g (as cuckovic points out) can be got using:
circle = d3.select("#circle_id");
g = circle.select(function() { return this.parentNode; })
This will return a d3 object on which you can call functions like:
transform = g.attr("transform");
Using
d3.select(this.parentNode)
will just return the SVG element. Below I have tested the different variants.
// Variant 1
circle = d3.select("#c1");
g = d3.select(circle.parentNode);
d3.select("#t1").text("Variant 1: " + g);
// This fails:
//transform = d3.transform(g.attr("transform"));
// Variant 2
circle = d3.select("#c1");
g = circle.node().parentNode;
d3.select("#t2").text("Variant 2: " + g);
// This fails:
//transform = d3.transform(g.attr("transform"));
// Variant 3
circle = d3.select("#c1");
g = circle.select(function() {
return this.parentNode;
});
transform = d3.transform(g.attr("transform"));
d3.select("#t3").text("Variant 3: " + transform);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<html>
<body>
<svg height="200" width="300">
<g>
<circle id="c1" cx="50" cy="50" r="40" fill="green" />
</g>
<text id="t1" x="0" y="120"></text>
<text id="t2" x="0" y="140"></text>
<text id="t3" x="0" y="160"></text>
</svg>
</body>
</html>
I'm having a problem with the SVG checkintersection() function. All I want to do is to check whether a small SVG-rectangle intersects the area of an SVG-path, but I can't figure out what to call the function on (I already tried to call it on the SVG DOM object, among several other things google turned up).
So what I need to know is what to put in for the placeholder ("foo") in this snippet:
var closedPath = document.getElementById(closedPath);
var rectangle = document.getElementById(rectangle);
if (foo.checkIntersection(closedPath, rectangle)) {
//do stuff
};
with the HTML being something along the lines of
<html>
<body>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svgroot">
<g>
<path id="closedPath" fill="{$c5}" d="M-250179-46928l-5051 1351l-867-1760l-33-146l-12-99l-82-678l-17-249l86-644l305-1800l158-2882l75-1425l-47-280l-22-131l-137-411l-300-892l1273 620l931-109l1957-734l1860-1096l292-192l884 547l2690 2153l480 963l36 244l-948 1878l-376 591l-60 567l-72 1147l97 847l-222 334l-122 117l-2403 2093l-353 76z"/>
<rect id="rectangle" fill="white" x="-126828" y="0" width="45000" height="45000"/>
</g>
</svg>
</body>
</html>
Any help would be much appreciated!
Edit: Just wanted to add that I now use a workaround, which consists of converting the SVG path to an array of point coordinates using a parser function I wrote, which is then put into a simple coordinate-test function.
Also this may have been a solution Hit-testing SVG shapes?
checkIntersection is a method on the <svg> element so you'd want something like this...
var svg = document.getElementById("svgroot");
var closedPath = document.getElementById(closedPath);
var rectangle = document.getElementById(rectangle);
var rect = svg.createSVGRect();
rect.x = rectangle.animVal.x;
rect.y = rectangle.animVal.y;
rect.height = rectangle.animVal.height;
rect.width = rectangle.animVal.width;
svg.checkIntersection(closedPath, rect) {
// do stuff
}
Note also how the second argument has to be an SVGRect and not an element.
SVG elements support SMIL animation, you could equally well write rectangle.baseVal.x etc but that wouldn't necessarily reflect the rectangle's current position if you were animating the rectangle. If you're not using SMIL then rectangle.baseVal.x = rectangle.animVal.x
Because a <rect> can have things like rounded corners it doesn't have an SVGRect interface so you have to convert from the interface it does have (SVGRectElement) to the one you need (SVGRect)
<svg width="390" height="248" viewBox="-266600, -68800, 195000, 124000" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path id="closedPath" fill="#ff9966" d="M-250179-46928l-5051 1351l-867-1760l-33-146l-12-99l-82-678l-17-249l86-644l305-1800l158-2882l75-1425l-47-280l-22-131l-137-411l-300-892l1273 620l931-109l1957-734l1860-1096l292-192l884 547l2690 2153l480 963l36 244l-948 1878l-376 591l-60 567l-72 1147l97 847l-222 334l-122 117l-2403 2093l-353 76z"/>
<rect id="rectangle" fill="#66ff66" x="-126828" y="0" width="45000" height="45000"/>
</svg>
<script>
var rectangle = document.getElementById('rectangle');
var closedPath = document.getElementById('closedPath');
var svgRoot = closedPath.farthestViewportElement;
var rect = svgRoot.createSVGRect();
rect.x = rectangle.x.animVal.value;
rect.y = rectangle.y.animVal.value;
rect.height = rectangle.height.animVal.value;
rect.width = rectangle.width.animVal.value;
var hasIntersection = svgRoot.checkIntersection(closedPath, rect);
console.log(hasIntersection);
</script>