I'm using the svg jquery plugin by Keith Wood, not the HTML5 canvas.
I define my svg image like this to scale my svg triangle image to fit its div container:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100%" viewBox="0 0 299 215" >
<g>
<polygon points="1,1 299,1 149,210" fill="blue" stroke="blue" stroke-width="0" class="votenow"/>
</g>
</svg>
But how do I then match the coordinate systems?
I want to capture the mouse location at some point over the triangle and draw a circle at those X Y coordinates, but the circle gets drawn in a different location because the coordinate systems don't match.
So a circle would be drawn at point 10,10 but appear to be at 50,60 for example.
How do people cope with this?
Thanks.
Final Solution: Using the JQuery plugin to draw the circle and getScreenCTM() to calculate the points.
Perhaps I no longer require the JQuery plugin but it will do for now. Couldn't see how to do it using only the plugin.
$('#cvtriangle .tri').on( "click", function(e) {
jqsvg = $('#cvtriangle').svg('get');
svg = document.querySelector("svg");
var pt = svg.createSVGPoint();
pt.x = e.clientX;
pt.y = e.clientY;
pt = pt.matrixTransform(svg.getScreenCTM().inverse());
jqsvg.circle(pt.x, pt.y, 5, {class: 'vote', fill: 'white', stroke: 'white', strokeWidth: 2, cursor: 'pointer'});
});
See http://msdn.microsoft.com/en-us/library/hh535760%28v=vs.85%29.aspx
It's my sample code.
For this usage, getScreenCTM method is very useful.
<svg viewBox="0 0 300 300" onload="
var c = document.getElementById('c');
var cx = c.cx.baseVal;
var cy = c.cy.baseVal;
var svg = this;
var point = svg.createSVGPoint();
svg.onmousemove = function(e){
point.x = e.clientX;
point.y = e.clientY;
var ctm = c.getScreenCTM();
var inverse = ctm.inverse();
var p = point.matrixTransform(inverse);
cx.value = p.x;
cy.value = p.y;
};
">
<rect width="100%" height="100%" fill="yellow"/>
<circle id="c" r="10" fill="blue"/>
</svg>
If you call the function getScreenCTM() on the SVG element, it will return the transform matrix used to convert document coordinates to screen coordinates. You want the transform matrix for the other direction, so call inverse() on the matrix object.
var transform = svg.getScreenCTM().inverse();
Now you can transform a point object to do the final conversion:
pt = pt.matrixTransform(transform);
Working demo here
var x = document.getElementById("x"),
y = document.getElementById("y"),
svg = document.querySelector("svg");
svg.addEventListener("mousemove", function(evt) {
var pt = svg.createSVGPoint();
pt.x = evt.pageX;
pt.y = evt.pageY;
pt = pt.matrixTransform(svg.getScreenCTM().inverse());
x.innerHTML = pt.x;
y.innerHTML = pt.y;
}, false);
#container {
width: 200px;
height: 200px;
}
div {
float: left;
margin-left: 1em;
}
<div id="container">
<svg version="1.0" viewbox="0 0 100 100">
<rect x="0" y="0" width="100" height="100" fill="blue"/>
</svg>
</div>
<div>
x = <span id="x"></span><br/>
y = <span id="y"></span>
</div>
If the above version (using pageX/Y) doesn't work for you, try this version instead.
var x = document.getElementById("x"),
y = document.getElementById("y"),
svg = document.querySelector("svg");
svg.addEventListener("mousemove", function(evt) {
var pt = svg.createSVGPoint();
pt.x = evt.clientX;
pt.y = evt.clientY;
pt = pt.matrixTransform(evt.target.getScreenCTM().inverse());
x.innerHTML = pt.x;
y.innerHTML = pt.y;
}, false);
#container {
width: 200px;
height: 200px;
}
div {
float: left;
margin-left: 1em;
}
<div id="container">
<svg version="1.0" viewbox="0 0 100 100">
<rect x="0" y="0" width="100" height="100" fill="blue"/>
</svg>
</div>
<div>
x = <span id="x"></span><br/>
y = <span id="y"></span>
</div>
Related
I'm creating a bar chart with SVG, by default bars are vertical. But in some cases it looks better if bars are horizontal, like when there are only 2 bars.
How can I reuse same SVG code and just switch X and Y axis to achieve that? It's actually more than just X and Y and also things like height and width for the rect.
Is that possible? I would like to avoid writing very similar code twice.
Example: I built two charts separately, ideally the second chart should be produced by reusing the code from the first chart.
<style>
svg { height: 20px; width: 100px; border: 1px solid #ccc;}
</style>
<svg>
<rect x="5%" y="60%" width="40%" height="40%" fill="black"/>
<rect x="55%" y="40%" width="40%" height="60%" fill="black"/>
</svg>
<svg>
<rect y="5%" x="0%" height="40%" width="40%" fill="black"/>
<rect y="55%" x="0%" height="40%" width="60%" fill="black"/>
</svg>
Maybe something like this:
function cloneWithTransformedAttributes(obj1, mapping) {
// returns copy of obj1 with child node attributes transformed according to mapping.
const obj2 = obj1.cloneNode(true);
[...obj1.children].forEach((child, idx)=>{
Object.keys(mapping).forEach((attribute) => {
const replacementVal = mapping[attribute].default ?
mapping[attribute].default :
child.getAttribute(mapping[attribute]);
obj2.children[idx].setAttribute(attribute, replacementVal);
})
})
return obj2;
}
mapping = {
x: {
default: 0
},
y: "x",
width: "height",
height: "width"
}
const verticalSvg = document.getElementsByTagName("svg")[0];
const horizontalSvg = cloneWithTransformedAttributes(verticalSvg, mapping);
const graphs = document.getElementById("graphs");
graphs.appendChild(horizontalSvg);
<style>
svg { height: 20px; width: 100px; border: 1px solid #ccc;}
</style>
<div id="graphs">
<svg>
<rect x="5%" y="60%" width="40%" height="40%" fill="black"/>
<rect x="55%" y="40%" width="40%" height="60%" fill="black"/>
</svg>
</div>
Edit: You could also do it with svg transforms but you need to put the inside of the SVG inside a group <g></g>. Here's an example with both a manually calculated transform and a JS solution that calculates the transform by itself by getting the original svg width and height:
const verticalSvg = document.getElementsByTagName("svg")[0];
const svgStyle = window.getComputedStyle(verticalSvg, null);
const width = parseInt(svgStyle.getPropertyValue("width"));
const height = parseInt(svgStyle.getPropertyValue("height"));
const whRatio = width / height;
const transform = `rotate(90) scale(${1 / whRatio} ${whRatio}) translate(0 -${height})`
const horizontalSvg = verticalSvg.cloneNode(true);
horizontalSvg.children[0].setAttribute("transform", transform);
const graphs = document.getElementById("graphs");
graphs.appendChild(horizontalSvg);
<style>
svg { height: 20px; width: 100px; border: 1px solid #ccc; overflow: visible}
</style>
<div id="graphs">
<svg>
<g>
<rect x="5%" y="60%" width="40%" height="40%" fill="black"/>
<rect x="55%" y="40%" width="40%" height="60%" fill="black"/>
</g>
</svg>
<svg>
<!-- Manually calculated and applied transform -->
<g transform="rotate(90)
scale(0.20 5)
translate(0 -20)
">
<rect x="5%" y="60%" width="40%" height="40%" fill="black"/>
<rect x="55%" y="40%" width="40%" height="60%" fill="black"/>
</g>
</svg>
<!-- JS generated SVG will get inserted here -->
</div>
In your case, you need to apply the transform to a group holding the SVG contents.
If you apply the transform to the SVG itself it will also scale the border according to the scale transform.
vector-effect="non-scaling-stroke" can not be applied to the border of the SVG which is not part of the SVG itself. But if you are using elements with a "stroke" property inside the SVG then you would probably want to also apply the vector-effect="non-scaling-stroke" attribute to them.
I am working on a navigation bar in a circle format split in 5.All the elements are SVGs.
It really look like a pie chart.While hovering one part, i would like to scale the svg element to the exterior. As i am trying to use scale with this, the element just translate itself further and do the scale effect.I'm not sure but i think it is a problem of overlapping css element? Anyway if someone could help me with this how to tell the part to stay at the same place and then scale bigger, or do i need to manually re-translate the element at the correct place?
Thanks, here is a represatation:
#group-part-1>text {
visibility: hidden;
}
#group-part-1:hover text {
visibility: visible;
}
#group-part-2>text {
visibility: hidden;
}
#group-part-2:hover text {
visibility: visible;
}
#group-part-3>text {
visibility: hidden;
}
#group-part-3:hover text {
visibility: visible;
}
#group-part-4>text {
visibility: hidden;
}
#group-part-4:hover text {
visibility: visible;
}
#group-part-5>text {
visibility: hidden;
}
#group-part-5:hover text {
visibility: visible;
}
#part-1:hover {
fill: red;
transform: scale(1.2);
}
#part-2:hover {
fill: green;
transform: scale(1.2);
}
#part-3:hover {
fill: purple;
}
#part-4:hover {
fill: orange;
}
#part-5:hover {
fill: blue;
}
<svg width="210" height="297" viewBox="0 0 210 297" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="example">
<g id="group-part-5">
<path id="part-5"
d="M156.393 107.998C159.608 118.055 159.557 128.873 156.247 138.899C152.936 148.926 146.536 157.647 137.964 163.813L108.768 123.223L156.393 107.998Z"
fill="black" stroke="black" stroke-width="0.0132292" />
<text x="160" y="150" fill="black">Project</text>
</g>
<g id="group-part-4">
<path id="part-4"
d="M137.963 163.813C129.391 169.979 119.087 173.272 108.529 173.222C97.9698 173.171 87.6979 169.78 79.1853 163.532L108.768 123.223L137.963 163.813Z"
fill="#1A1A1A" stroke="black" stroke-width="0.0132292" />
<text x="90" y="200" fill="black">About</text>
</g>
<g id="group-part-3">
<path id="part-3"
d="M79.2054 163.547C70.6897 157.304 64.3689 148.526 61.1491 138.469C57.9293 128.413 57.9756 117.596 61.2815 107.568L108.768 123.223L79.2054 163.547Z"
fill="#333333" stroke="black" stroke-width="0.0132292" />
<text x="10" y="150" fill="black">Contact</text>
</g>
<g id="group-part-2">
<path id="part-2"
d="M61.2928 107.534C64.606 97.508 71.008 88.7885 79.5814 82.625C88.1547 76.4615 98.4593 73.1703 109.018 73.2231L108.768 123.223L61.2928 107.534Z"
fill="#4D4D4D" stroke="black" stroke-width="0.0132292" />
<text x="25" y="75" fill="black">Home</text>
</g>
<g id="group-part-1">
<path id="part-1"
d="M108.941 73.2228C119.5 73.2594 129.776 76.6378 138.297 82.8738C146.818 89.1097 153.146 97.8831 156.374 107.937L108.768 123.223L108.941 73.2228Z"
fill="#666666" stroke="black" stroke-width="0.0132292" />
<text x="150" y="75" fill="black">Work</text>
</g>
</svg>
thanks
This is the way I would do it:
the svg element is centered around the point {x:0,y:0}: viewBox="-105 -105 210 210"
I calculate the points for the arc in base of the angle of the wedge and the radius of the circle
In this example I'm rotating the text too so I'm putting both the wedge and the text in the same group (#group_part5) and I'm transforming the group on mouse over: transform: translate(10px, 0) scale(1.2);
I'm wrapping everything in a different group (#example) and I'm rotating this group to the needed position.
//the angle for the circle wedge
let angle = 2*Math.PI/5;
//the radius of the circle wedge
let r = 60;
//calculate the points for the arc
let p1 = {x:r*Math.cos(-angle/2),
y:r*Math.sin(-angle/2)};
let p2 = {x:r*Math.cos(angle/2),
y:r*Math.sin(angle/2)};
//build the d attribute
let d = `M0,0L${p1.x},${p1.y}A${r},${r} 0 0 1 ${p2.x},${p2.y} z`;
//set the d attribute of the path
part5.setAttribute("d",d);
svg {
border: solid;
}
#example {
transform: rotate(36deg);
}
#group_part5:hover {
fill: red;
transform: translate(10px, 0) scale(1.2);
}
#group_part5:hover text {
fill: black;
}
<svg width="210" viewBox="-105 -105 210 210">
<g id="example">
<g id="group_part5">
<path id="part5" d="" />
<text fill="none" x="30">Project</text>
</g>
</g>
</svg>
Observation: If you don't want to use javascript you can take the d attribute for the path from the inspector.
UPDATE
The OP is commenting
The only thing is that the 'origin' top-left corner is moving, i want it to stay at the same place as well as the borders, the border only need to be longer and the circle exterior border further
If I understand you correctly in the previous demo please replace transform: translate(10px, 0) scale(1.2); with transform: scale(1.2);
If this is what you need there is a simpler way to do this: instead or scaling the wedge you can add a wide stroke - the same color as the fill - like in the following demo:
Please take a look:
svg {
border: solid;
}
#example {
transform: rotate(36deg);
}
#group_part5:hover {
fill: red;
/*transform: translate(10px, 0) scale(1.2);*/
}
#group_part5:hover path:nth-of-type(2){stroke:red;}
#group_part5:hover text {
fill: black;
}
<svg width="210" viewBox="-105 -105 210 210">
<g id="example">
<g id="group_part5">
<path id="part5" d="M0,0L48.54101966249685,-35.26711513754839A60,60 0 0 1 48.54101966249685,35.26711513754839 z"></path>
<path d="M48.54101966249685,-35.26711513754839A60,60 0 0 1 48.54101966249685,35.26711513754839" fill="none" stroke-width="10"></path>
<text fill="none" x="30">Project</text>
</g>
</g>
</svg>
I am trying to create a scale with radial lines and numbers with range 0-100.
Here is my code:
<!DOCTYPE html>
<html>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<title>SVG Gauge</title>
</head>
<style>
#wrapper {
position: relative;
margin: auto;
}
#meter {
width: 100%;
height: 100%;
transform: rotateX(180deg);
}
.circle {
fill: none;
}
#mask {
stroke: #404040;
stroke-width: 60;
}
.blackie {
fill:none;
stroke: #000000;
stroke-width: 30;
}
.range {
stroke-width: 60;
}
.scale {
stroke: #cccccc;
}
#slider, #lbl {
position: absolute;
}
#slider {
cursor: pointer;
left: 0;
margin: auto;
right: 0;
top: 58%;
width: 94%;
}
#lbl {
background-color: #4B4C51;
border-radius: 2px;
color: white;
font-family: 'courier new';
font-size: 15pt;
font-weight: bold;
padding: 4px 4px 2px 4px;
right: -48px;
top: 57%;
}
#meter_needle {
height: 40%;
left: 0;
margin: auto;
position: absolute;
right: 0;
top: 10%;
transform-origin: bottom center;
transform: rotate(270deg);
}
</style>
<body>
<div id="wrapper">
<svg id="meter">
<g class="scale">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:rgb(102, 102, 255);stop-opacity:1" />
<stop offset="100%" style="stop-color:rgb(204, 204, 255);stop-opacity:1" />
</linearGradient>
</defs>
<circle id="high" class="circle range" cx="50%" cy="50%" stroke="url(#grad)">
</circle>
<circle id="mask" class="circle" cx="50%" cy="50%">
</circle>
<circle id="low" class="blackie" cx="50%" cy="50%" r="360">
</circle>
<circle id="outline_ends" class="circle outline" cx="50%" cy="50%">
</circle>
</g>
</svg>
<img id="meter_needle" src="gauge-needle.svg" alt="">
<input id="slider" type="range" min="0" max="100" value="0" />
<label id="lbl" id="value" for="">0%</label>
</div>
<script>
var r = 400;
var circles = document.querySelectorAll('.circle');
var total_circles = circles.length;
for (var i = 0; i < total_circles; i++) {
circles[i].setAttribute('r', r);
}
var meter_dimension = (r * 2) + 100;
var wrapper = document.querySelector('#wrapper');
wrapper.style.width = meter_dimension + 'px';
wrapper.style.height = meter_dimension + 'px';
var cf = 2 * Math.PI * r;
var semi_cf = cf / 2;
var z = 40 * Math.PI;
document.querySelector('#outline_ends')
.setAttribute('stroke-dasharray', 2 + ',' + (semi_cf - 2));
document.querySelector('#high')
.setAttribute('stroke-dasharray', semi_cf + ',' + cf);
document.querySelector('#mask')
.setAttribute('stroke-dasharray', semi_cf + ',' + cf);
document.querySelector('#low')
.setAttribute('stroke-dasharray', semi_cf - z + ',' + cf);
var slider = document.querySelector('#slider');
var lbl = document.querySelector("#lbl");
var svg = document.querySelector('#meter');
var high = document.querySelector('#high');
var mask = document.querySelector('#mask');
var low = document.querySelector('#low');
var meter_needle = document.querySelector('#meter_needle');
function range_change_event() {
var percent = slider.value;
var meter_value = semi_cf - ((percent * semi_cf) / 100);
mask.setAttribute('stroke-dasharray', meter_value + ',' + cf);
meter_needle.style.transform = 'rotate(' + (270 + ((percent * 180) / 100)) + 'deg)';
lbl.textContent = percent + '%';
}
slider.addEventListener('input', range_change_event);
</script>
</body>
</html>
I have found on the web plenty of good examples with HTML canvas and D3js but nothing based on SVG..
I am thinking to create an element "line" and add it across the black arc.
What is the simplest way to create the numeric scale?
Here you go:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-250 -250 500 500" width="500" height="500" id="svg">
<defs>
<style>
line {
stroke: black;
stroke-width: 1px;
}
text {
fill: red;
text-anchor: middle;
font-size: 16px;
font-family: sans-serif;
}
rect {
fill: transparent;
}
#id {
display: none;
}
.origin {
fill: green;
}
.outer {
fill: none;
stroke: black;
}
</style>
</defs>
<circle r="5" cx="0" cy="0" class="origin"/>
<path d="M-180,0 a1,1 0 0,1 360,0" class="outer"/>
<g id="gauge" transform="rotate(-90)">
<g id="noon">
<rect x="-10" y="-220" width="20" height="100"/>
<line x1="0" y1="-190" x2="0" y2="-180"/>
<text x="0" y="-200"></text>
</g>
</g>
</svg>
<script>
for (i=0; i<=180; i = i + 18) {
var new_tick = noon.cloneNode(true);
new_tick.getElementsByTagName('text')[0].textContent = i/180 * 100;
new_tick.removeAttribute("id");
new_tick.setAttribute("transform", "rotate(" + i + " 0 0)");
gauge.appendChild(new_tick);
}
</script>
I think this is fairly self-explanatory. (For example the RECT is just a guide which you can turn on - by changing the fill - if you want to better visualize what's going in inside each G.)
Let me know if you have any follow on questions.
Here's a Codepen, if it helps: https://codepen.io/MSCAU/pen/OoQMdV
What is the simplest way to create the numeric scale?
The easiest way to animate the scaling of the svg circle is to animate its radius
<svg version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="400" height="400" viewBox="0 0 400 400" >
<circle cx="200" cy="200" r="10" fill="none" stroke-width="2" stroke="purple" >
<animate attributeName="r" values="1;100;1" dur="4s" repeatCount="indefinite" />
</circle>
</svg>
I'm able to center text for each path/shape explicitly, but I don't want to do the math for each one. I've tried this the code below but it just puts the two texts on top of one another, which would make sense if it's referring to the one <svg> and not each <g> or <path>.
<svg viewBox="0 0 91.742 214.2" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#000" stroke-width="1">
<g>
<text class="room-text" x="50%" y="50%">145</text>
<path class="room" d="m 0.134593,0.134751 2.2679,133.799999 86.179,0.75595 -5.2917,-133.049999 z"/>
</g>
<g>
<text class="room-text" x="50%" y="50%">146</text>
<path class="room" d="m 2.402593,133.94175 0.75595,80.131 88.446,-0.75594 -3.0238,-78.619 z"/>
</g>
</svg>
I've also tried anchor-text and baseline-alignment but an explicit x and y value are still needed.
Yeah. I ended up just doing it through JavaScript.
document.addEventListener("DOMContentLoaded", function(event) {
var svg = document.getElementById("svg");
/***** Get the room box and text box *****/
for (i = 1; i < svg.childNodes[1].childNodes.length; i += 2) {
var room = svg.childNodes[1].childNodes[i].childNodes[3]; // path element
var textbox = svg.childNodes[1].childNodes[i].childNodes[1]; // text element
var new_x = document.createAttribute("x"); // make a new attribute to add to the textbox
new_x.value = room.getBBox().width / 2 + room.getBBox().x - textbox.getBBox().width / 2; // calculate the textbox's new x position
textbox.setAttributeNode(new_x) // assign the textbox the new x value
}
});
body {
box-sizing: border-box;
}
.main {
width: 100%;
}
svg {
display: block;
margin: 0 auto;
width: 400px;
height: 400px;
}
.room {
fill: rgba(220, 220, 220, .4);
}
.room:hover {
fill: rgba(0, 255, 255, 0.4);
}
.room-text {
fill: black;
font-size: 14px;
font-family: "Helvetica";
}
<div id="svg">
<svg viewBox="0 0 181.01 255.02" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="#000" stroke-width="1">
<g id="r145">
<text class="room-text" y="40">145</text>
<path class="room" d="m0.13374 1.6456 0.75595 68.792 135.32-2.2679-4.5357-68.036z"/>
</g>
<g>
<text class="room-text" y="130">146</text>
<path class="room" d="m0.88969 70.437 29.482 103.57 150.43-3.7798-44.601-102.05z"/>
</g>
<g>
<text class="room-text" y="220">147</text>
<path class="room" d="m30.372 174 42.333 79.375 96.762 1.5119 11.339-84.667z"/>
</g>
</svg>
</div>
Why, "box-sizing: border-box;" only when it is used, and become a point?
Please tell me whether guided by what formula?
Chrome Version
44.0.2403.157 m
Do not use box-sizing
<style>
svg {
border: 1px solid #ddd;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<svg preserveAspectRatio="none" viewBox="0 0 2000 400" width="500" height="400">
<rect id="hoge" x="50" y="50" width="50" height="50"/>
</svg>
<script>
console.log($("#hoge").offset()); //Object {top: 59, left: 21.5}
</script>
use box-sizing
<style>
svg {
border: 1px solid #ddd;
}
* {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<svg preserveAspectRatio="none" viewBox="0 0 2000 400" width="500" height="400">
<rect id="hoge" x="50" y="50" width="50" height="50"/>
</svg>
<script>
console.log($("#hoge").offset()); //Object {top: 58.75, left: 21.450000762939453}
</script>
When you use border-box the width of content is changed, instead of using 400px in height it can now only use (400px - 2px) 398px. Because of that the svg itself has to displayed smaller.
The underlaying calculations for your example looks as follows:
offset.top = margin of body + border + (svg content height / viewBox height * rect y)
In your first example this means
:8px (default value) + 1px + (400px / 2000 * 50) = 59
Your second example:
8px (default value) + 1px + (398px / 400 * 50) = 58.75
Read this css-tricks article for further explanation of the box model.