D3 - Create Dynamic "Border" Rectangle around SVG group - javascript

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>

Related

How to catch SVG click events that hit the viewbox, rather than elements on it?

I have an SVG file with various paths, it is embedded into an HTML page using the object tag. Javascript is used to provide some interactivity to each path - when it is clicked, a tooltip rect is shown. This is what it looks like:
I want the tooltip to disappear when someone clicks outside of the path the tooltip is associated with, this is implemented by adding such an event listener to every path:
path.addEventListener("click", function(event){
if (!isTipShown()){
createTooltip()
}
else{
hideTooltip()
}
})
isTipShown, createTooltip and hideTooltip are functions that check the SVG DOM and modify it accordingly.
This works, but it fails if the click goes to the empty space between the paths themselves - because there is no object to catch it.
What approach can be chosen to implement such functionality?
My current thoughts:
Create a transparent rectangle that covers the entire viewport, and use that as a click target. How would one ensure the rectangle goes to the bottom of everything?
A click handler for the entire HTML document does the trick, but only if users click outside of the viewport itself.
Tooltip on shapes not my SVG!
How to best remove the tooltip when pressing the svg?
They way i would solve it is:
ID on the tooltip.
Modify the existing tooltip with the ID.
Remove tooltip with ID when pressed anywhere else
By reusing the tooltip, there can only be one tooltip on the page at one time.
Removing the tooltip (not deleting) makes it possible to reuse the same tooltip again when a new path is presed.
Here is an example:
document.addEventListener("DOMContentLoaded", function() {
var pathRed = document.getElementById("red");
var pathOrange = document.getElementById("orange");
var pathBlue = document.getElementById("blue");
var paths = [pathRed, pathOrange, pathBlue];
var toolTip = document.createElement("div");
toolTip.id = "toolTip";
var svg = document.getElementById("box");
var shown = false;
paths.forEach(function(element) {
element.addEventListener("click", function(event) {
if (shown == false) {
toolTip.innerText = element.id;
toolTip.style.top = (event.pageY) + "px";
toolTip.style.left = (event.pageX) + "px";
document.body.appendChild(toolTip);
shown = true;
//Only click the path
event.stopPropagation();
} else {
removeToolTip();
}
});
});
svg.addEventListener("click", function(event) {
removeToolTip();
event.preventDefault();
});
function removeToolTip() {
shown = false;
if (document.body.contains(toolTip)) {
document.body.removeChild(toolTip);
}
}
});
#toolTip {
position: absolute;
background-color: #00000099;
padding: 2px;
border-radius: 2px;
color: white;
font-size: 20px;
}
<h1>Click the boxes!</h1>
<svg id="box" viewBox="0 0 15 15" width="250px">
<path id="red" fill="red" d="m0,0 5,0 0,5 -5,0Z"/>
<path id="orange" fill="orange" d="m5,5 5,0 0,5 -5,0Z"/>
<path id="blue" fill="blue" d="m10,10 5,0 0,5 -5,0Z"/>
</svg>
The solution is to make sure the rectangle goes below the paths, as if it were a bottom layer.
SVG does not have a concept of layers, but it can be achieved by making sure that the rect is before all the elements in the SVG DOM, and all subsequent elements will be placed on top of it, visually:
<rect x="0" y="0" width="30" height="30" fill="purple"/>
<rect x="20" y="5" width="30" height="30" fill="blue"/>
<rect x="40" y="10" width="30" height="30" fill="green"/>
<rect x="60" y="15" width="30" height="30" fill="yellow"/>
<rect x="80" y="20" width="30" height="30" fill="red"/>
Here is how this was accomplished in practice (the svgDoc variable is the root SVG element):
function createBackgroundRectangle(svgDoc){
var rect = svgDoc.createElementNS("http://www.w3.org/2000/svg", 'rect')
rect.setAttributeNS(null, 'height', 500)
rect.setAttributeNS(null, 'width', 900)
rect.setAttributeNS(null, 'id', 'pseudo-background')
rect.setAttributeNS(null, 'x', 0)
rect.setAttributeNS(null, 'y', 0)
// the opacity is set to 0, so it doesn't get in the way visually. For debugging
// purposes, you can change it to another value and see the actual rectangle
rect.setAttributeNS(null, 'style', 'opacity:0;fill:#ffd42a;fill-opacity:1;')
svgDoc.rootElement.insertBefore(rect, svgDoc.rootElement.children[0])
}
svgDoc.rootElement.insertBefore(rect, svgDoc.rootElement.children[0]) makes it the first, as it is inserted before the current child at index 0.

How to draw Polyline object shaped exactly same with the given image data on SVG

Doughnut picture
For example, given image like above, what I want to do is draw the exact same shaped polyline object on SVG(Im creating a 'drawing' or should I say 'brush' tool based on SVG and that is why Im using polyline so that user can paint with his mouse or can even use eraser with his or hers mouse). And following is how I would achieve this.
draw the given image on canvas context.
get all the coordinates of pixel that is colored #000000.
with that list of coordinates create a Polyline on SVG.
and by this process I get this as a result doughnut drawin with svg polyline(now this is just an example result that it is formed ugly because I had to draw it manually with my hand. But my purpose is to get same shaped with an input image)
But I'm not sure if this is the only way or even not sure if I should stick with SVG. Are there any other good ways to achieve this? or would using canvas instead of SVG make it easier?
This shape can be drawn with circles.
Cutouts made using a mask composed of circles
<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="405" height="401" viewBox="0 0 405 401" >
<defs>
<mask id="msk1" >
<rect width="100%" height="100%" fill="white" />
<g fill="black">
<circle cx="202" cy="200" r="40" />
<circle cx="260" cy="298" r="40" />
<circle cx="215" cy="303" r="20" />
</g>
</mask>
</defs>
<circle cx="202" cy="200" r="98" fill="black" mask="url(#msk1)" />
This is supposing that you already have an SVG path.
In order to draw a polygon you will need to split your path by the M commands. In the next example I did it manually but you can do it dynamically. This is important because otherwise you'll get a split in the polygon.
You will also need to set a precision, meaning the distance between the points of the polygon.
Please read the comments in my code.
let paths = document.querySelectorAll("#theGroup path");
let precision = 5;//how far one of other the points of the polygon are
let points = [];// the array of points
// for each path in the array of paths
paths.forEach(p=>{
// get the total length
let totalLength = p.getTotalLength();
// get the number of points for the polygon in base of the precision
let numPoints = ~~(totalLength / precision);
// calculate the segment length
let segmentLength = totalLength / numPoints;
for(let i = 0; i <= numPoints; i++ ){
let point = p.getPointAtLength(i*segmentLength);
// get the coordinates of this point and push it
points.push(point.x);
points.push(point.y);
}
})
//set the value for the points attribute of the polygon
poly.setAttributeNS(null,"points", points.join())
svg{border:1px solid; width:90vh;}
path{fill:none}
<svg viewBox="0 0 531 531">
<g id="theGroup">
<path id="donut" d="M268.64,76.066c70.065,2.632,125.154,32.347,163.73,91.372
c14.944,22.864,23.47,48.161,27.698,75.22c3.987,25.512,2.188,50.551-3.64,75.354c-4.821,20.522-13.383,39.648-24.866,57.406
c-2.003,3.099-3.899,3.396-7.365,1.548c-30.011-16.005-64.509-10.767-87.731,14.13c-6.295,6.748-9.985,15.893-15.108,23.783
c-1.548,2.384-3.508,5.256-5.938,6.189c-19.202,7.375-32.874,20.547-41.279,39.064c-1.911,4.211-4.254,5.562-8.308,5.085
c-13.198-1.554-26.507-2.515-39.562-4.873c-30.46-5.502-57.275-19.262-81.055-38.724c-28.703-23.491-49.496-52.646-61.424-88.046
c-7.479-22.198-11.34-44.892-10.42-68.225c2.042-51.761,20.944-96.305,57.854-133.023c22.272-22.156,48.427-37.859,78.3-47.183
C228.671,79.17,248.365,75.884,268.64,76.066z"/>
<path id="hole" d="M340.466,271.259c0.179-40.212-32.175-73.14-72.067-73.348
c-40.072-0.208-73.264,32.326-73.485,72.032c-0.226,40.441,32.218,73.372,72.436,73.522
C307.646,343.616,340.286,311.382,340.466,271.259z"/>
</g>
<polygon id="poly" fill="gold" points = "" />
</svg>

How to zoom in on a polygon in D3.js

I am trying to figure out how to do the same zooming behavior as shown in the example below, but with a normal polygon instead of the geo paths.
https://bl.ocks.org/mbostock/4699541
I have seen some answers here on SO that kind of address this, but the animation is choppy or jumps around strangely.
The html I have is
<div id="map-container">
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg"
id="canvas"
viewBox="0 0 4328 2880">
<defs>
<pattern id="mapPattern"
patternUnits="userSpaceOnUse"
x="0"
y="0"
width="4328"
height="2880">
<image xlink:href="/development/data/masterplan.png"
x="0"
y="0"
width="4328"
height="2880"></image>
</pattern>
</defs>
<g id="masterGroup">
<rect fill="url(#mapPattern)"
x="0"
y="0"
width="4328"
height="2880" />
</g>
</svg>
I would like to be able to add some polygons in the same group as the map rectangle and then zoom on the polygon's boundary. Can anyone please show me a fiddle of such behaviour?
I should also add that I do not want to use the scroll wheel or panning. Just zooming in on a clicked polygon and then zooming out on another click.
Maybe this will help you. I answered a question here earlier today : D3js outer limits
Here is the fiddle I put together : http://jsfiddle.net/thatOneGuy/JnNwu/921/
I have added a transition : svg.transition().duration(1000).attr('transform',function(d){
Notice if you click one of the nodes the area moves to cater for the size of the new layout.
The basics are explained in the link to the question, but basically I got the bounding box and translated the SVG accordingly. So I translated and scaled to the size of the new rectangle.
Take a look, quite easy to understand. Here is the main part of the transition :
svg.transition().duration(1000).attr('transform',function(d){
var testScale = Math.max(rectAttr[0].width,rectAttr[0].height)
var widthScale = width/testScale
var heightScale = height/testScale
var scale = Math.max(widthScale,heightScale);
var transX = -(parseInt(d3.select('#invisRect').attr("x")) + parseInt(d3.select('#invisRect').attr("width"))/2) *scale + width/2;
var transY = -(parseInt(d3.select('#invisRect').attr("y")) + parseInt(d3.select('#invisRect').attr("height"))/2) *scale + height/2;
return 'translate(' + transX + ','+ transY + ')scale('+scale+')' ;
})
So with your code, your rectAttr values as seen in the snippet above would be the values retrieved from the getBoundingClientRect() of your polygon : x, y, width and height.
Where I have used d3.select('#invisRect'), this should be your boundingBoxRect() also. And the rest should just work.
EDIT
Here are the edits I made with the fiddle provided : http://jsfiddle.net/thatOneGuy/nzt39dym/3/
I used this function to get the bounding box of the polygon and set the rectangles values accordingly :
var bbox = d3.select(polygon).node().getBBox();
var rectAttr = {
x: bbox.x,
y: bbox.y,
width: bbox.width,
height: bbox.height,
};

How to use the SVG checkintersection() function correctly?

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>

d3.js: binding data to existing svg. what selector.each does?

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.

Categories

Resources