I am parsing an xml string and display it as a tree in d3.js. I can build the whole tree in the 'enter' step, but now I want to make it interactive and pull out the configuration into an update step. I'm following https://stackoverflow.com/a/24912466 to implement the general update pattern but I can't seem to set the attribute to the g element outside the enter step:
const svg = d3.select("#canvas");
const xmlAsText = `
<root attrib1="foo" att2="foO">
<child1 attrib1="ba">zwei</child1>
<child2>eins</child2>
</root>`;
treeDataXml = (new DOMParser()).parseFromString(xmlAsText, "text/xml");
let hierarchy = d3.hierarchy(treeDataXml.children[0], d => d.children);
nodesData = d3.tree()(hierarchy);
var myGroups = svg.selectAll("g").data(nodesData.descendants());
myGroupsEnter = myGroups.enter().append("g")
// 1) works
//myGroupsEnter .attr("class", "x");
// 2) doesn't work
myGroups.select("g").attr("class", "x");
console.log(document.getElementById("canvas").childNodes[0].attributes[0])
alert(document.getElementById("canvas")
.childNodes[0].attributes.length)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg id="canvas"></svg>
with 1) I get
<body>
<svg id="canvas">
<g class="x"></g>
<g class="x"></g>
<g class="x"></g>
</svg>
</body>
but with 2) it's just:
<body>
<svg id="canvas">
<g></g>
<g></g>
<g></g>
</svg>
</body>
I would expect that with myGroups.select("g").attr("class", "x") the previously entered <g>s will all be selected and have their attribute class set to "x". Why doesn't this work? How can I fix this?
Selections are immutable, myGroups is an empty selection: it doesn't contain any DOM elements yet. As this selection is the update selection, this makes sense, there is nothing to update.
The selection myGroupsEnter contains the newly created elements. Entering elements does not modify myGroups, this remains an empty selection. This explains why myGroupsEnter.attr("class", "x"); works and myGroups.select("g").attr("class", "x"); does not.
D3 v3 and earlier added newly entered elements in the update selection, which is why some examples might be misleading, but as this behavior was not explicit it was removed
Often you want to combine the entered and updated elements into one selection, you can use:
let combined = myGroups.merge(myGroupsEnter);
This way regardless of whether you are entering all elements, updating all elements, or entering some and updating others you can modify all elements that exist (that are not exited).
I know this has been answered before but no solutions are working for me. I want to render an svg file on an html page and access its code elements from javascript. I want to change the attributes of specific elements. Specifically I want to change add an attribute style="fill:#FFFFFF;". Later I will want to remove this. I want to access the element with the id property of the element. I tried doing this with a plethora of javascript code and rendering the svg as an img and an object and nothing works. Is this possible? If so how? Thanks in advance.
You can create a react component for your SVG and control their properties with props in react way:
const SampleIcon = ({fill='red',backgroundColor='white', width='300', height='200'}) => (
<svg version="1.1"
width={width} height={height}
xmlns="http://www.w3.org/2000/svg">
<rect width="100%" height="100%" fill={backgroundColor} />
<text x="150" y="125" font-size="60" text-anchor="middle" fill={fill}>SVG</text>
</svg>
)
Now use it:
const App = () => (
<div>
<SampleIcon fill='green' backgroundColor='#ff0'/>
<SampleIcon with={400} height={300}/>
</div>
)
There is no way to change the fill property of an SVG you want to display in an img tag. However, you can change the filter property.
So what you want to do is display the svg file like so:
<img class="svg-icon" src="./your.svg"/>
You have to give your img tag a classname (also, do not set the fill property inline).
Then you can simply apply a filter and the color of the svg will be changed, like so:
.your-icon {
filter: invert(14%) sepia(95%) saturate(6069%) hue-rotate(360deg) brightness(108%) contrast(117%);
}
The above filter is red. Use this hex to filter converter to find the right color for you: hex to filter.
Here is a codesandbox demo.
I'm pretty new to coding (januari,2021) and with a lot of online searching I'm able to get pretty far but at this point I'm pretty stuck.
setTimeout(function(){
var supbtn = document.createElement("BUTTON");
supbtn.innerHTML = "Support";
supbtn.className = "Support-logout-class";
supbtn.onclick = function(){window.open("LinkToSite","_blank")};
document.body.appendChild(supbtn);
}, 2000);
There is a default wordpress plugin that the company that I work for uses, and as we are in the proces of building a custom site for this use case we want to try and upgrade the current.
(With a lot of succes so far)
Basically there is a default code and I want to add the above stated "button" element at a specific place, currently it's adding inside the "body" and that works perfect!
The code where I want to place it:
<div class="wpws-webinar-summary-content sc-iBEsjs sJWcq">
<div class="sc-fZwumE cyVGPD"></div>
<div class="wpws-webinar-summary-logout sc-gmeYpB fOXvtE">
<svg class="wpws-33" focusable="false" viewBox="0 0 24 24" aria-hidden="true" role="presentation" title="Verlaat het webinar"><path fill="none" d="M0 0h24v24H0z"></path>
<path d="M10.09 15.59L11.5 17l5-5-5-5-1.41 1.41L12.67 11H3v2h9.67l-2.58 2.59zM19 3H5c-1.11 0-2 .9-2 2v4h2V5h14v14H5v-4H3v4c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"></path>
</svg>
</div>
</div>
I want the generated button to appear before: class="wpws-webinar-summary-logout"
(why before?, because if I'm right the code "" isn't set, as in it can change between pages. And I know for sure "wpws-webinar-summary-logout" isn't going to change)
But I just can't seem to find the right search term for this,
and when I think I'm close I don't quite seem to understand it yet.
Any, tips, tricks, examples, someone can give to me?
Many thanks in advance!
Gr. Ian
You can use insertBefore() to inject an element before another element, like this for example:
setTimeout(function(){
// Create button element
var supbtn = document.createElement("BUTTON");
supbtn.innerHTML = "Support";
supbtn.className = "Support-logout-class";
supbtn.onclick = function(){window.open("LinkToSite","_blank")};
// Find the element that we'll use as reference to inject our button
var webinar_summary_logout = document.querySelector('.wpws-webinar-summary-logout');
// Get the parent element
var parentDiv = webinar_summary_logout.parentNode;
// Inject the button before our referenced element
parentDiv.insertBefore(supbtn, webinar_summary_logout);
}, 2000);
<div class="wpws-webinar-summary-content sc-iBEsjs sJWcq">
<div class="sc-fZwumE cyVGPD"></div>
<div class="wpws-webinar-summary-logout sc-gmeYpB fOXvtE">
<svg class="wpws-33" focusable="false" viewBox="0 0 24 24" aria-hidden="true" role="presentation" title="Verlaat het webinar"><path fill="none" d="M0 0h24v24H0z"></path>
<path d="M10.09 15.59L11.5 17l5-5-5-5-1.41 1.41L12.67 11H3v2h9.67l-2.58 2.59zM19 3H5c-1.11 0-2 .9-2 2v4h2V5h14v14H5v-4H3v4c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"></path>
</svg>
</div>
</div>
I usually use insertAdjacentElement to have fine control on where to insert elements (there are some shortcuts but I try to always use that one for consistency)
var supbtn = document.createElement("BUTTON");
document.querySelector('.wpws-webinar-summary-logout').insertAdjacentElement('beforebegin', supbtn)
https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement
Select that element and then i assume you have selected it and gave a name like const node = document.queryselector(“something”) node.before(paste element here) Or node.after(element here)
So not sure I'm following 100%. You want to add a programmatically-generated button in div.wpws-webinar-summary-content and before div.wpws-webinar-summary-logout (as the second child of the parent element)?
Several ways to do this:
https://developer.mozilla.org/en-US/docs/Web/API/Element <- check out the Web API - getting familiar with Element (and DOM) traversal within JS is super helpful. Lots of specific ways to implement this by modifying children, innerHTML (not recommended), etc. (Edit: several others answered while I was typing this - some of the other suggestions are also documented there including: https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement!)
Another approach involves adding a placeholder element where you want along with a unique identifier (ID attribute). You can alternately hide (display: none) or otherwise append the desired button into this placeholder element. Assuming your CSS is correct, the placeholder element shouldn't cause too many problems with your surrounding elements (don't give it the same classes, etc.).
Cheers!
I have a map drawn in CorelDRAW that shows the location of some company assets. Each one in shown as a square on the map. My aim is to create an interactive version of the map, where the user can click on a square and be shown more information about the asset.
CorelDRAW allows me to specify a name for each shape. Then, if the diagram is exported as an SVG, the shape names are used for the id attributes.
This gives me a map in SVG format; let's say it looks like this:
<svg width="100" height="100">
<rect id="firstAsset" x="25" y="50" width="20" height="20" />
<rect id="secondAsset" x="50" y="15" width="20" height="20" />
</svg>
I want to be able to click on each rectangle and have information displayed about it. A popup would do, such as this example at w3cschools, or a modal box if there's more information to display.
So, I've started by embedding a test svg in an html document. This doesn't mean it will end up on a web server; the file may just end up saved on a SharePoint. Ideally it would end up a single file that can be distributed by email if need be.
Here is what I have so far, using modal boxes: link to js fiddle.
The problem is, the full SVG is going to have dozens more clickable shapes, and if the map is updated in CorelDRAW, I will have to add the onclick() and class attributes to each shape all over again. I need to be able to take information about each asset in some standard format, and then have it automatically added to each asset in the SVG.
I originally envisioned writing a script that would take the SVG file and a file with the information to display. It would search the SVG for id attributes, check for corresponding information to display, and if so make the element clickable to display that information. This is where I'm stuck. Do I need to write a separate program just to maintain this map? Or could this be done using javascript in the document itself?
To recap, I have:
An automatically generated SVG with id attributes against each asset
I will have some information about each asset, which I can put in html format
and I want to be able to click on an asset on the map and show the information about it.
P.S. I'm new to html and javascript.
This code example might help you a bit to get started.
var rect = document.querySelectorAll('rect');
rect.forEach(item=>{
item.addEventListener('click', tooltip);
});
function tooltip(){
console.log(this.id);
}
I have tweaked your fiddle a little please look
$(function () {
$( "#redButtons,#blueButtons" ).on( "click", "rect", function() {
//alert(this.id);
$( "#theInfo" ).html(data[this.id]);
$( "#modal" ).show();
event.stopPropagation();
});
$( "#close,body" ).on( "click", function() {
$( "#modal" ).hide();
});
})
I believe jQuery will help some. jsFiddle somehow support it, but you can embed jquery if you want a single file, or link it from separate file or internet.
The solution I have is in this js fiddle. It uses pure javascript, and the svg can be pasted in unmodified from the graphics application.
Data about each asset is stored in div tags like this:
<!--Information about each asset, that is added to the modal before it's displayed -->
<div id="html_asset1" class="hiddenInfo" data-asset="asset1" data-category="redCategory">
<p>This is some information about asset one.</p>
</div>
The id attribute has to be the same as the corresponding shape in the svg prepended with "html_". The class "hiddenInfo" hides the data. There are two custom attributes. The first is the id attribute of the corresponding shape. The second is just a the name of a class that will be applied to the shape to set the colour.
I then have a little javascript that gets each div element like the above, and uses the custom attributes I just mentioned to turn the corresponding shapes into buttons:
var hiddenInfo = document.getElementsByClassName('hiddenInfo');
var eachAsset;
for (i = 0; i < hiddenInfo.length; i++) {
eachAsset = document.getElementById(hiddenInfo[i].dataset.asset);
eachAsset.className.baseVal += " button " + hiddenInfo[i].dataset.category;
eachAsset.addEventListener('click', displayModal);
}
When any button is clicked, the contents of the modal window is set to the hidden information:
//Populate the information in the modal and then make it visible
function displayModal() {
var hiddenHTMLid = "html_" + this.id
//use the hidden HTML to populate the modal
document.getElementById("theInfo").innerHTML = document.getElementById(hiddenHTMLid).innerHTML;
modal.style.display = "block";
}
Overall it works nicely and keeps the data separate from how it is displayed, which I like.
Thanks for the help.
SVG which was created programmatically does not convert into base64 correctly.
In my application I've got a service which get a response with g-element and then put into created svg-element and convert it into base64, but if I try to open a link I find that svg does not render on the page.
var xmlns = 'http://www.w3.org/2000/svg',
IMAGE_TEMPLATE = document.createElementNS(xmlns, 'svg');
IMAGE_TEMPLATE.appendChild(document.body.querySelector('#ico-appliance-thermostat-128'));
IMAGE_TEMPLATE.setAttribute('id', 'svg');
IMAGE_TEMPLATE.setAttributeNS(null, 'width', 128);
IMAGE_TEMPLATE.setAttributeNS(null, 'height', 128);
IMAGE_TEMPLATE.setAttributeNS(null, 'viewBox', '0 0 128 128');
document.body.querySelector('#test').appendChild(IMAGE_TEMPLATE);
test = function(){
var s = new XMLSerializer().serializeToString(document.getElementById("svg"))
var encodedData = window.btoa(s);
console.log('data:image/svg+xml;base64,' + encodedData);
}
https://jsfiddle.net/6sra5c5L/
Try adding <svg></svg> around <g> element , closing } at test function ; defining test as a named function
var xmlns = 'http://www.w3.org/2000/svg',
IMAGE_TEMPLATE = document.createElementNS(xmlns, 'svg');
IMAGE_TEMPLATE.appendChild(document.body.querySelector('#ico-appliance-thermostat-128'));
IMAGE_TEMPLATE.setAttribute('id', 'svg');
IMAGE_TEMPLATE.setAttributeNS(null, 'width', 128);
IMAGE_TEMPLATE.setAttributeNS(null, 'height', 128);
IMAGE_TEMPLATE.setAttributeNS(null, 'viewBox', '0 0 128 128');
document.body.querySelector('#test').appendChild(IMAGE_TEMPLATE);
function test() {
var s = new XMLSerializer().serializeToString(document.getElementById("svg"))
console.log(document.getElementById("svg"))
console.log(s)
var encodedData = window.btoa(s);
console.log('data:image/svg+xml;base64,' + encodedData);
}
<svg>
<g id="ico-appliance-thermostat-128" transform="scale(2)">
<path d="M106.949,128.009 L105.294,124.692 C115.967,119.333 123.298,108.278 123.298,95.500 C123.298,82.722 115.967,71.666 105.294,66.308 L106.949,62.990 C118.835,68.958 126.999,81.270 126.999,95.500 C126.999,109.730 118.835,122.042 106.949,128.009 ZM117.376,95.500 C117.376,105.954 111.378,115.000 102.645,119.384 L100.990,116.067 C108.510,112.292 113.676,104.502 113.676,95.500 C113.676,86.497 108.510,78.708 100.990,74.933 L102.645,71.615 C111.378,76.000 117.376,85.045 117.376,95.500 ZM106.999,95.213 C106.999,98.063 104.756,100.373 101.988,100.373 C100.251,100.373 98.720,99.462 97.822,98.080 L91.490,98.080 L85.840,116.427 L85.662,116.427 L85.840,117.000 L80.829,117.000 L70.082,82.422 L65.795,97.506 L65.795,98.080 L54.999,98.080 L54.999,92.920 L62.087,92.920 L67.465,74.000 L72.477,74.000 L83.234,108.615 L88.067,92.920 L89.738,92.920 L93.079,92.920 L97.504,92.920 C98.324,91.222 100.021,90.053 101.988,90.053 C104.756,90.053 106.999,92.363 106.999,95.213 ZM24.999,128.000 C11.787,128.000 0.999,117.189 0.999,103.993 C0.999,96.779 4.177,90.380 8.986,85.988 C8.986,85.173 8.986,84.776 8.986,83.981 L8.986,15.997 C8.986,7.193 16.177,-0.000 24.979,-0.000 C33.780,-0.000 40.972,7.193 40.972,15.997 L40.972,83.981 C40.972,84.796 40.972,85.194 40.972,85.988 C45.780,90.380 48.979,96.779 48.999,103.993 C48.999,117.210 38.212,128.000 24.999,128.000 ZM33.999,90.000 L33.999,17.000 C33.999,12.373 29.662,8.009 24.988,8.009 C20.314,8.009 16.000,12.373 16.000,17.000 L16.000,90.000 C10.903,92.952 7.985,97.813 7.985,104.136 C7.985,113.411 15.641,120.990 25.011,120.990 C34.380,120.990 42.037,113.389 41.992,104.114 C41.992,97.791 39.118,92.952 33.999,90.000 ZM24.999,112.990 C19.904,112.990 15.999,109.082 15.999,103.983 C15.999,100.092 18.383,96.796 21.989,95.588 C21.989,95.290 21.989,95.290 21.989,94.992 L21.989,38.991 C21.989,37.500 23.181,35.994 24.984,35.994 C26.787,35.994 27.979,37.187 27.979,38.991 L27.979,95.008 C27.979,95.306 27.979,95.306 27.979,95.604 C31.585,96.812 33.984,100.107 33.999,103.983 C33.999,109.082 30.095,112.990 24.999,112.990 Z"
style="fill: #5aac21;fill-rule: evenodd;"></path>
</g>
</svg>
<div id="test"></div>
<button onclick="test()">Test</button>
jsfiddle https://jsfiddle.net/6sra5c5L/5/
Difference between #guest271314 and my answer:
#guest271314 his answer:
Wrap the g element inside a svg element to make sure that the browser renders the g element onload.
My answer:
Forces the svg element inside #test to render the g element since the g element wasn't rendered onload.
Best answer in this case: #guest271314
Reason: A g element should be inside a svg element in valid html.
When should my answer be used?
In the case that the g element is not an element in the html document.
You didn't close test() with a }.
Code below returns a base64 encoded svg:
https://jsfiddle.net/seahorsepip/6sra5c5L/1/
Edit:
svg render issue is something I ran into myself before, here's the fix with a line of jquery:
https://jsfiddle.net/seahorsepip/6sra5c5L/3/
//Force refresh svg
$("#test").html($("#test").html());
Here's the original SO thread about the issue: jquery's append not working with svg element?
I don't know the javascript equivalent for the jQuery code I added, I tried to write it but it didn't work :/
Edit 2:
Here's the pure js equivalent:
https://jsfiddle.net/seahorsepip/6sra5c5L/4/
//Force refresh svg
var svg = document.body.querySelector('#test').innerHTML;
document.body.querySelector('#test').innerHTML = "";
document.body.querySelector('#test').innerHTML = svg;
HTML elements and SVG elements have different namespaces. By putting the <g> element in your HTML you have created an <html:g> element. When it is moved inside the <svg>, it is still an <html:g> and won't be recognised by the SVG renderer.
You either have to put it inside soem <svg> tags as #guest271314 described. Or, after you append to the SVG, go through all the elements in the <g> and change all their namespaces to the SVG one.
I don't see a full explanation what actually happened anywhere here, so here it goes.
Browsers parse HTML as HTML
While this seems obvious, it is not so in the case, when you put non-HTML (SVG, XML, ...) elements into it.
Browsers are too nice and when you provide valid SGML (parent of XML, HTML, ...) and they find element that doesn't belong there(<g>) according to what you said will be there (HTML) - they don't complain and only treat the unknown elmenents as unknown HTML elements, resulting in class HTMLUnknownElement.
Browsers generally don't change element object class
So you are basically putting HTMLUnknownElement into SVGSVGElement, which will put the tag content into the svg, but since the underlying object is not a valid subelement of SVG (SVGGElement) it won't do anything.
This is the same reason why dynamically created <svg> has to be created with namespace => the namespace, when combined with <svg> element, is telling browser - hey!, this tag is from SVG specification => treat it as an SVG!
Solutions
Your current example code has a syntax error, which I will not address, since that is not the real issue.
Statically defined <g> template
If the template is already in the original document - force browser to take it as SVGSVGElement by wrapping with <svg xmlns="http://www.w3.org/2000/svg"></svg> (browsers may eat it without xmlns attribute, but it is the safer way).
Worried that it would display? Just hide it with CSS display: none; - it will not affect the rendering.
Dynamically defined <g> template
If you are creating <g> element dynamically, you should create it while specifying the namespace using createElementNS() like:
document.createElementNS('http://www.w3.org/2000/svg', 'g');
Static, but non-changable <g> template
If you are stuck in the middle and can't change the static template, you can still build upon the dynamic approach:
create new <g> with the right namespace
var newGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
get the "raw" old <g> - it will be HTMLUnknownElement, but it is ok, since it is built upon HTMLElement and that gives us enaugh API to transfer all we need
var oldGroup = document.querySelector('oldGroupSelector');
possibly transfer old group attributes to new group - handy is element.attributes
for (var i = 0; i < oldGroup.attributes.length; ++i) {
newGroup.setAttribute(
oldGroup.attributes.item(i).name,
oldGroup.attributes.item(i).value
);
}
Now browser already knows contents of newGroup <g> are actually svg - newGroup is SVGGElement - now we can just refill the contents
newGroup.innerHTML = oldGroup.innerHTML;