SVG Won't show until I edit the element in Chrome developer tools - svg

I'm creating a dom structure which includes an SVG element:
<div data-bind="with: searchable_select.f0000001" style="verticalAlign: top">
<input data-bind="value: select_filter_value, valueUpdate: 'afterkeydown', event: { change: change_filter_, blur: blur_filter_ }" style="display: none">
<div>
<select data-bind="options: select_list, value: working_value, event: { change: change_selector_ }, optionsText: 'label', optionsValue: 'value'" style="display: inline-block; maxWidth: 150px">
<option value="person_full_name_asc">Member Full Name (A-Z)</option>
<option value="person_full_name_desc">Member Full Name (Z-A)</option>
</select>
<svg style="display: inline-block; verticalAlign: middle" width="18" height="18" xmlns="http://www.w3.org/2000/svg" version="1.1">
<g>
<circle cx="6" cy="6" r="5" fill="#AAAAAA" stroke="#000000" stroke-width="2"></circle>
<path fill="#AAAAAA" stroke="#000000" stroke-width="2" d="M10,10 L17,17"></path>
</g>
</svg>
</div>
</div>
The svg element does not actually display. When I find the svg element in Chrome developer tools, both width and height show as zero.
I've tried removing the style attribute. I've tried setting the width and height in the style attribute.
I've copied the svg to a separate HTML file:
<html>
<head><title>maggen</title></head>
<body>
<svg style="display: inline-block; verticalAlign: middle" width="18" height="18" xmlns="http://www.w3.org/2000/svg" version="1.1">
<g>
<circle cx="6" cy="6" r="5" fill="#AAAAAA" stroke="#000000" stroke-width="2"></circle>
<path fill="#AAAAAA" stroke="#000000" stroke-width="2" d="M10,10 L17,17"></path>
</g>
</svg>
</body>
</html>
Where it displays fine.
If I wrap the svg element in a div, the svg element shows up. In fact, if I edit the HTML in Chrome developer tools it will show up.
So, yeah, why? I've done a search on Google for this, but either it isn't there or (more likely) my Google Fu is not up to the task. I mean, sure, I can wrap it in a div - and maybe that's the right thing to do - but I'd rather not because then I'd need to wrap other stuff in divs and the dom will start to get cluttered.
EDIT: On a hunch I tried editing viewport attribute into the SVG element in Chrome tools. Voila! The element is visible! Expect when I included the viewport attribute when creating the document, it's not visible. So I tried just adding a random attribute in Chrome tools to the SVG element. Voila! The Element is visible! So, I thought, the problem is specific to Chrome and tried running it in Firefox...
...where the element doesn't show up.
EDIT: Great, so wrapping it in a div is not guaranteed to make it show up. But doing an "edit as HTML" in Chrome developer tools does make it show up.
EDIT: Well, I've gotten it to work correctly and, yes, it turns out to be a function of the Javascript creating the DOM elements. There's a lot of stuff in the code, but I can boil it down to this:
This code works (createElement creates a tag and sets attributes based on passed in parameters):
var div = this.createElement(
'div',
element_name + '_div',
null, style_options, null, null
);
div.innerHTML = [
'<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width=' + width + ' height=' + height + '>',
svg_xml.join(''),
'</svg>'
].join('');
if (parent) parent.appendChild(div);
return div;
This code doesn't:
var svg = this.createElement('svg', element_name, null, style_options, classlist, {
viewport: '0 0 ' + width + ' ' + height,
version: "1.1",
xmlns: "http://www.w3.org/2000/svg",
width: width,
height: height
});
svg.innerHTML = svg_xml.join('');
if (parent) parent.appendChild(svg);
return svg;
So, sure, I've got something working now, but I don't understand why. Or more to the point, I don't understand why one way works and the other doesn't. I have a couple of guesses, but they are just wild guesses, really.

"I've got something working now, but I don't understand why."
The issue is that the methods you were using do not create an SVG element in the SVG namespace. The xmlns attribute only affects the behaviour of an XML parser, not of DOM methods.
I'm not sure which library you're using for the this.createElement() method with multiple parameters. However, I suspect it probably starts by calling the basic document.createElement(tagName) method. This is what MDN says about the standard DOM createElement method:
In an HTML document creates the specified HTML element or HTMLUnknownElement if the element is not known. ... In other documents creates an element with a null namespaceURI.
In other words, because you're (presumably, indirectly) calling createElement on an HTML document, it always creates an HTML element. An HTML element with tag name "svg" is just treated as an unknown span-type element.
In contrast, using div.innerHTML to pass a markup string creates the SVG element correctly because it invokes the HTML5 parser to figure out what type of element to create. The namespace is determined using the same rules as when parsing markup from a file. Editing the HTML in the Chrome developer tools has the same effect.
Sidenote: Avoid calling .innerHTML on an SVG element. Some browsers support it, but it's not part of the specs. You're not getting an error because your svg variable is actually an instance of HTMLUnknownElement. Passing SVG code to the innerHTML method of a parent <div> usually works, although there are some bugs with SMIL animation. As #Robert Longson says in the comments, you can use the DOMParser object to parse either HTML or XML code into a document.
The other way to dynamically create an SVG element is to use document.createElementNS(namespaceURI, tagName). You'll also have to use this method to create all the child elements of the SVG. Once they are created, you may be able to set attributes, styles, and classes using your library methods. (But you haven't specified what library you're using, so I'm not sure.)

Related

SVG: Dragging objects between two different SVG elements

I'm developing a prototype to mock this tool and provide minimal functionality like
Drag-n-Drop simple objects
Connect related objects via paths
Generate JSON from this structure
I opted SVG and Snap.svg framework (had hard time in deciding between D3 and Snap.svg but ended up with latter just because it is latest and successor of Raphael) to implement it. I stuck with below during implementation
Question 1: How to drag objects between different SVG elements? Try dragging elements in this Fiddle, objects were hidden when dragged outside of its parent dimension. Fiddle snippet below for your perusal.
HTML/SVG:
<div class="stencil">
<svg id="stencil" height="300" version="1.1" width="120" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
</svg>
</div>
<div class="paper">
<div class="paper__scroller">
<svg id="paper" height="1000" version="1.1" width="1000" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
</svg>
</div>
</div>
Javascript:
(function(){
var pap = Snap("#paper"),
stencil = Snap("#stencil"),
cir = stencil.circle(30, 50, 20),
rec = stencil.rect(70, 33, 35, 35);
stencilElements = stencil.group(rec, cir);
stencilElements.attr({
fill: "#f00",
stroke: "#000",
strokeWidth: 2,
"fill-opacity": 0.5
});
cir.clone().drag();
rec.clone().drag();
}());
Question 2: How to connect related objects via paths that adjust/moves accordingly when the object is dragged?
Question 3: How to convert the generated graph/diagram to JSON structure?
Really appreciate any reference or hints in implementing this.

D3 and SVG namespaces for custom svg elements or attributes to be valid?

I created some custom svg attributes yet want to pass the svg validator test.
I saw the D3 > Namespace page, the previous How can I specify a custom XML/SVG namespace with D3.js? and thus processed as follow:
// d3.ns.prefix.geo = "http://www.example.com/boundingbox/";
// d3.ns.prefix.inkscape = "http://www.inkscape.org/namespaces/inkscape";
// SVG injection:
var width = 600;
var svg = d3.select("#hook").append("svg")
.attr("name", "Country's_name_administrative_map_\(2015\)")
.attr("id", "Country_s_name")
.attr("width", width)
.attr(':xmlns:geo','http://www.example.com/boundingbox/')
.attr(':xmlns:inkscape','http://www.inkscape.org/namespaces/inkscape')
.attr(":xmlns:cc","http://creativecommons.org/ns#");
// Tags:
svg.append(":geo:g")
.attr(':xmlns:geo','http://www.example.com/boundingbox/')
.attr(":geo:id","geo")
.attr(':geo:syntax', "WSEN bounding box in decimal degrees")
.attr(':geo:west', WEST)
.attr('geo:south', SOUTH)
.attr(':geo:east', EAST)
.attr(':geo:north', NORTH)
.attr(':geo:title', title);
Produce :
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg"
name="Country's_name_administrative_map_(2015)"
id="Country_s_name"
xmlns:geo="http://www.example.com/boundingbox/"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:cc="http://creativecommons.org/ns#"
width="600"
height="579.2009758421691"
version="1.1">
<defs xmlns="http://www.w3.org/1999/xhtml">
<style type="text/css"><![CDATA[
svg { border: 1px solid rgb(100, 100, 100); }]]>
</style>
</defs>
<geo:g xmlns:geo="http://www.example.com/boundingbox/"
geo:id="geo"
geo:syntax="WSEN bounding box in decimal degrees"
geo:west="-5.8"
south="41"
geo:east="10"
geo:north="51.5"
geo:title="Country's name" />
<defs><pattern id="hash2_4" width="6" h…
…
</svg>
I still get all the errors (larger image) :
The first type of error is related to the custom <geo:g … > element itself, visible above. The 2nd type of errors is related to custom attributes such geo:west="…" or inkscape:group="…", which I expected to be valid due to the earlier xmlns declarations.
Am I walking the wrong road ? How to make custom attribute valid via d3js ?
EDIT: a minimal jsfiddle provide a demo of the buggy output.
About d3js and namespaces. According to Selvin on D3 doesn't append xmlns:something namespace properly to svg element, it's a D3js know bug with known but not yet implemented solution. Also, current way to overcome it is via JQuery
I made some mildly successful tries.
1) Reading: To produce valid svg document which could be saved, #Selim pointed out that d3.ns.prefix is NOT suitable (see d3 doesn't append namespace attributes to svg element ).
2) Reading more: So I read Namespaces Crash Course. The key point is that
[...] namespace prefixes are used to prefix attribute names and tag [element] names [...]
From the official doc with declaration and usages, for the attributes (<script> changed into <a>) :
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<a xlink:href="space-rocket.html">...</a>
</svg>
and for elements:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:svg="http://www.w3.org/2000/svg">
<body>
<h1>SVG embedded inline in XHTML</h1>
<svg:svg width="300px" height="200px">
<svg:circle cx="150" cy="100" r="50" fill="#ff0000"/>
</svg:svg>
</body>
</html>
2) Porting: So I went ahead, NOT d3.ns.prefix, but with the handmade custom namespace ("geo") declaration as an attribute to the svg element via :
d3.select("svg").attr(":xmlns:geo","http://www.example.com/something/")
together with usages such
svg.append("geo").attr("id","geo")
.attr('manual', "WSEN bounding box in decimal degrees")
.attr('WEST', "-4.05")
.attr('SOUTH', "40.5")
.attr('EAST', "10.0")
.attr('NORTH', "54.5")
.attr('Title', "Imaginary map of Kinglons ruling Europe");
or
svg.append("geo").attr("id","geo")
.attr(':geo:manual', "WSEN bounding box in decimal degrees")
.attr(':geo:WEST', "-4.05")
.attr(':geo:SOUTH', "40.5")
.attr(':geo:EAST', "10.0")
.attr(':geo:NORTH', "54.5")
.attr(':geo:Title', "Imaginary map of Kinglons ruling Europe");
or
svg.append(":geo:geo").attr("id","geo")
.attr(':geo:manual', "WSEN bounding box in decimal degrees")
.attr(':geo:WEST', "-4.05")
.attr(':geo:SOUTH', "40.5")
.attr(':geo:EAST', "10.0")
.attr(':geo:NORTH', "54.5")
.attr(':geo:Title', "Imaginary map of Kinglons ruling Europe");
and about 3 other variants, but none is valid, all fails the svg validator.
I have no more ideas how to get a valid svg.
Fiddle (downloadable) , svg validator

SVG (1.1) : how to 'link-to' or 'centre' on a shape in a browser?

Is there is browser-independant way getting the browser to centre on a particular shape (by 'id' attribute) ?
I have tried using xlinks wrapped around shapes like this:
<a xlink:href="#node24"> .... </a>
I have reasonably busy (100+ shapes) directed graph diagrams (generated from dot): and when I load them up in Chrome , more often than not, the intial screen is just blank - forcing the user to use scrollbars to find the diagram at all.
I'm afraid I don't have any good news for you.
For stand-alone SVG documents, you can manipulate the part of an SVG displayed when following a link by linking to a <view> element (distinct from, but making use of, the SVG "viewBox" attribute). The view element specifies the viewBox to use and possibly some other parameters, and the graphic will be displayed with those parameters instead of the default ones.
Example code:
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMin meet" >
<circle cx ="50" r="40"/>
<view id="panUp" viewBox="0 -50 100 100" />
<view id="zoomIn" viewBox="25 25 50 50" />
</svg>
If you linked to the file as a whole it would show you an image with half a circle centered at the top of the screen.
If, however, you linked to it like http://example.com/sample.svg#panUp, the circle would be the same size but centered on screen. If you linked to http://example.com/sample.svg#zoomIn, you'd only see the bottom edge of a circle that is twice as big.
(I don't have anywhere to host the file that can serve up raw SVG files, but this CodePen uses data URI to show the effects, although the data URI fragment identifiers doesn't seem to work in Firefox.)
You are supposed to be able to even specify the desired viewBox, transforms, or other attributes as part of the URL fragment (like http://example.com/sample.svg#myView(viewBox(0,0,200,200))), but I don't think that's widely implemented -- it had no effect on either Firefox or Chrome.
And even <view> fragments don't seem to work when the SVG is embedded within an HTML document. So unless your SVG is stand-alone, creating a view for each element (or one view that your dynamically change to match the clicked element), isn't going to be worth the trouble.
So what does work?
The default behaviour, when linking to a fragment (element id) that is not a <view> is to display the nearest ancestor <svg> element that contains that element ("nearest ancestor" because an SVG can contain nested <svg> tags). So if your document has a natural structure to it, you could replace some <g> elements with <svg> with a specified x,y,height and width parameter, and then linking to an element within that sub-graphic would show that view. That should work even when the SVG is embedded within a larger HTML document. But if you've got hundreds of elements moving around, it's probably not a practical solution.
Which leaves #Ian's solution of programmatically manipulating the main SVG viewBox. If you don't want to zoom in, just pan, leave the width and height as the full size of your visualization, and just change the x and y offsets. Something like:
function centerViewOnElement( el ) {
var bbox = el.getBBox()
var elCenterX = bbox.x + bbox.width/2,
elCenterY = bbox.y + bbox.height/2;
svg.setAttribute("viewBox", [(elCenterX - width/2),
(elCenterY - height/2),
width,
height
].join(" ") );
//assuming you've got the svg, width and height already saved in variables...
}
Thought I would do a simpler example, as this feels quite useful in general...with a jsfiddle here
<svg id="mySvg">
<circle id="myCirc" cx="20" cy="20" r="20"/>
<rect id="myRect" x="50" y="50" width="50" height="50"/>
</svg>
var mySvg = document.getElementById("mySvg");
function getNewViewbox( el ) {
var bbox = el.getBBox();
return newViewbox = bbox.x + " " + bbox.y + " " + bbox.width + " " + bbox.height;
}
function focusElement( ev ) {
ev.stopPropagation();
mySvg.setAttribute("viewBox", getNewViewbox( ev.target ) );
}
//click on any element, or even the svg paper
document.getElementById("mySvg").addEventListener("click", focusElement);

How to append a text element with inline tspan children?

Starting with a DOM that already contains something like
<svg id="svg0" width="600" height="300" xmlns="http://www.w3.org/2000/svg" version="1.1">
</svg>
...I want to programmatically modify the element in d3.select("#svg0") so that I end up with
<svg id="svg0" width="600" height="300" xmlns="http://www.w3.org/2000/svg" version="1.1">
<text x="20" y="20">
Lorem ipsum
<tspan style="alignment-baseline:text-before-edge">dolor</tspan>
sit amet</text>
</svg>
This is as far as I can get:
var $svg = d3.select("#svg0");
$svg.append("text").text("Lorem ipsum ")
.attr({x:"20", y:"20"});
It looks as though the rest should be easy, but I've spent the last two hours trying all the "obvious" things to finish this without success.1
What does one have to do to finish the task described above?
1I've tried far too many things to describe them all. Suffice it to say that the text method, when used as a setter, wipes out whatever textContent the text object had before. This means that, effectively, this method can be called only once, which precludes solutions relying on calling .text(...) a second time to add the " sit amet" fragment.)
Normally you would think to use the html function for this, but from the docs:
Note: as its name suggests, selection.html is only supported on HTML
elements. SVG elements and other non-HTML elements do not support the
innerHTML property, and thus are incompatible with selection.html.
Consider using XMLSerializer to convert a DOM subtree to text. See
also the innersvg polyfill, which provides a shim to support the
innerHTML property on SVG elements.
Here's with the polyfill: http://jsfiddle.net/GNGF5/
And if you don't want to do that, you can hack it up using multiple tspan elements w/ a transform, as seen here: http://jsfiddle.net/cAuCM/
var $svg = d3.select("#svg0");
var $text = $svg.append("text");
var $tspan1 = $text.append('tspan');
var $tspan2 = $text.append('tspan');
var $tspan3 = $text.append('tspan');
$text.attr('transform', 'translate(0, 18)');
$tspan1.text('Lorem ipsum');
$tspan2.text('dolor').style('alignment-baseline', 'text-before-edge');
$tspan3.text('sit amet');
Here's how to do it with Snap.svg:
var paper = Snap("#svg0");
var t1 = paper.text(50, 50, "Snap");
var t2 = paper.text(50, 70, ["S","n","a","p"]);
<script src="https://cdn.jsdelivr.net/snap.svg/0.1.0/snap.svg-min.js"></script>
<svg id="svg0" width="600" height="300" xmlns="http://www.w3.org/2000/svg" version="1.1">
</svg>

D3: Select and alter external SVG?

Is it possible to select and alter elements in an embedded (external) SVG , created in Adobe Illustrator?
html:
<object data="circles.svg" type="image/svg+xml" id="circles"></object>
circles.svg:
<svg xmlns="http://www.w3.org/2000/svg" width="100px" height="100px" >
<circle id="c_red" fill="#A00" stroke="#000" cx="40" cy="40" r="40"/>
<circle id="c_grn" fill="#0A0" stroke="#000" cx="60" cy="60" r="40"/>
</svg>
d3 code:
<script>
var my_circles = d3.select("#circles svg").selectAll("circles");
my_circles.attr("fill", "black");
</script>
Otherwise, I'm open to other ways of doing this. For example, something like this might work to select (which does indeed locate the SVG):
var svg = document.getElementById('circles');
But how to then parse and alter in D3?
Bonus question: best way to debug D3 selectors?
This is actually a nasty case, because you can't use DOM selectors directly on embedded documents. In principle, the selector you need is "#circles > circle", but this won't work in this case. So you need something rather ugly like
var my_circles = d3.select(document.getElementById("circles").contentDocument)
.selectAll("circle");
I find the Javascript console quite useful for debugging selectors. Just type in what you want to test and see if the things you want are returned.
The problem is that the above code only works once the object has been loaded. Even using something like JQuery's .ready() won't be sufficient to ensure that. A quick and dirty solution is to repeatedly check whether the elements are present until they are:
function changeColor() {
var sel = d3.select(document.getElementById("circles").contentDocument)
.selectAll("circle");
if(sel.empty()) {
setTimeout(changeColor, 100);
} else {
sel.attr("fill", "black");
}
}
changeColor();
Full example here.

Resources