Recreating the dynamic donut chart with SVG and Vue - javascript

I need a dynamic donut chart with SVG and Vue.
It has to be exactly the same SVG as on this page:
https://www.chipotle.co.uk/nutrition-calculator (to see the chart diagram in motion, first you must choose a dish and then click on some ingredients)
Maybe it was bad direction but that is what I was able find and modify to the task
(https://codepen.io/fifuruho/pen/zYOBWLx)
All code in codeopen.io
I'm really bad at complex svg's.
I need any solutions or tips you can offer.
(but better in code examples)

I would do this in a different way. Instead of using strokes I would use paths. I would make the paths touching one with another and next I would apply the same path (stroke:#000; stroke-width:3; fill: #ffffff) as a mask. You can change the stroke width for a different gap.
This would be the resulting svg:
svg{border:1px solid}
mask use{stroke:#000; stroke-width:3; fill: #ffffff}
<svg width="320" viewBox="-80 -80 160 160" class="donut-chart">
<defs id="theDefs">
<path d="M5.475206276408082,-34.56909192082982 L10.16824022761501,-64.19974213868396 A65,65 0 0 1 10.16824022761501,64.19974213868396 L5.475206276408082,34.56909192082982 A35,35 0 0 0 5.475206276408082,-34.56909192082982" id="elmt45" style="mask: url(#mask45)"></path>
<mask id="mask45">
<use xlink:href="#elmt45"></use>
</mask>
<path d="M18.75393782426488,-29.55147739257053 L34.828741673634774,-54.88131515763098 A65,65 0 0 1 34.828741673634774,54.88131515763098 L18.75393782426488,29.55147739257053 A35,35 0 0 0 18.75393782426488,-29.55147739257053" id="elmt32" style="mask: url(#mask32)"></path>
<mask id="mask32">
<use xlink:href="#elmt32"></use>
</mask>
<path d="M26.253887437066084,-23.145915286327813 L48.75721952597986,-42.98527124603737 A65,65 0 0 1 48.75721952597986,42.98527124603737 L26.253887437066084,23.145915286327813 A35,35 0 0 0 26.253887437066084,-23.145915286327813" id="elmt23" style="mask: url(#mask23)"></path>
<mask id="mask23">
<use xlink:href="#elmt23"></use>
</mask>
</defs>
<g id="theG">
<use xlink:href="#elmt45" fill="rgb(182, 130, 7)" transform="rotate(-9)"></use><use xlink:href="#elmt32" fill="rgb(120, 98, 89)" transform="rotate(129.6)"></use>
<use xlink:href="#elmt23" fill="rgb(195, 180, 166)" transform="rotate(228.6)"></use>
</g>
</svg>
As for the Javascript to produce this, I'm using plain javascript. I hope you'll be able to translate it to Vue.
const SVG_NS = "http://www.w3.org/2000/svg";
const SVG_XLINK = "http://www.w3.org/1999/xlink";
let colors = ["rgb(182, 130, 7)", "rgb(120, 98, 89)", "rgb(195, 180, 166)"];
class Sector {
constructor(color, prev, a) {
this.color = color;
this.R = 65; //external radius
this.r = 35; //inner radius
this.a = a * 360 / 100; //360degs = 100%
this.A = this.a * Math.PI / 180; // angle in radians
this.prev = prev;
this.color = color;
this.elmt = document.createElementNS(SVG_NS, "path");
document.querySelector("#theDefs").appendChild(this.elmt);
this.p1 = {};
this.p2 = {};
this.p3 = {};
this.p4 = {};
this.p1.x = this.r * Math.cos(-this.A / 2);
this.p1.y = this.r * Math.sin(-this.A / 2);
this.p2.x = this.R * Math.cos(-this.A / 2);
this.p2.y = this.R * Math.sin(-this.A / 2);
this.p3.x = this.R * Math.cos(this.A / 2);
this.p3.y = this.R * Math.sin(this.A / 2);
this.p4.x = this.r * Math.cos(this.A / 2);
this.p4.y = this.r * Math.sin(this.A / 2);
this.d = `M${this.p1.x},${this.p1.y} L${this.p2.x},${this.p2.y} A${
this.R
},${this.R} 0 0 1 ${this.p3.x},${this.p3.y} L${this.p4.x},${this.p4.y} A${
this.r
},${this.r} 0 0 0 ${this.p1.x},${this.p1.y}`;
this.elmt.setAttributeNS(null, "d", this.d);
this.elmt.setAttribute("id", `elmt${a}`);
this.mask = document.createElementNS(SVG_NS, "mask");
this.use = document.createElementNS(SVG_NS, "use");
this.use.setAttributeNS(SVG_XLINK, "xlink:href", `#elmt${a}`);
this.mask.appendChild(this.use);
this.mask.setAttribute("id", `mask${a}`);
document.querySelector("#theDefs").appendChild(this.mask);
this.use1 = document.createElementNS(SVG_NS, "use");
this.use1.setAttributeNS(SVG_XLINK, "xlink:href", `#elmt${a}`);
this.use1.setAttributeNS(null, "fill", this.color);
theG.appendChild(this.use1);
this.elmt.setAttribute("style", `mask: url(#mask${a})`);
this.use1.setAttributeNS(
null,
"transform",
`rotate(${-90 + this.a / 2 + this.prev})`
);
}
}
let s1 = new Sector(colors[0], 0, 45);
let s2 = new Sector(colors[1], s1.a, 32);
let s3 = new Sector(colors[2], s1.a + s2.a, 23);
svg{border:1px solid}
mask use{stroke:#000; stroke-width:3; fill: #ffffff}
<svg height="250" width="320" viewBox="-80 -80 160 160" class="donut-chart">
<defs id="theDefs"></defs>
<g id="theG">
</g>
</svg>

Related

JS SVG: get path from SVG polgon

I have the following SVG element which was created using JS: https://akzhy.com/blog/create-animated-donut-chart-using-svg-and-javascript
<div class="doughnut">
<svg width="100%" height="100%" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="30" stroke="#80e080" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="141.372" transform='rotate(-90 50 50)'/>
<circle cx="50" cy="50" r="30" stroke="#4fc3f7" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="103.6728" transform='rotate(0 50 50)'/>
<circle cx="50" cy="50" r="30" stroke="#9575cd" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="169.6464" transform='rotate(162 50 50)'/>
<circle cx="50" cy="50" r="30" stroke="#f06292" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="150.7968" transform='rotate(198 50 50)'/>
</svg>
</div>
Is it possible to to get a path from the svg node?
Conversion via graphic app
Open you svg in an application like Illustrator/inkscape etc.
You could use path operations like "stroke-to-path" to convert stroke based chart segments (the visual colored segments are just dashed strokes applied to a full circle).
Use a pie/donut generator script returning solid paths
Based on this answer by #ray hatfield Pie chart using circle element you can calculate d properties based on the arc command.
Example json based pie chart generator
let pies = document.querySelectorAll('.pie-generate');
generatePies(pies);
function generatePies(pies) {
if (pies.length) {
pies.forEach(function(pie, i) {
let data = pie.getAttribute('data-pie');
if (data) {
data = JSON.parse(data);
w = data['width'];
h = data['height'];
let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
svg.setAttribute('width', w);
svg.setAttribute('height', h);
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
pie.appendChild(svg);
addSegments(svg, data);
}
})
}
}
function addSegments(svg, data) {
let segments = data["segments"];
let strokeWidth = data["strokeWidth"];
let centerX = data["centerX"];
let centerY = data["centerY"];
let radius = data["radius"];
let startingAngle = (data["startingAngle"] || data["startingAngle"] == 0) ? data["startingAngle"] : -90;
let gap = data["gap"];
let decimals = data["decimals"];
let offset = 0;
let output = "";
// calculate auto percentages
let total = 0;
let calc = data["calc"] ? true : false;
if (calc) {
segments.forEach(function(segment, i) {
total += segment[0];
});
}
// prevent too large gaps
let circumference = Math.PI * radius * 2;
let circumferencePerc = (circumference / 100);
let gapPercentOuter = 100 / circumference * gap;
if (gapPercentOuter > circumferencePerc) {
gap = gap / (gapPercentOuter / circumferencePerc)
}
segments.forEach(function(segment, i) {
let percent = segment[0];
// calc percentages
let percentCalc = percent.toString().indexOf('/') != -1 ? segment[0].split('/') : [];
percent = percentCalc.length ? percentCalc[0] / percentCalc[1] * 100 : +percent
// calculate auto percentages to get 100% in total
if (total) {
percent = 100 / total * percent;
}
let percentRound = percent.toFixed(decimals);
// auto fill color
let segOptions = segment[1] ? segment[1] : '';
let fill = segOptions ? 'fill="' + segOptions['color'] + '"' : "";
if (!fill) {
let hueCut = 0;
let hueShift = 0;
let hue = Math.abs((360 - hueCut) / 100 * (offset + percent)) + hueShift;
let autoColor = hslToHex(hue.toFixed(0) * 1, 60, 50);
fill = 'fill="' + autoColor + '"';
}
let className = segOptions['class'] ? segOptions['class'] : "";
let classPercent = percentRound.toString().replaceAll('.', '_');
let id = segOptions['id'] ? 'id="' + segOptions['id'] + '" ' : '';
let d = getArcD(centerX, centerY, strokeWidth, offset, percent, radius, gap, decimals, startingAngle);
output +=
`\n<path d="${d}" ${fill} class="segment segment-${classPercent} segment-${(i+1)} ${className}" ${id} data-percent="${percentRound}"/>`;
offset += percent;
});
svg.innerHTML = output;
}
function getArcD(centerX, centerY, strokeWidth, percentStart, percent, radiusOuter, gap, decimals = 3, startingAngle = -90) {
let radiusInner = radiusOuter - strokeWidth;
let circumference = Math.PI * radiusOuter * 2;
let isPieChart = false;
// if pie chart – stroke equals radius
if (strokeWidth + gap >= radiusOuter) {
isPieChart = true;
}
let circumferenceInner = Math.PI * radiusInner * 2;
let gapPercentOuter = ((100 / circumference) * gap) / 2;
let gapPercentInner = ((100 / circumferenceInner) * gap) / 2;
//add offset from previous segments
percentStart = percentStart;
let percentEnd = percent + percentStart;
// outer coordinates
let [x1, y1] = getPosOnCircle(centerX, centerY, (percentStart + gapPercentOuter), radiusOuter, decimals, startingAngle);
let [x2, y2] = getPosOnCircle(centerX, centerY, percentEnd - gapPercentOuter, radiusOuter, decimals, startingAngle);
// switch arc output between long or short arc segment according to percentage
let longArc = percent >= 50 ? 1 : 0;
let rotation = 0;
let clockwise = 1;
let counterclockwise = 0;
let d = '';
// if donut chart
if (!isPieChart) {
//inner coordinates
let [x3, y3] = getPosOnCircle(centerX, centerY, percentEnd - gapPercentInner, radiusInner, decimals, startingAngle);
let [x4, y4] = getPosOnCircle(centerX, centerY, percentStart + gapPercentInner, radiusInner, decimals, startingAngle);
d = [
"M", x1, y1,
"A", radiusOuter, radiusOuter, rotation, longArc, clockwise, x2, y2,
"L", x3, y3,
"A", radiusInner, radiusInner, rotation, longArc, counterclockwise, x4, y4,
"z"
];
}
// if pie chart – stroke equals radius: drop inner radius arc
else {
// find opposite coordinates
let [x1o, y1o] = getPosOnCircle(centerX, centerY, (percentStart - gapPercentOuter) - 50, radiusOuter, decimals, startingAngle);
let [x2o, y2o] = getPosOnCircle(centerX, centerY, (percentEnd + gapPercentOuter) - 50, radiusOuter, decimals, startingAngle);
let extrapolatedIntersection = getLinesIntersection(
[x1, y1, x1o, y1o], [x2, y2, x2o, y2o],
decimals);
d = [
"M", x1, y1,
"A", radiusOuter, radiusOuter, rotation, longArc, clockwise, x2, y2,
"L", extrapolatedIntersection.join(" "),
"z"
];
}
return d.join(" ");
}
// helper: get x/y coordinates according to angle percentage
function getPosOnCircle(centerX, centerY, percent, radius, decimals = 3, angleOffset = -90) {
let angle = 360 / (100 / percent) + angleOffset;
let x = +(centerX + Math.cos((angle * Math.PI) / 180) * radius).toFixed(
decimals
);
let y = +(centerY + Math.sin((angle * Math.PI) / 180) * radius).toFixed(
decimals
);
return [x, y];
}
// helper: get intersection coordinates
function getLinesIntersection(l1, l2, decimals = 3) {
let intersection = [];
let c2x = l2[0] - l2[2];
let c3x = l1[0] - l1[2];
let c2y = l2[1] - l2[3];
let c3y = l1[1] - l1[3];
// down part of intersection point formula
let d = c3x * c2y - c3y * c2x;
if (d != 0) {
// upper part of intersection point formula
let u1 = l1[0] * l1[3] - l1[1] * l1[2];
let u4 = l2[0] * l2[3] - l2[1] * l2[2];
// intersection point formula
let px = +((u1 * c2x - c3x * u4) / d).toFixed(decimals);
let py = +((u1 * c2y - c3y * u4) / d).toFixed(decimals);
intersection = [px, py];
}
return intersection;
}
function hslToHex(h, s, l) {
l /= 100;
const a = s * Math.min(l, 1 - l) / 100;
const f = n => {
const k = (n + h / 30) % 12;
const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
};
return `#${f(0)}${f(8)}${f(4)}`;
}
<div class="pie-generate" data-pie='{
"width": 100,
"height": 100,
"radius": 50,
"centerX": 50,
"centerY": 50,
"strokeWidth": 20,
"gap": 0,
"decimals": 3,
"segments": [
["25", {"color":"#80e080", "id":"seg01", "class":"segCustom"}],
["45", {"color":"#4fc3f7", "id":"seg02", "class":"segCustom"}],
["10", {"color":"#9575cd", "id":"seg03", "class":"segCustom"}],
["20", {"color":"#f06292", "id":"seg04", "class":"segCustom"}]
]
}'>
</div>
You can tweak different segment percentages by changing the JSOn data-attribute.
<div class="pie-generate" data-pie='{
"width": 100,
"height": 100,
"radius": 50,
"centerX": 50,
"centerY": 50,
"strokeWidth": 20,
"gap": 0,
"decimals": 3,
"segments": [
["25", {"color":"#80e080", "id":"seg01", "class":"segCustom"}],
["45", {"color":"#4fc3f7", "id":"seg02", "class":"segCustom"}],
["10", {"color":"#9575cd", "id":"seg03", "class":"segCustom"}],
["20", {"color":"#f06292", "id":"seg04", "class":"segCustom"}]
]
}'>
</div>
Segment output:
<path d="M 50 0 A 50 50 0 0 1 100 50 L 80 50 A 30 30 0 0 0 50 20 z" fill="#80e080" />

create svg hexagon points with only only a length

I am trying to create a hexagon using an svg polygon.
I want to create the x and why coordinates but my code is not working.
I thought I could use the trig functions by transforming each point by 60 degrees.
It is clearly not working.
const radius = 25;
const points = [0, 1, 2, 3, 4, 5, 6].map((n) => {
const current = n * 60;
return [radius * Math.cos(current), -radius * Math.sin(current)];
}).map((p) => p.join(','))
.join(' ');
document.querySelector('polygon')
.setAttribute("points", "100,0 50,0 100,100");
<svg width="200px" height="200px" viewBox="0 0 200 200">
<polygon points="" style="fill: #ccffcc; stroke: red;stroke-width: 3;"
/>
</svg>
According to this article, it can be converted to javascript like:
const radius = 50;
const height = 200;
const width = 200;
const points = [0, 1, 2, 3, 4, 5, 6].map((n, i) => {
var angle_deg = 60 * i - 30;
var angle_rad = Math.PI / 180 * angle_deg;
return [width/2 + radius * Math.cos(angle_rad), height/2 + radius * Math.sin(angle_rad)];
}).map((p) => p.join(','))
.join(' ');
document.querySelector('polygon')
.setAttribute("points", points);
<svg width="200px" height="200px" viewBox="0 0 200 200">
<polygon points="" style="fill: #ccffcc; stroke: red;stroke-width: 3;"
/>
</svg>
You have one too many indexes in the example above and your actually adding commas in your first join when you don't need to be. Here is a cleaned-up version.
const generateHexPoints = (radius, height, width) => {
const hexPoints = new Array(6)
for (let i = 0; i < hexPoints.length; i++) {
const angleDeg = 60 * i - 30
const angleRad = (Math.PI / 180) * angleDeg;
hexPoints[i] = [
width/2 + radius * Math.cos(angleRad),
height/2 + radius * Math.sin(angleRad)
];
}
return hexPoints.map((p) => p.join(' ')).join(' ')
}

Why does rotating element make it bounce?

I've got an svg element and I'm adding a simple rotation attribute to it.
Problem is, I want the element to rotate relative to center of itself, not the whole svg, so I specify x and y for rotate, but it has a weird bouncy effect.
let currentAngle = 0;
function rotate() {
d3.select('.group1')
.transition()
.attr('transform', function() {
let bb = this.getBBox();
let rx = bb.x + bb.width / 2;
let ry = bb.y + bb.height / 2;
currentAngle += 90;
return `rotate(${currentAngle}, ${rx}, ${ry})`;
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg viewbox="0 0 1600 1600" width="500" height="500">
<g class="group1" onclick="rotate()">
<rect x="250" y="250" width="100" height="100" />
<circle cx="420" cy="300" r="50" />
</g>
</svg>
As an alternative, I tried adding transform-origin on css, something like transform-origin: 800px 800px; (but with valid center px of course) and while it works in Chrome, it doesn't work in IE and Safari.
Why is supplying x and y on rotate making my element bounce?
There are a few questions floating around that deal with this issue. Here's one that explains a bit what's going on: D3.js animate rotation
The way Mike Bostock does it here: https://bl.ocks.org/mbostock/3305854 is by placing the object in a <g> that is translated to the position you want and then rotating. This might be the easiest way to get a nice animation. For example:
let currentAngle = 0;
function rotate() {
d3.select('rect')
.transition()
.attr('transform', function() {
let bb = this.getBBox();
let rx = bb.x + bb.width / 2;
let ry = bb.y + bb.height / 2;
currentAngle += 45;
return `rotate(${currentAngle}, ${rx}, ${ry})`;
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg viewbox="0 0 1600 1600" width="500" height="500">
<g transform="translate(250, 250)">
<rect x="-50", y="-50" width="100" height="100" onclick="rotate()" />
</g>
</svg>
LOOK AT THIS DIFFERENT
if you select by id or by class and rotate it slowly, you will see the red dot in middle rotation move by little, but select by id i thing it more stable not like select by class it bouncing harder
let currentAngle = 0;
let currentAngle2 = 0;
function rotate() {
d3.select('#aa')
.transition()
.duration(300)
.attr('transform', function() {
let bb = this.getBBox();
let rx = bb.x + bb.width / 2;
let ry = bb.y + bb.height / 2;
d3.select(this).append('circle')
.attr('cx',rx)
.attr('cy',ry)
.attr('r',20)
.attr('fill','red')
currentAngle += 10;
return `rotate(${currentAngle}, ${rx}, ${ry})`;
});
}
function rotate2() {
d3.selectAll('.aa')
.transition()
.attr('transform', function() {
let bb = this.getBBox();
let rx = bb.x + bb.width / 2;
let ry = bb.y + bb.height / 2;
d3.select(this).append('circle')
.attr('cx',rx)
.attr('cy',ry)
.attr('r',20)
.attr('fill','red')
currentAngle2 += 10;
return `rotate(${currentAngle2}, ${rx}, ${ry})`;
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg viewbox="0 0 1600 1600" width="500" height="500">
<g id ='aa' onclick="rotate()">
<rect x="100", y="100" width="100" height="100" />
<circle cx="250" cy="150" r="50"/>
</g>
<g class ='aa' onclick="rotate2()">
<rect x="600", y="100" width="100" height="100" />
<circle cx="750" cy="150" r="50" />
</g>
</svg>
then if you bouncing it with harsh value(big value) you will defintely see the red dot bouncing, but once again select by id doing more stable
let currentAngle = 0;
let currentAngle2 = 0;
function rotate() {
d3.select('#aa')
.transition()
.duration(300)
.attr('transform', function() {
let bb = this.getBBox();
let rx = bb.x + bb.width / 2;
let ry = bb.y + bb.height / 2;
d3.select(this).append('circle')
.attr('cx',rx)
.attr('cy',ry)
.attr('r',20)
.attr('fill','red')
currentAngle += 90;
return `rotate(${currentAngle}, ${rx}, ${ry})`;
});
}
function rotate2() {
d3.selectAll('.aa')
.transition()
.attr('transform', function() {
let bb = this.getBBox();
let rx = bb.x + bb.width / 2;
let ry = bb.y + bb.height / 2;
d3.select(this).append('circle')
.attr('cx',rx)
.attr('cy',ry)
.attr('r',20)
.attr('fill','red')
currentAngle2 += 90;
return `rotate(${currentAngle2}, ${rx}, ${ry})`;
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg viewbox="0 0 1600 1600" width="500" height="500">
<g id ='aa' onclick="rotate()">
<rect x="100", y="100" width="100" height="100" />
<circle cx="250" cy="150" r="50"/>
</g>
<g class ='aa' onclick="rotate2()">
<rect x="600", y="100" width="100" height="100" />
<circle cx="750" cy="150" r="50" />
</g>
</svg>
SOLUTION
you must make it slowly to rotated to end point, you can make tween
using d3.interpolate, and boom... it rotated perfecly by id or by
class
let currentAngle = 0;
let currentAngle2 = 0;
function rotate() {
d3.select('#aa')
.transition()
.duration(300)
.attrTween("transform", tween);
/*
.attr('transform', function() {
let bb = this.getBBox();
let rx = bb.x + bb.width / 2;
let ry = bb.y + bb.height / 2;
d3.select(this).append('circle')
.attr('cx',rx)
.attr('cy',ry)
.attr('r',20)
.attr('fill','red')
currentAngle += 10;
// console.log(rx,ry,currentAngle)
return `rotate(${currentAngle}, ${rx}, ${ry})`;
});
*/
function tween(d, i, a) {
let bb = this.getBBox();
let rx = bb.x + bb.width / 2;
let ry = bb.y + bb.height / 2;
d3.select(this).append('circle')
.attr('cx',rx)
.attr('cy',ry)
.attr('r',20)
.attr('fill','red')
var e = `rotate(${currentAngle}, ${rx}, ${ry})`
currentAngle += 90;
var o = `rotate(${currentAngle}, ${rx}, ${ry})`
return d3.interpolateString(e,o);
}
}
function rotate2() {
d3.selectAll('.aa')
.transition()
.duration(300)
.attrTween("transform", tween);
/*
.attr('transform', function() {
let bb = this.getBBox();
let rx = bb.x + bb.width / 2;
let ry = bb.y + bb.height / 2;
d3.select(this).append('circle')
.attr('cx',rx)
.attr('cy',ry)
.attr('r',20)
.attr('fill','red')
currentAngle2 += 10;
// console.log(rx,ry,currentAngle)
return `rotate(${currentAngle2}, ${rx}, ${ry})`;
});
*/
function tween(d, i, a) {
let bb = this.getBBox();
let rx = bb.x + bb.width / 2;
let ry = bb.y + bb.height / 2;
d3.select(this).append('circle')
.attr('cx',rx)
.attr('cy',ry)
.attr('r',20)
.attr('fill','red')
var e = `rotate(${currentAngle2}, ${rx}, ${ry})`
currentAngle2 += 90;
var o = `rotate(${currentAngle2}, ${rx}, ${ry})`
return d3.interpolateString(e,o);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<svg viewbox="0 0 1600 1600" width="500" height="500">
<g id ='aa' onclick="rotate()">
<rect x="100", y="100" width="100" height="100" />
<circle cx="250" cy="150" r="50"/>
</g>
<g class ='aa' onclick="rotate2()">
<rect x="600", y="100" width="100" height="100" />
<circle cx="750" cy="150" r="50" />
</g>
</svg>

SVG: filled arc animation?

This question is an extension of Define a circle / arc animation in SVG and How to calculate the SVG Path for an arc (of a circle).
I have modified the answer of #opsb as follows:
function calculateArcPath(x, y, radius, spread, startAngle, endAngle){
var innerStart = polarToCartesian(x, y, radius, endAngle);
var innerEnd = polarToCartesian(x, y, radius, startAngle);
var outerStart = polarToCartesian(x, y, radius + spread, endAngle);
var outerEnd = polarToCartesian(x, y, radius + spread, startAngle);
var largeArcFlag = endAngle - startAngle <= 180 ? "0" : "1";
var d = [
"M", outerStart.x, outerStart.y,
"A", radius + spread, radius + spread, 0, largeArcFlag, 0, outerEnd.x, outerEnd.y,
"L", innerEnd.x, innerEnd.y,
"A", radius, radius, 0, largeArcFlag, 1, innerStart.x, innerStart.y,
"L", outerStart.x, outerStart.y, "Z"
].join(" ");
return d;
}
function polarToCartesian(centerX, centerY, radius, angleInDegrees) {
var angleInRadians = (angleInDegrees-90) * Math.PI / 180.0;
return {
x: centerX + (radius * Math.cos(angleInRadians)),
y: centerY + (radius * Math.sin(angleInRadians))
};
}
var startPath = calculateArcPath(250, 250, 50, 30, 0, 30)
var endPath = calculateArcPath(250, 250, 50, 30, 0, 150)
d3.select("path").attr("d", startPath)
d3.select("path").transition().duration(2000).attr("d", endPath)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width="500" height="500" style="border:1px gray solid">
<path id="path" fill="blue" stroke="black"></path>
</svg>
However, the path isnt a smooth transition around the circle.
Your animation looks weird because you're animating linearly between two curve shapes. One end point stays fixed while the other moves in a straight line (instead of along an arc).
I think you'll find it much easier to use the stroke-dashoffset trick to animate your curve. Here's a simple example:
function update_arc() {
/* Fetch angle from input field */
angle = document.getElementById("ang").value;
if (angle < 0) angle = 0;
if (angle > 360) angle = 360;
/* Convert to path length using formula r * θ (radians) */
var arclen = Math.PI * 50 * (360-angle) / 180.0;
/* Set stroke-dashoffset attribute to new arc length */
document.getElementById("c").style.strokeDashoffset = arclen;
}
#c {
stroke-dashoffset: 157.08;
stroke-dasharray: 314.16;
-webkit-transition: stroke-dashoffset 0.5s;
transition: stroke-dashoffset 0.5s;
}
<svg width="120" height="120" viewBox="0 0 120 120">
<!-- Circle element of radius 50 units. -->
<!-- (Rotated 90° CCW to put start point is at top.) -->
<circle id="c" cx="60" cy="60"
r="50" fill="none" stroke="blue"
stroke-width="15"
stroke-dashoffset="157.08"
stroke-dasharray="314.16"
transform="rotate(-90 60 60)" />
</svg>
<p>Enter new angle (0–360):<br />
<input type="text" id="ang" width="20" value="180" />
<button onclick="return update_arc()">Set</button></p>

Dynamically update Snap.svg gauge chart

I found some code for a chart on Codepen (credits: Henry Poydar) that uses snap.svg to create an animated gauge chart.
angular.module('app', []);
angular.module('app')
.controller('metricsCtrl', function($scope) {
$scope.percentage = .8;
var polar_to_cartesian, svg_circle_arc_path, animate_arc;
polar_to_cartesian = function(cx, cy, radius, angle) {
var radians;
radians = (angle - 90) * Math.PI / 180.0;
return [Math.round((cx + (radius * Math.cos(radians))) * 100) / 100, Math.round((cy + (radius * Math.sin(radians))) * 100) / 100];
};
svg_circle_arc_path = function(x, y, radius, start_angle, end_angle) {
var end_xy, start_xy;
start_xy = polar_to_cartesian(x, y, radius, end_angle);
end_xy = polar_to_cartesian(x, y, radius, start_angle);
return "M " + start_xy[0] + " " + start_xy[1] + " A " + radius + " " + radius + " 0 0 0 " + end_xy[0] + " " + end_xy[1];
};
animate_arc = function(ratio, svg, perc) {
var arc;
arc = svg.path('');
return Snap.animate(0, ratio, (function(val) {
var path;
arc.remove();
path = svg_circle_arc_path(500, 500, 450, -90, val * 180.0 - 90);
arc = svg.path(path);
arc.attr({
class: 'data-arc'
});
perc.text(Math.round(val * 100) + '%');
}), Math.round(2000 * ratio), mina.easeinout);
};
$scope.$watch('percentage', function() {
$('.metric').each(function() {
var ratio, svg, perc;
//ratio = $(this).data('ratio');
ratio = $scope.percentage;
svg = Snap($(this).find('svg')[0]);
perc = $(this).find('text.percentage');
animate_arc(ratio, svg, perc);
});
});
});
.metric {
padding: 10%;
}
.metric svg {
max-width: 100%;
}
.metric path {
stroke-width: 75;
stroke: #ecf0f1;
fill: none;
}
.metric path.data-arc {
stroke: #3498db;
}
.metric text {
fill: #3498db;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.4.1/snap.svg-min.js"></script>
<div ng-app="app" ng-controller="metricsCtrl">
<div class="metric">
<svg viewBox="0 0 1000 500">
<path d="M 950 500 A 450 450 0 0 0 50 500"></path>
<text class='percentage' text-anchor="middle" alignment-baseline="middle" x="500" y="300" font-size="140" font-weight="bold">0%
</text>
<text class='title' text-anchor="middle" alignment-baseline="middle" x="500" y="450" font-size="90" font-weight="normal">Empty
</text>
</svg>
</div>
<input ng-model="percentage">
</div>
I would like to be able to dynamically update the charts data and have the SVG render accordingly. I am able to get the chart to show an increase value, but a decrease in value is not working. Here is a demo that reproduces my problem http://codepen.io/EvanWieland/pen/bpxqpV. In the demo, if you increase the value in the input below the chart and then decrease it, you will be able to observe my dilemma. Note that the demo uses Angularjs, this is not a requirement. Thanks in advanced!
This was due to "svg.path(path)" that creates a new arc each time, thus decreasing value draws an arc hidden by the previous ones. The solution is to remove the previous arc at each repaint.
angular.module('app', []);
angular.module('app')
.controller('metricsCtrl', function($scope) {
$scope.percentage = .8;
var polar_to_cartesian, svg_circle_arc_path, animate_arc;
polar_to_cartesian = function(cx, cy, radius, angle) {
var radians;
radians = (angle - 90) * Math.PI / 180.0;
return [Math.round((cx + (radius * Math.cos(radians))) * 100) / 100, Math.round((cy + (radius * Math.sin(radians))) * 100) / 100];
};
svg_circle_arc_path = function(x, y, radius, start_angle, end_angle) {
var end_xy, start_xy;
start_xy = polar_to_cartesian(x, y, radius, end_angle);
end_xy = polar_to_cartesian(x, y, radius, start_angle);
return "M " + start_xy[0] + " " + start_xy[1] + " A " + radius + " " + radius + " 0 0 0 " + end_xy[0] + " " + end_xy[1];
};
animate_arc = function(ratio, svg, perc) {
var arc;
arc = svg.path('');
return Snap.animate(0, ratio, (function(val) {
var path;
arc.remove();
path = svg_circle_arc_path(500, 500, 450, -90, val * 180.0 - 90);
var previousArc = svg.select('.data-arc')
if (previousArc){
previousArc.remove(); // REMOVES PREVIOUS ARC
}
arc = svg.path(path);
arc.attr({
class: 'data-arc'
});
perc.text(Math.round(val * 100) + '%');
}), Math.round(2000 * ratio), mina.easeinout);
};
$scope.$watch('percentage', function() {
$('.metric').each(function() {
var ratio, svg, perc;
//ratio = $(this).data('ratio');
ratio = $scope.percentage;
svg = Snap($(this).find('svg')[0]);
perc = $(this).find('text.percentage');
animate_arc(ratio, svg, perc);
});
});
});
.metric {
padding: 10%;
}
.metric svg {
max-width: 100%;
}
.metric path {
stroke-width: 75;
stroke: #ecf0f1;
fill: none;
}
.metric path.data-arc {
stroke: #3498db;
}
.metric text {
fill: #3498db;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/snap.svg/0.4.1/snap.svg-min.js"></script>
<div ng-app="app" ng-controller="metricsCtrl">
<div class="metric">
<svg viewBox="0 0 1000 500">
<path d="M 950 500 A 450 450 0 0 0 50 500"></path>
<text class='percentage' text-anchor="middle" alignment-baseline="middle" x="500" y="300" font-size="140" font-weight="bold">0%
</text>
<text class='title' text-anchor="middle" alignment-baseline="middle" x="500" y="450" font-size="90" font-weight="normal">Empty
</text>
</svg>
</div>
<input ng-model="percentage">
</div>

Categories