How to position a checkbox inside an svg element? - javascript

Assume I want to create a small square web element which has circle and a checkbox in the center. Ticking the checkbox should change the colour of the circle. I have provided a working code example below. The problem I currently face is that my current implementation places the checkbox below the svg, not within it.
Hence my question: is there a way to position this tickbox within or above the svg element, and specify its position in terms of percentages of width and height? (e.g., x = width/2, y = height/2)
I expect that this would be a simple problem, but I cannot seem to find a solution - perhaps I am searching for the wrong keywords. I would appreciate any advice!
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/mathjs/lib/browser/math.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
</head>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz">
<svg id="click" xmlns="http://www.w3.org/2000/svg" style="background-color:#cccccc">
<defs>
</defs>
</svg>
</div>
<input type="checkbox" onclick='check()' id='myCheckbox'>
<body>
<script>
// ===================================================
// Set up basic viewport options
// ===================================================
// Get viewport sizes
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
// Fit in a square window
var height = math.min([vw,vh])*0.75;
var width = math.min([vw,vh])*0.75;
// Create a svg
var svg = d3.select("#click") // This selects the div
.attr("width", width) // This defines the canvas' width
.attr("height", height) // This defines the canvas' height
// Plot a circle
svg.append("circle")
.attr("r", height/4)
.attr("cx", width/2)
.attr("cy", height/2)
.attr("fill", "#ff0000")
.attr("opacity", 0.75)
.attr("id","circ");
// Checking the box changes the color
function check() {
// If the box is ticked, make the circle green
if (d3.select("#myCheckbox").property("checked")) {
d3.select("#circ")
.attr("fill", "#00ff00")
// If the box is not ticked, make the circle red
} else {
d3.select("#circ")
.attr("fill", "#ff0000")
}
}
</script>
</body>
</html>
EDIT: Following HereticMonkey's suggestion, do you recommend editing the html section as follows?
<!-- Create a div where the graph will take place -->
<div id="my_dataviz" position="relative">
<svg id="click" xmlns="http://www.w3.org/2000/svg" style="background-color:#cccccc" position="absolute">
<defs>
</defs>
</svg>
<input type="checkbox" onclick='check()' id='myCheckbox' top="0px" left="-100px" position="absolute">
</div>

ditch D3
put the HTML in the SVGs foreignObject
finetune positioning with the x and y attributes
use the modern CSS :has selector to color the circle based on the :checked state
not yet supported in FireFox: https://caniuse.com/css-has
so add your own JS onclick inline Event Handler to color the circle
Modern version with :has selector
No JavaScript required
<svg viewBox="0 0 200 200" style="height:180px">
<style>
svg:has(input:checked) circle { fill:green }
input { zoom: 4 }
</style>
<circle cx="50%" cy="50%" r="49%" fill="red" />
<foreignObject x="30%" y="31%" width="100%" height="100%">
<input type="checkbox" CHECKED>
</foreignObject>
<circle cx="50%" cy="50%" r="3" fill="black" />
</svg>
Classic FireFox version
now requires two CHECKED settings to set an initial :checked state
<svg viewBox="0 0 200 200" style="height:180px">
<style>
circle[checked] { fill:green }
input { zoom: 4 }
</style>
<circle cx="50%" cy="50%" r="49%" fill="red" CHECKED />
<foreignObject x="30%" y="32%" width="100%" height="100%">
<input type="checkbox" CHECKED
onclick="this.closest('svg')
.querySelector('circle')
.toggleAttribute('checked')">
</foreignObject>
<circle cx="50%" cy="50%" r="3" fill="black" />
</svg>

Thanks to Heretic Monkey's awesome advice, I have got it to work!
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/mathjs/lib/browser/math.js"></script>
<script src="https://d3js.org/d3.v4.min.js"></script>
<style>
#my_dataviz { position: relative; }
#click { background-color: #ccc; position: absolute; }
#myCheckbox { position: absolute; top: 0; left: 0; }
</style>
</head>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz" position="relative">
<g id="group">
<svg id="click" xmlns="http://www.w3.org/2000/svg" style="background-color:#cccccc" position="absolute">
<defs>
</defs>
</svg>
<input type="checkbox" onclick='check()' id='myCheckbox' top="0px" left="-100px" position="relative">
</g>
</div>
<body>
<script>
// ===================================================
// Set up basic viewport options
// ===================================================
// Get viewport sizes
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
// Fit in a square window
var height = math.min([vw,vh])*0.75;
var width = math.min([vw,vh])*0.75;
// Create a svg
var svg = d3.select("#click") // This selects the div
.attr("width", width) // This defines the canvas' width
.attr("height", height) // This defines the canvas' height
// Plot a circle
svg.append("circle")
.attr("r", height/4)
.attr("cx", width/2)
.attr("cy", height/2)
.attr("fill", "#ff0000")
.attr("opacity", 0.75)
.attr("id","circ");
document.getElementById('myCheckbox').style.top = (width/2).toString()+"px";
document.getElementById('myCheckbox').style.left = (height/2).toString()+"px";
// Checking the box changes the color
function check() {
// If the box is ticked, make the circle green
if (d3.select("#myCheckbox").property("checked")) {
d3.select("#circ")
.attr("fill", "#00ff00")
// If the box is not ticked, make the circle red
} else {
d3.select("#circ")
.attr("fill", "#ff0000")
}
}
d3.select("#click")
.append("input")
.attr("type", "checkbox")
.style("position", "absolute")
.style("top", 100)
.style("left", 100)
</script>
</body>
</html>

Related

Coloring a straight path from X1=0 to X2=100, depending on user inputs

I'm making a progressive line bar using SVG elements, my logic is that for every number of inputs , the line gets divided on even parts and each part will get colored by percentage, depending of the number of valid interactions.
In the console I can see the logic is working as it should, however the coloring of the line is not.
I'm not fully sure how can I make that the progress gets colored from left to right progressively, right now its behaviour doesn't follow the desired path.
function updateBar() {
//Path that will be painted
var myProgress = document.getElementById("myProgress");
//Reference the number of inputs
var numberOfInputs =document.getElementById("totalInput").value;
//number that we will use to divide the valid interaction with the inputs
var interactionTimes = document.getElementById("validInput").value;
//getting the # to get the right percent to paint the line
var lineDivision = [(100 / numberOfInputs)];
console.log("this is lineDivision:" + lineDivision);
//and then with this lineDivision variable, replace the 20 from the previous code. But here is where I have the main problem, because as result of every click I do in a checkbox the var percent always get 100 and as result the ring is already fully colored.
var percent = (interactionTimes) * (lineDivision);
console.log("this is the percent:" + percent);
myProgress.style.strokeDashoffset = 100 - percent;
if (interactionTimes === numberOfInputs) {
myProgress.style.stroke = "#1DEBA1";
} else if (interactionTimes < numberOfInputs) {
myProgress.style.stroke = "purple";
}
return true;
}
checks = document.querySelectorAll("input[type='number']");
checks.forEach(function(paint) {
paint.addEventListener("change", function() {
updateBar()
});
});
#mySvg {
transform: rotate(-180deg);
}
#myRect {
width: 0;
height: 30px;
stroke: #E3E5E7;
stroke-width: 3px;
}
#myProgress {
margin: auto;
width: 50%;
/*stroke: red;*/
stroke-width: 3px;
stroke-dasharray: 100;
stroke-dashoffset: 100;
stroke-linecap: square;
}
<svg id="line-progress" height="4" width="300">
<g >
<line class="myRect" id="myRect" x1="0" y1="50%" x2="100%" y2="50%" stroke-width="4" fill="transparent" />
</g>
<g >
<line class="myProgress" id="myProgress" x1="0" y1="50%" x2="100%" y2="50%" pathLength="100" fill="transparent"/>
</g>
</svg>
<form>
<label> Total inputs to interact with
<input id="totalInput" type="number" min="0" max="100">
</label>
<br>
<label> Number of valid inputs
<input id="validInput" type="number" min="0" max="100">
</label>
</form>
<script src="script.js"></script>

How to compute getBoundingClientRect() in a scaled svg?

I have a usecase where designers supply us with a SVG, and we use certain elements in that SVG to position our dynamically created elements.
In the snippet below I try to overlap the rect#overlayTarget with the div#overlay using getBoundingClientRect: it doesn't take the scaling of the parent element into account, and the elements don't overlap.
The answers from this question is not applicable here as it uses element.offsetLeft and element.offsetTop, which aren't available for SVG: How to compute getBoundingClientRect() without considering transforms?
How do I make the #overlay and #overlayTarget overlap?
const target = document.querySelector("#overlayTarget");
const position = target.getBoundingClientRect();
const overlay = document.querySelector("#overlay");
overlay.style.top = `${position.y}px`;
overlay.style.left = `${position.x}px`;
overlay.style.width = `${position.width}px`;
overlay.style.height = `${position.height}px`;
#overlay {
position: absolute;
background: hotpink;
opacity: 0.3;
width: 100px;
height: 100px;
}
<div id="app" style="transform: scale(0.875);">
Test
<div id="overlay"></div>
<svg xmlns="http://www.w3.org/2000/svg" width="1809" height="826" viewBox="0 0 809 826">
<g
id="Main_overview"
data-name="Main overview"
transform="translate(-49.5 -155)"
>
<g
id="overlayTarget"
data-name="DC-DC converter"
transform="translate(400 512)"
>
<rect
id="Rectangle_29"
data-name="Rectangle 29"
width="74"
height="74"
fill="none"
stroke="#47516c"
stroke-width="2"
/>
</g>
</g>
</svg>
</div>
If you cannot set your overlay element outside of the transformed element, this answer will work, but only for some simple transformations:
translations and
scales with factors > 0
In these cases, the corners of the bounding box aren't moved out of their top/left and bottom/right orientation. Rotations or skews, and most of the 3D transforms won'T work out.
You can then compute the resulting box values for your overlay by transforming the corners of position with the inverse matrix to that set for the #app element. The DOMPoint and DOMMatrix interfaces help with that.
It is important to remember that transform sets an implicit position: relative, so the top and left values of the overlay are not in relation to the viewport.
const app = document.querySelector('#app');
const relative = app.getBoundingClientRect();
const target = document.querySelector("#overlayTarget");
const position = target.getBoundingClientRect();
const matrix = new DOMMatrix(app.style.transform).inverse();
const topleft = new DOMPoint(
position.x - relative.x,
position.y - relative.y
).matrixTransform(matrix);
const bottomright = new DOMPoint(
position.x - relative.x + position.width,
position.y - relative.y + position.height
).matrixTransform(matrix);
const overlay = document.querySelector("#overlay");
overlay.style.top = `${topleft.y}px`;
overlay.style.left = `${topleft.x}px`;
overlay.style.width = `${bottomright.x - topleft.x}px`;
overlay.style.height = `${bottomright.y - topleft.y}px`;
#overlay {
position: absolute;
background: hotpink;
opacity: 0.3;
width: 100px;
height: 100px;
}
<div id="app" style="transform: scale(0.875);">
Test
<div id="overlay"></div>
<svg xmlns="http://www.w3.org/2000/svg" width="1809" height="826" viewBox="0 0 809 826">
<g
id="Main_overview"
data-name="Main overview"
transform="translate(-49.5 -155)"
>
<g
id="overlayTarget"
data-name="DC-DC converter"
transform="translate(400 512)"
>
<rect
id="Rectangle_29"
data-name="Rectangle 29"
width="74"
height="74"
fill="none"
stroke="#47516c"
stroke-width="2"
/>
</g>
</g>
</svg>
</div>

Can't translate by vh/vw - I need this to be responsive, how can I convert the vh/vw to px?

I am working on centering a semi-circle inside of another circle.
I would like this to be responsive, so changes in the browser keep the elements centered to where they need to be.
My idea is to have two semi-circle buttons on top of one another in another circle.
This is what I have so far.
const dims = {height: '80vh', width: '90vw', radius: '7vw'};
const cent = {x: '50vw', y: '35vh'};
const svg = d3.select('#button-box');
const width = svg.attr('width');
const height = svg.attr('height');
console.log(width)
svg.append('circle')
.attr('r', dims.radius)
.attr('cx', cent.x)
.attr('cy', cent.y)
.attr('fill', '#0000a0')
.attr('stroke', 'green')
const g = svg.append('g')
.attr('transform', `translate(${width /2}), ${height /2}`);
g.append('path')
.attr('d', d3.arc()({
innerRadius: 0,
outerRadius: 100,
startAngle: Math.PI / 2,
endAngle: Math.PI * 3/2
}))
.attr('fill', 'whitesmoke')
<script src="https://d3js.org/d3.v5.js"></script>
<script src="js/jonsIdea.js"></script>
<div class="jonsIdea">
<div class="canvas">
<h1>Please select your businesses</h1>
<svg id='button-box' width="90vw" height="80vh">
</svg>
</div>
</div>
Could anyone please help me in converting my vh/vw width/height params into pixels dynamically so I can center and adjust the semi circles? Thanks
Do you want have somethink like this? Than you can add a click-event-listner to each path.
update:
now the example have onclick event.
document.getElementById("idPathRed").onclick = (f => console.log('click red'));
document.getElementById("idPathGreen").onclick = (f => console.log('click green'));
document.getElementById("idPathBlue").onclick = (f => console.log('click blue'));
<div style="display: flex; padding: 20px; justify-content: center; align-items:center; background-color: lightblue">
<div>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116px" height="116px" viewBox="0 0 4800965 4800965">
<path id="idPathRed" style="fill:red" d="M2400483 2400483l2393194 0c0,1321728 -1071466,2393194 -2393194,2393194l0 -2393194z" />
<path id="idPathGreen" style="fill: green" d="M2400483 2400483l-2393195 0c0,-1321729 1071466,-2393195 2393195,-2393195 1321728,0 2393194,1071466 2393194,2393195l-2393194 0z" />
<path id="idPathBlue" style="fill:blue" d="M2400483 2400483l-2393195 0c0,1321728 1071466,2393194 2393195,2393194l0 -2393194z" />
</svg>
</div>
</div>

Get position of svg element in svg with multiple transforms

I have a SVG-document that contains a circle. I would like to place an absolutely positioned html-element on top of this circle. The circle contains a cx and cy and there are several transforms on parent elements. How can I translate the position inside the SVG to the coordinate space of the parent html-element (normal pixels)?
The SVG is generated by a program, so I will have little control over it, meaning that I will need a general solution that can handle any number of transforms on parent element.
I'm not using d3 or any similar library, so I'm looking for a way to solve this using plain JavaScript. This is a simplified example of my problem:
<head>
<style>
#box {
background-color: greenyellow;
position: absolute;
width: 100px;
height: 100px;
}
</style>
</head>
<body>
<div id="root">
<svg width="1274.2554" height="692.35712" id="svg">
<g id="viewport" transform="matrix(0.655937472561484,0,0,0.655937472561484,162.2578890186666,57.23719435833714)">
<g id="layer1" transform="translate(-5.49584,-171.51931)">
<g id="elem" transform="translate(34.862286,232.62127)">
<circle id="center" cx="19.952429" cy="19.90885" style="fill:#ffcc33" r="20"></circle>
</g>
</g>
</g>
</svg>
<div id="box" ></div>
</div>
<script>
function positionBoxAtCenter() {
var box = document.getElementById('box');
var center = document.getElementById(('center'));
// TODO: get x, y from 'center' so that 'box' can be placed on top of center
var x = 0, y = 0;
box.setAttribute('style', 'top: ' + y + 'px; left: ' + x + 'px');
}
</script>
</body>
JSFiddle: https://jsfiddle.net/p9pf56sz/
Using getBoundingClientRect():
var pos = center.getBoundingClientRect();
You can get the center of the circle:
var x = pos.left + pos.width/2, y = pos.top + pos.height/2;
Here is the demo:
function positionBoxAtCenter() {
var box = document.getElementById('box');
var center = document.getElementById(('center'));
var pos = center.getBoundingClientRect();
var x = pos.left + pos.width/2, y = pos.top + pos.height/2;
box.setAttribute('style', 'top: ' + y + 'px; left: ' + x + 'px');
}
positionBoxAtCenter();
#box {
background-color: greenyellow;
position: absolute;
width: 100px;
height: 100px;
}
<body>
<div id="root">
<svg width="1274.2554" height="692.35712" id="svg">
<g id="viewport" transform="matrix(0.655937472561484,0,0,0.655937472561484,162.2578890186666,57.23719435833714)">
<g id="layer1" transform="translate(-5.49584,-171.51931)">
<g id="elem" transform="translate(34.862286,232.62127)">
<circle id="center" cx="19.952429" cy="19.90885" style="fill:#ffcc33" r="20"></circle>
</g>
</g>
</g>
</svg>
<div id="box" ></div>
</div>
</body>
However, as you can see in the demo, you are moving the top/left corner of the rectangle (its origin) to the center of the circle. So, you'll have to calculate the center of the rectangle as well (and subtract its difference to the origin).

How can I set a viewBox on an SVG element created with JavaScript?

When I use a setAttribute('viewBox') on an SVG element that already exists in the DOM, it gets set properly (with an uppercase 'b').
When I create an SVG element in JavaScript and add the 'viewBox' attribute there . . . it ends up in the DOM in all lowercase letters.
Why is this?
document.querySelector('#circle svg').setAttribute('viewBox', '190 190 20 20');
let svg = document.createElement('svg'),
circle = document.createElement('circle');
circle.setAttribute('cx', 200);
circle.setAttribute('cy', 200);
circle.setAttribute('r', 10);
svg.setAttribute('viewBox', '190 190 20 20');
svg.appendChild(circle);
document.body.appendChild(svg);
// document.querySelector('#circleGenerated svg').setAttribute('viewBox', '190 190 20 20');
svg {
border: 2px dashed #666;
width: 200px;
height: 200px;
}
<p>Existing svg with dynamically added viewBox attribute:</p>
<div id="circle">
<svg>
<circle cx="200" cy="200" r="10" id="center-point" fill="#bbb" />
</svg>
</div>
<p>Generated svg with dynamically added viewBox attribute:</p>
You have to use document.createElementNS("http://www.w3.org/2000/svg", "svg") instead of document.createElement('svg')
document.querySelector('#circle svg').setAttribute('viewBox', '190 190 20 20');
let svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"),
circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
circle.setAttribute('cx', 200);
circle.setAttribute('cy', 200);
circle.setAttribute('r', 10);
svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink");
svg.setAttribute('viewBox', '190 190 20 20');
svg.appendChild(circle);
document.body.appendChild(svg);
// document.querySelector('#circleGenerated svg').setAttribute('viewBox', '190 190 20 20');
svg {
border: 2px dashed #666;
width: 200px;
height: 200px;
}
<p>Existing svg with dynamically added viewBox attribute:</p>
<div id="circle">
<svg>
<circle cx="200" cy="200" r="10" id="center-point" fill="#bbb" />
</svg>
</div>
<p>Generated svg with dynamically added viewBox attribute:</p>
You're not creating a valid SVG element in the first place though. You can't use createElement to create SVG elements (that's only for HTML elements). You must use createElementNS and provide the SVG namespace as the first argument.
let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');

Categories