Cross-Browser SVG rendering problems with circle and stroke-dasharray - svg

my problem is explained quite simply. I've gotten a screenshot of the situation and snippet a jsFiddle code.
The problem I have, is clearly visible on the screenshot, the circular sections look perfectly in the Chrome browser, but in FireFox & Edge etc. the sections are slightly offset.
Prior to the current status, I had set the r / cx / cy properties to css, but that was not compatible either. I found out that you have to write them directly into the circle tag.
Has anyone ever had the problem, I mean, but can anyone explain why it does not work as expected?
[EDIT] THANKS #Sphinxxx for answer the basic question of y doesn't work that.
Is there a hack / workaround to solve the problem?
Screenshot:
Browser on this Screen:
1. Chrome
2. FireFox
3. Edge
[UPDATE] (In the current version of FireFox that issue is fixed)
Now we have that problem only in the Edge browser
Here to the code example:
const duration = 1200
Array.from(document.getElementsByClassName('count')).forEach(el => {
const target = parseInt(el.innerText)
const step = (duration / target)
const increment = step < 10 ? Math.round(10 / step) : 1
let current = 0
console.log(el.innerText + ': ' + step)
el.innerText = current
window.addEventListener('load', _ => {
const timer = setInterval(_ => {
current += increment
if (current >= target) {
el.innerText = target
clearInterval(timer)
} else
el.innerText = current
}, step)
})
})
function getlength(number) {
return number.toString().length;
}
svg.chart {
width: 100px;
border-radius: 50%;
background: yellowgreen;
transform: rotate(-90deg);
animation: grow-up cubic-bezier(0, 0, 0.18, 1) 2s;
animation-delay: 0.3s;
}
.chart > circle {
fill: none;
stroke-width: 32;
}
.chart > circle.first {
stroke: deeppink;
}
.chart > circle.second {
stroke: mediumpurple;
}
.chart > circle.third {
stroke: #fb3;
}
.chart > circle.fourth {
stroke: #ce3b6a;
}
.legend-list li{
width: 40%;
}
.legend-list span.glyphicon {
color: yellowgreen;
}
.legend-list .first span.glyphicon {
color: deeppink;
}
.legend-list .second span.glyphicon {
color: mediumpurple;
}
.legend-list .third span.glyphicon {
color: #fb3;
}
.legend-list .fourth span.glyphicon {
color: #ce3b6a;
}
svg circle {
animation: rotate-in cubic-bezier(0, 0, 0.18, 1) .7s;
animation-delay: 0.3s;
transform-origin: 50% 50%
}
#keyframes rotate-in {
from {
opacity: 0;
stroke-dashoffset: 30;
}
to {
opacity: 1;
stroke-dashoffset: 0;
}
}
#keyframes grow-up {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
<svg class="chart" viewBox="0 0 32 32">
<!-- circle zero from 0 to 100 for filling yellowgreen --> <!-- 75 - 100 = 25 % -> realy 0 - 100 background color -->
<circle class='fourth' stroke-dasharray="75 100" r="16" cx="16" cy="16"></circle> <!-- 60 - 75 = 15 % -->
<circle class='third' stroke-dasharray="60 100" r="16" cx="16" cy="16"></circle> <!-- 40 - 60 = 20 % -->
<circle class='second' stroke-dasharray="40 100" r="16" cx="16" cy="16"></circle> <!-- 30 - 40 = 10 % -->
<circle class='first' stroke-dasharray="30 100" r="16" cx="16" cy="16"></circle> <!-- 0 - 30 = 30 % -->
</svg>

Both Edge and Firefox clearly do something wrong when drawing circles where the stroke meets itself in the circle center. Your example can be simplified to this:
<svg class="chart" width="320" height="340" viewBox="1 0 32 34">
<circle cx="16" cy="1" r="8" stroke-width="15.5" stroke="green" stroke-dasharray="20 999" fill="none"></circle>
<circle cx="16" cy="18" r="8" stroke-width="16" stroke="blue" stroke-dasharray="20 999" fill="none"></circle>
</svg>
The green circle has a stroke that's just a little bit too thin to reach the center, and looks like you would expect, with a tiny hole in the middle. The blue circle should perfectly close that gap, but somehow overshoots in a strange way:
The problem might be related to this: Paths: Stroking and Offsetting, but doesn't quite look the same.

Related

Donut Chart connect all arcs

Drawing svg donut chart using circle. I need to connect all arcs, instead gap between them. is it possible?
I set stroke-dasharray="${item.percent} 100" but it doesn't work. also I set pathLength="${100+0*arr.length}" but it doesn't work either. See the code below
var DonutSlice = [{
id: 1,
percent: 60,
color: 'DarkSeaGreen',
label: 'Slice 1'
},
{
id: 2,
percent: 30,
color: 'DarkOrchid',
label: 'Slice 2'
},
{
id: 3,
percent: 10,
color: 'Tomato',
label: 'Slice 3'
}
];
var circlecontainer = document.getElementById('circlecontainer');
var output = document.getElementById('output');
circlecontainer.innerHTML = DonutSlice.map((item, i, arr) => {
let offset = 1 * i + arr.filter((item, j) => j < i)
.reduce((total, item) => total + item.percent, 0);
return `<circle data-id="${item.id}" stroke="${item.color}"
cx="80" cy="80" r="79" class="slice"
pathLength="${100+0*arr.length}"
stroke-dasharray="${item.percent} 100"
stroke-dashoffset="-${offset}" />`;
}).join('');
circlecontainer.addEventListener('mouseover', e => {
let slice = e.target.closest('.slice');
if (slice) {
item = DonutSlice.find(item => item.id == parseInt(slice.dataset.id));
output.innerHTML = `${item.label} was clicked`;
}
});
.slice {
/* stroke-linecap: round; */
stroke-width: 15;
fill: none;
cursor: pointer;
}
p#output {
text-align: center;
}
<div style="width: 200px">
<svg class="svg2" width="174" height="174" preserveAspectRatio="xMinYMin meet"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 174 174" version="1.1">
<circle fill="#000000" cx="87" cy="87" r="87" />
<g id="circlecontainer" transform="rotate(0 0 0)"></g>
</svg>
<p id="output"></p>
</div>
Instead of using stroke-dashoffset I prefer to use transform rotate. So the offset value needs to be a number of degrees. I added / 100 * 360 to the calculation.
If you use the circlecontainer group to position the chart you can skip the cx and cy attributes on the circle elements.
Update
There was a gap between the slices. I changed the pathLength to 360 so that is matches the rotation. Then I add 1 degree to the length of the slice and rotate the slice 1 degree. Now, the end of each slice will overlap the slice next to it.
var DonutSlice = [{
id: 1,
percent: 60,
color: 'DarkSeaGreen',
label: 'Slice 1'
},
{
id: 2,
percent: 30,
color: 'DarkOrchid',
label: 'Slice 2'
},
{
id: 3,
percent: 10,
color: 'Tomato',
label: 'Slice 3'
}
];
var circlecontainer = document.getElementById('circlecontainer');
var output = document.getElementById('output');
circlecontainer.innerHTML = DonutSlice.map((item, i, arr) => {
let offset = arr.filter((item, j) => j < i)
.reduce((total, item) => total + item.percent, 0) / 100 * 360;
return `<circle data-id="${item.id}" stroke="${item.color}"
r="72" class="slice"
pathLength="360"
stroke-dasharray="${item.percent / 100 * 360 + 1} 360"
transform="rotate(${offset - 1})" />`;
}).join('');
circlecontainer.addEventListener('mouseover', e => {
let slice = e.target.closest('.slice');
if (slice) {
item = DonutSlice.find(item => item.id == parseInt(slice.dataset.id));
output.innerHTML = `${item.label} was clicked`;
}
});
.slice {
/* stroke-linecap: round; */
stroke-width: 16;
fill: none;
cursor: pointer;
}
p#output {
text-align: center;
}
<div style="width: 200px">
<svg class="svg2" preserveAspectRatio="xMinYMin meet"
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 164 164">
<circle fill="#000000" cx="84" cy="84" r="80" />
<g id="circlecontainer" transform="translate(80 80) rotate(-90)"></g>
</svg>
<p id="output"></p>
</div>
Your offset calculation adds a gap.
Change it to (delete the additional 1 * i +):
let offset = arr.filter((item, j) => j < i)
.reduce((total, item) => total + item.percent, 0);
var DonutSlice = [{
id: 1,
percent: 60,
color: 'DarkSeaGreen',
label: 'Slice 1'
},
{
id: 2,
percent: 30,
color: 'DarkOrchid',
label: 'Slice 2'
},
{
id: 3,
percent: 10,
color: 'Tomato',
label: 'Slice 3'
}
];
var circlecontainer = document.getElementById('circlecontainer');
var output = document.getElementById('output');
circlecontainer.innerHTML = DonutSlice.map((item, i, arr) => {
let offset = arr.filter((item, j) => j < i)
.reduce((total, item) => total + item.percent, 0);
return `<circle data-id="${item.id}" stroke="${item.color}"
cx="80" cy="80" r="79" class="slice"
pathLength="${100+0*arr.length}"
stroke-dasharray="${item.percent} 100"
stroke-dashoffset="-${offset}" />`;
}).join('');
circlecontainer.addEventListener('mouseover', e => {
let slice = e.target.closest('.slice');
if (slice) {
item = DonutSlice.find(item => item.id == parseInt(slice.dataset.id));
output.innerHTML = `${item.label} was clicked`;
}
});
.slice {
/* stroke-linecap: round; */
stroke-width: 15;
fill: none;
cursor: pointer;
}
p#output {
text-align: center;
}
<div style="width: 200px">
<svg class="svg2" width="174" height="174" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 174 174" version="1.1">
<circle fill="#000000" cx="87" cy="87" r="87" />
<g id="circlecontainer" transform="rotate(0 0 0)"></g>
</svg>
<p id="output"></p>
</div>

Fix linear gradient of SVG circle

I have the following circle progress bar. Everything is fine except for the gradient. The circle is actually 2 arcs. And when the script draws the first arc the gradient is red->blue. So the start of the circle is red. And the tail is blue. But when the second arc is being drawn the start of the gradient switches to blue and I don't know how to fix it. I don't want it to switch colors. I want the gradient to always be the same
function update(percentage) {
var width = 160,
height = 160,
cx = width / 2,
cy = height / 2,
start_angle = 0,
barsize = 10;
var r = Math.min(cx, cy) - barsize / 2;
if (percentage === 100) {
percentage -= 0.0001;
}
var end_angle = start_angle + percentage * Math.PI * 2 / 100;
var x1 = cx + r * Math.sin(start_angle),
y1 = cy - r * Math.cos(start_angle),
x2 = cx + r * Math.sin(end_angle),
y2 = cy - r * Math.cos(end_angle);
// This is a flag for angles larger than than a half circle
// It is required by the SVG arc drawing component
var big = 0;
if (end_angle - start_angle > Math.PI) big = 1;
// This string holds the path details
var d = "M" + x1 + "," + y1 + // Start at (x1,y1)
" A" + r + "," + r + // Draw an arc of radius r
" 0 " + big + " 1 " + // Arc details...
x2 + "," + y2;
document.getElementById('path').setAttribute('d', d);
}
function animate(start, finish) {
setTimeout(function() {
update(start);
console.log(document.getElementsByClassName('progress__content'))
let element = document.getElementsByClassName('progress__content')[0];
element.textContent = start + '%';
start += 1;
if (start <= finish) {
animate(start, finish);
} else {
return;
}
}, 10);
}
function go() {
animate(0, 100);
}
.progress {
position: absolute;
top: 50%;
left: 50%;
margin-top: -100px;
margin-left: 100px;
width: 200px;
height: 200px;
}
.progress__content {
position: absolute;
left: 50%;
top: 50%;
margin-left: -50px;
margin-top: -23px;
font-family: Helvetica;
font-size: 40px;
width: 103px;
height: 47px;
text-align: center;
}
body {
background: #f1f1f1;
}
<button onclick="go()">Click me</button>
<div class="progress clip-svg">
<div class="progress__content">0%</div>
<svg width="160" height="160">
<defs>
<linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop stop-color="#EE3028" offset="0" />
<stop stop-color="#067BC2" offset="1" />
</linearGradient>
</defs>
<ellipse rx="75" ry="75" cx="80" cy="80" stroke="#f2f2f2" fill="none" stroke-width="10"></ellipse>
<g>
<path id="path" stroke-width="10" stroke="url(#gradient)" fill="none" d="">
</path>
</g>
</svg>
</div>
The gradient changes because you define the gradient on the entire path, and as the path object grows down and left, the gradient stop positions are continuously redefined as well. The solution is to change your gradientUnits to userSpaceOnUse - so the gradient is defined vs. the drawing surface/viewBox vs. relative to the object. Possible implementation below (I'm not entirely sure what color scheme you're aiming for - but tweak the stop locations & colors until you have what you want).
function update(percentage) {
var width = 160,
height = 160,
cx = width / 2,
cy = height / 2,
start_angle = 0,
barsize = 10;
var r = Math.min(cx, cy) - barsize / 2;
if (percentage === 100) {
percentage -= 0.0001;
}
var end_angle = start_angle + percentage * Math.PI * 2 / 100;
var x1 = cx + r * Math.sin(start_angle),
y1 = cy - r * Math.cos(start_angle),
x2 = cx + r * Math.sin(end_angle),
y2 = cy - r * Math.cos(end_angle);
// This is a flag for angles larger than than a half circle
// It is required by the SVG arc drawing component
var big = 0;
if (end_angle - start_angle > Math.PI) big = 1;
// This string holds the path details
var d = "M" + x1 + "," + y1 + // Start at (x1,y1)
" A" + r + "," + r + // Draw an arc of radius r
" 0 " + big + " 1 " + // Arc details...
x2 + "," + y2;
document.getElementById('path').setAttribute('d', d);
}
function animate(start, finish) {
setTimeout(function() {
update(start);
console.log(document.getElementsByClassName('progress__content'))
let element = document.getElementsByClassName('progress__content')[0];
element.textContent = start + '%';
start += 1;
if (start <= finish) {
animate(start, finish);
} else {
return;
}
}, 10);
}
function go() {
animate(0, 100);
}
.progress {
position: absolute;
top: 50%;
left: 50%;
margin-top: -100px;
margin-left: 100px;
width: 200px;
height: 200px;
}
.progress__content {
position: absolute;
left: 50%;
top: 50%;
margin-left: -50px;
margin-top: -23px;
font-family: Helvetica;
font-size: 40px;
width: 103px;
height: 47px;
text-align: center;
}
body {
background: #f1f1f1;
}
<button onclick="go()">Click me</button>
<div class="progress clip-svg">
<div class="progress__content">0%</div>
<svg width="160" height="160">
<defs>
<linearGradient id="gradient" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="160" y2="0">
<stop stop-color="#EE3028" offset="0" />
<stop stop-color="#067BC2" offset="160" />
</linearGradient>
</defs>
<ellipse rx="75" ry="75" cx="80" cy="80" stroke="#f2f2f2" fill="none" stroke-width="10"></ellipse>
<g>
<path id="path" stroke-width="10" stroke="url(#gradient)" fill="none" d="">
</path>
</g>
</svg>
</div>

Jquery - Draggable feature Containment property for a polygonal parent

Referencing https://jqueryui.com/draggable/ i am able to implement a drag drop feature within a parent element (e.g. div). However my need is to have this draggable feature to work within a polygonal element (Like a SVG polygon).
I have been searching the net, however there are examples of how to make a svg polygon draggable but not 'how to contain drag drop feature within a polygonal parent (div).
Any ideas / pointers will be helpful.
Thanks.
The short story is you need a function to check if a point is within a polygon, and then check if the four corners of your draggable object are within that shape.
Here's a rough example of doing that, using the draggable sample from jQuery, along with a point in polygon function from this answer. This example is far from perfect, but I hope it points you in the right direction.
// These are the points from the polygon
var polyPoints = [
[200, 27],
[364, 146],
[301, 339],
[98, 339],
[35, 146]
];
$("#draggable").draggable({
drag: function(e, ui) {
var element = $("#draggable")[0];
var rect = element.getBoundingClientRect();
var rectPoints = rect2points(rect);
let inside = true;
rectPoints.forEach(p => {
if(!pointInside(p, polyPoints)){
inside = false;
}
});
$("#draggable")[inside ? 'addClass' : 'removeClass']('inside').text(inside ? 'Yay!' : 'Boo!');
}
});
function rect2points(rect) {
return ([
[rect.left, rect.top],
[rect.right, rect.top],
[rect.right, rect.bottom],
[rect.left, rect.bottom]
]);
};
function pointInside(point, vs) {
var x = point[0],
y = point[1];
var inside = false;
for (var i = 0, j = vs.length - 1; i < vs.length; j = i++) {
var xi = vs[i][0],
yi = vs[i][1];
var xj = vs[j][0],
yj = vs[j][1];
var intersect = ((yi > y) != (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
};
#draggable {
width: 100px;
height: 80px;
background: red;
position: absolute;
text-align: center;
padding-top: 20px;
color:#fff;
}
#draggable.inside{
background: green;
}
html, body{
margin: 0;
}
<script src="//code.jquery.com/jquery-1.12.4.js"></script>
<script src="//code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<div id="draggable">Drag me</div>
<svg width="400px" height="400px" viewBox="0 0 400 400">
<rect width="600" height="600" fill="#efefef"></rect>
<polygon points="200,27 364,146 301,339 98,339 35,146" fill="rgba(255,200,0, 1)" stroke="rgba(255,0,0,0.2" stroke-width="2"></polygon>
</svg>

SVG text background color with border radius and padding that matches the text width

I need to wrap a background around a text element inside an SVG, it needs to have padding and a border radius. The issue is the text will be dynamic so I need the background to expand the width of the text. I found a solution to this using foreign object but this isn't support in IE 11 which is a problem. Can anyone suggest a workaround.
if you can use script, you can use this little function. It handles some of the CSS values. You could however implement whatever you need...
function makeBG(elem) {
var svgns = "http://www.w3.org/2000/svg"
var bounds = elem.getBBox()
var bg = document.createElementNS(svgns, "rect")
var style = getComputedStyle(elem)
var padding_top = parseInt(style["padding-top"])
var padding_left = parseInt(style["padding-left"])
var padding_right = parseInt(style["padding-right"])
var padding_bottom = parseInt(style["padding-bottom"])
bg.setAttribute("x", bounds.x - parseInt(style["padding-left"]))
bg.setAttribute("y", bounds.y - parseInt(style["padding-top"]))
bg.setAttribute("width", bounds.width + padding_left + padding_right)
bg.setAttribute("height", bounds.height + padding_top + padding_bottom)
bg.setAttribute("fill", style["background-color"])
bg.setAttribute("rx", style["border-radius"])
bg.setAttribute("stroke-width", style["border-top-width"])
bg.setAttribute("stroke", style["border-top-color"])
if (elem.hasAttribute("transform")) {
bg.setAttribute("transform", elem.getAttribute("transform"))
}
elem.parentNode.insertBefore(bg, elem)
}
var texts = document.querySelectorAll("text")
for (var i = 0; i < texts.length; i++) {
makeBG(texts[i])
}
text {
background: red;
border-radius: 5px;
border: 2px solid blue;
padding: 5px
}
text:nth-of-type(2) {
background: orange;
border-color: red
}
g text {
border-width: 4px
}
<svg width="400px" height="300px">
<text x="20" y="40">test text</text>
<text x="20" y="80" transform="rotate(10,20,55)">test with transform</text>
<g transform="translate(0,100) rotate(-10,20,60) ">
<text x="20" y="60">test with nested transform</text>
</g>
</svg>

Make "widget" for SVG-group in D3.js

I am using D3 to manipulate an SVG, that contains several "widgets" that have behavior and can be controlled eg via events. For instance I have a spinning fan. It should be possible to turn on and off the fan. I have been able to build that in D3, but not in an elegant way, and with all the code being global. What I want to end up with is something as below:
Creation of "widget", as known from jQuery:
d3.select('svg').append('g').attr('id', 'myFan').fan();
And then I would like to turn on and off with smth like the following, also as known from jQuery:
d3.select('#myFan').fan('start')
d3.select('#myFan').fan('stop')
Is it possible to achieve this in D3, ie to create such a "widget"?
Is there generally a different approach to such a problem, when using D3?
My current unelegant solution with global code:
Robert asked for this. Code at Codepen: link.
Javascript
var fan = d3.select('body')
.append('svg')
.attr('width', 200)
.attr('height', 200)
.append('g')
.attr('class', 'fan')
.attr('id', 'myFan')
.attr('transform', 'translate(75,75)')
// Static, background disc
fan.append('circle')
.attr('cx',0)
.attr('cy',0)
.attr('r',60)
// Dynamic, rotating path
var blade = fan.append('path')
.attr('d', 'M 0 0 L -30 15 L -60 0 Z L 30 -15 L 60 0 Z ')
.attr('transform', 'translate(50,50)')
.attr('transform', 'rotate(90)');
// Static, hub
fan.append('circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', 10)
var rotation = 0; // degrees
var speed = 10; // degree per call
function rotate() {
rotation = (rotation + speed) % 360;
blade.attr('transform', 'rotate('+rotation+')')
}
setInterval(rotate, 100);
function stop() { speed = 0; }
function start() { speed = 10; }
var ctrlPanel = d3.select('body').append('div');
ctrlPanel.append('button').text('Start').on('click', start);
ctrlPanel.append('button').text('Stop').on('click', stop);
CSS
path {
fill: red;
strok: blue;
stroke-width: 3px;
}
.fan circle {
fill: lightgray;
stroke: black;
stroke-width: 3px;
}
.fan path {
fill: darkgray;
stroke: black;
stroke-width: 3px;
}

Resources