D3 - smooth radial chart transition animation - javascript

I've prepared D3 Radial Chart component to show percentage value of some target. It would be great to add smoothy transition effect when start drawing foreground circle from 0 to chartPercentage (eg. 70%).
The question is - how to prepare transition / delay / duration effect with code which is attached below ?
Second idea which I also want to implement is to count value inside of the circle (radial-content) with animation from 0 to chartValue. How to prepare such solution?
Thank you !
const chartPercentage = 70;
const chartValue = 1.1242
const radius = 75;
const border = 7;
const padding = 0;
const width = 400;
const height = 400;
const twoPi = Math.PI * 2;
const boxSize = (radius + padding) * 2;
let svg;
function setArc() {
return d3.arc()
.startAngle(0)
.innerRadius(radius)
.outerRadius(radius - border)
.cornerRadius(50);
}
function draw() {
svg = d3.select(".chart").append("svg")
.attr('width', width)
.attr('height', height);
svg.append("foreignObject")
.attr("width", boxSize)
.attr("height", boxSize)
.append("xhtml:div")
.attr('class', 'radial-wrapper')
.html(`<div class="radial-content">${chartValue}</div>`);
const field = svg.append('g')
.attr('transform', 'translate(' + boxSize / 2 + ',' + boxSize / 2 + ')');
const meter = field.append('g')
.attr('class', 'progress-meter');
const background = meter.append("path")
.datum({endAngle: twoPi})
.attr('class', 'background')
.attr('fill', '#2D2E2F')
.attr('fill-opacity', 0.1)
.attr("d", setArc());
const foreground = meter.append("path")
.datum({endAngle: (chartPercentage/100) * twoPi})
.attr('class', 'foreground')
.attr('fill', 'red')
.attr('fill-opacity', 1)
.attr('d', setArc());
}
draw();
body { margin:30px;position:fixed;top:0;right:0;bottom:0;left:0; }
.radial-wrapper{ display: flex; align-items: center; justify-content: center;width: 100%; height: 100%;}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div class="chart"></div>

I rewrote your code. When you need to animate some attribute, you should use attrTween not attr method.
const chartPercentage = 70;
const chartValue = 1.1242
const radius = 75;
const border = 7;
const padding = 0;
const width = 400;
const height = 400;
const twoPi = Math.PI * 2;
const boxSize = (radius + padding) * 2;
let svg;
const setArc = d3.arc()
.startAngle(0)
.innerRadius(radius)
.outerRadius(radius - border)
.cornerRadius(50);
const arcParams = {};
function draw() {
svg = d3.select(".chart").append("svg")
.attr('width', width)
.attr('height', height);
svg.append("foreignObject")
.attr("width", boxSize)
.attr("height", boxSize)
.append("xhtml:div")
.attr('class', 'radial-wrapper')
.html(`<div class="radial-content"></div>`);
const field = svg.append('g')
.attr('transform', 'translate(' + boxSize / 2 + ',' + boxSize / 2 + ')');
const meter = field.append('g')
.attr('class', 'progress-meter');
const background = meter
.append("path")
.attr('class', 'background')
.attr('fill', '#2D2E2F')
.attr('fill-opacity', 0.1)
.attr("d", setArc({ endAngle: twoPi }));
const foreground = meter
.append("path")
.transition()
.ease(d3.easeBounce)
.duration(1500)
.attr('class', 'foreground')
.attr('fill', 'red')
.attr('fill-opacity', 1)
.attrTween("d", function() {
return arcTween({ endAngle: 0 }, chartPercentage/100 )
})
}
function arcTween(d, new_score) {
var new_startAngle = 0
var new_endAngle = new_startAngle + new_score * 2 * Math.PI
var interpolate_start = d3.interpolate(d.startAngle, new_startAngle)
var interpolate_end = d3.interpolate(d.endAngle, new_endAngle)
return function(t) {
d.endAngle = interpolate_end(t)
d3.select('.radial-content')
.text((d.endAngle / new_endAngle * chartValue).toFixed(4));
return setArc(d)
}
}
draw();
body {
margin: 30px;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.radial-wrapper {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div class="chart"></div>

Make a function for attribute tween.
function arcTween(a) {
var j = {"endAngle":0};//start angle
var i = d3.interpolateObject(j, a);
return function(t) {
d3.select(".radial-content").text(d3.format(".4n")(chartValue*t));
return arc(i(t));
};
}
In the above function
d3.select(".radial-content").text(d3.format(".4n")(chartValue*t));
this will change the text(and output it in the format) in the radial content as the transition runs.
now add the tween function to the foreground path.
const foreground = meter.append("path")
.datum({
endAngle: (chartPercentage / 100) * twoPi
})
.attr('class', 'foreground')
.attr('fill', 'red')
.attr('fill-opacity', 1)
.transition().duration(750).attrTween("d", arcTween);
working code here

Related

Dynamically Create Arrows between Circles (D3)

I have a d3 (v7) visualization where I have a variable number of circles being drawn on the screen depending on my data set.
How can I get arrows connecting these circles? I'm trying to follow this guide: https://observablehq.com/#harrylove/draw-an-arrow-between-circles-with-d3-links
However, this is only for a set number of circles (2) and I will have a variable number of them depending on my dataset.
Below is my current d3 code that draws circles:
var svgContainer = d3.select("body")
.append("svg")
.attr("width", 800)
.attr("height", 200);
var circles = svgContainer.selectAll("circle")
.data(nodeObjs)
.enter()
.append("circle");
circles
.attr("cx", function (d, i) {return i * 100 + 30})
.attr("cy", 60)
.attr("r", 30)
.style("fill", "steelblue");
To make the observable example dynamic there are a few factors to take into account:
you need 2 link functions; 1 for horizontal and 1 for vertical - below I have linkH and linkV instead of just link
the link function doesn't need to be called immediately so lose the ({ source: linkSource, target: linkTarget}); - you are going to need an array of links instead
some choice between linkH and linkV - you can test if the x-gap is greater than the y-gap between two circles and choose a horizontal link; and vice versa
in the example I've made the horizontal vs vertical decision in the link creation; then call linkH or linkV in the .attr("d", ...) section
in the case that the arrow runs right to left or top to bottom you need to reverse the sign on the adjustment of the link x and y
See the working example below:
const svgWidth = 480;
const svgHeight = 180;
const svg = d3.select("body")
.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight);
// Define the arrowhead marker variables
const markerBoxWidth = 8;
const markerBoxHeight = 8;
const refX = markerBoxWidth / 2;
const refY = markerBoxHeight / 2;
const markerWidth = markerBoxWidth / 2;
const markerHeight = markerBoxHeight / 2;
const arrowPoints = [[0, 0], [0, 8], [8, 4]];
// Add the arrowhead marker definition to the svg element
svg
.append("defs")
.append("marker")
.attr("id", "arrow")
.attr("viewBox", [0, 0, markerBoxWidth, markerBoxHeight])
.attr("refX", refX)
.attr("refY", refY)
.attr("markerWidth", markerBoxWidth)
.attr("markerHeight", markerBoxHeight)
.attr("orient", "auto-start-reverse")
.append("path")
.attr("d", d3.line()(arrowPoints))
.attr("stroke", "black");
// horizontal link
const linkH = d3
.linkHorizontal()
.x(d => d.x)
.y(d => d.y);
// vertical link
const linkV = d3
.linkVertical()
.x(d => d.x)
.y(d => d.y);
// circle data
const n = (Math.floor(Math.random() * 12) * 2) + 2;
const circleRadius = 10;
const nodes = [];
const links = [];
for (let i=0; i<n; i++) {
nodes.push({
x: Math.floor(Math.random() * (svgWidth - 20)) + 20,
y: Math.floor(Math.random() * (svgHeight - 20)) + 20,
r: circleRadius
});
}
for (let i=0; i<n; i+=2) {
const xdelta = Math.abs(nodes[i + 1].x - nodes[i].x);
const ydelta = Math.abs(nodes[i + 1].y - nodes[i].y);
links.push({
source: { x: nodes[i].x, y: nodes[i].y },
target: { x: nodes[i + 1].x, y: nodes[i + 1].y },
arrowDirection: ydelta >= xdelta ? "V" : "H"
});
}
const circles = svg.selectAll(".node")
.data(nodes)
.enter()
.append("circle")
.attr("class", "node");
circles
.attr("cx", (d, i) => d.x)
.attr("cy", (d, i) => d.y)
.attr("r", d => d.r);
const arrows = svg.selectAll(".arrow")
.data(links)
.enter()
.append("path")
.attr("class", "arrow");
arrows
.attr("d", (d, i) => {
let reversed;
if (d.arrowDirection === "H") {
reversed = d.source.x < d.target.x ? 1 : -1;
d.source.x += circleRadius * reversed;
d.target.x -= (circleRadius + markerWidth) * reversed;
return linkH(d);
} else {
reversed = d.source.y > d.target.y ? 1 : -1;
d.source.y -= circleRadius * reversed;
d.target.y += (circleRadius + markerWidth) * reversed;
return linkV(d);
}
})
.attr("marker-end", "url(#arrow)");
.node {
fill: green;
stroke: steelblue;
}
.arrow {
stroke: black;
fill: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>

Round edges at the ends of radial progress bar

I'm using d3.js to create a radial progress bar i have achieved that but the problem which I'm facing is that I'm unable to create the round edges of each side
Im attaching the image of the output and the expected change in output as well.
Below is thee code that I'm using to create the radial progress bar
<script src="https://d3js.org/d3.v6.min.js"></script>
<style>
.progress {
display: inline-block;
padding: 20px;
}
.radial-progress {
&__text {
font-family: Arial, sans-serif;
font-size: 2rem;
font-weight: bold;
}
}
</style>
<div style="background:#2E98C5" class="background">
<div
data-text-colour="#2E98C5" //Text color
class="progress">
</div>
</div>
<script>
var renderProgress = function(el,background) {
//Note: i have kept the track color to be same as the background color so that
// it seems that there is no track
var start = 0;
//var background='#2E98C5';
var actual_count=12;
var total_count=20;
var end = (actual_count/total_count)*100;
//var end = el.dataset.progress;
var colours = {
fill: "white", //common for both
//track: '#' + el.dataset.trackColour,
text: el.dataset.textColour,
//stroke: '#' + el.dataset.strokeColour,
}
var radius = 68;
var border = 8;
var strokeSpacing = el.dataset.strokeSpacing;
var endAngle = Math.PI * 2;
var formatText = d3.format('.0%');
var boxSize = radius * 2;
var count = end;
var progress = start;
var step = end < start ? -0.01 : 0.01;
//Define the circle
var circle = d3.arc()
.startAngle(0)
.innerRadius(radius)
.outerRadius(radius - border);
//setup SVG wrapper
var svg = d3.select(el)
.append('svg')
.attr('width', boxSize)
.attr('height', boxSize);
// ADD Group container
var g = svg.append('g')
.attr('transform', 'translate(' + boxSize / 2 + ',' + boxSize / 2 + ')');
//Setup track
var track = g.append('g').attr('class', 'radial-progress');
track.append('path')
.attr('class', 'radial-progress__background')
.attr('fill', background)
.attr('stroke', colours.stroke)
.attr('stroke-width', '0px')
.attr('d', (end/100)*360);
//.attr('d', circle.endAngle(endAngle));
//Add colour fill
var value = track.append('path')
.attr('class', 'radial-progress__value')
.attr('fill', colours.fill)
.attr('stroke', colours.stroke)
.attr('stroke-width', strokeSpacing + 'px');
//This circle is just created to give the effect of border over circle
var numberText = track.append('circle')
.attr('fill', "#DBDBDB") // This colr is constant in bot the control design
.attr('dy', '.5rem')
.attr('r', 51);
//Add circle
var numberText = track.append('circle')
.attr('fill', "white") //This
.attr('dy', '.5rem')
.attr('r', 48);
//Add text value
var numberText = track.append('text')
.attr('class', 'radial-progress__text')
.attr('fill', colours.text)
.text( function (d) {
return actual_count.toString() +"/" +total_count.toString();
})//Pass the value that should be at the centre
.attr('font-size', '30px')
.attr('text-anchor', 'middle')
.attr('dy', '.5rem');
function update(progress) {
//update position of endAngle
value.attr('d', circle.endAngle(-(endAngle * progress)));// -ve so that it goes left to right
//update text value
//numberText.text(formatText(progress));
//numberText.text(actual_count.toString() +"/" +total_count.toString()); //Puts the text in the center
};
function iterate() {
//call update to begin animation
update(progress);
if (count > 0) {
//reduce count till it reaches 0
count--;
//increase progress
progress += step;
//Control the speed of the fill
setTimeout(iterate, 10);
}
};
iterate();
}
Array.prototype.slice.call(document.querySelectorAll('.progress')).forEach(el => {
renderProgress(el,'#2E98C5');
});
</script>
I have taken the image from some other source as it has highlighted the thing that I want to do .
Thanks in advance any help or guidance will be great.
Edit 1 :
As per adrew Reid suggestion I was able to make to work and below is the code which worked
<script src="https://d3js.org/d3.v6.min.js"></script>
<style>
.progress {
display: inline-block;
padding: 20px;
}
.radial-progress {
&__text {
font-family: Arial, sans-serif;
font-size: 2rem;
font-weight: bold;
}
}
</style>
<div style="background:#2E98C5" class="background">
<div
data-text-colour="#2E98C5" //Text color
class="progress">
</div>
</div>
<script>
var renderProgress = function(el,background) {
//Note: i have kept the track color to be same as the background color so that
// it seems that there is no track
var start = 0;
//var background='#2E98C5';
var actual_count=12;
var total_count=20;
var end = (actual_count/total_count)*100;
//var end = el.dataset.progress;
var colours = {
fill: "white", //common for both
//track: '#' + el.dataset.trackColour,
text: el.dataset.textColour,
//stroke: '#' + el.dataset.strokeColour,
}
var radius = 68;
var border = 8;
var strokeSpacing = el.dataset.strokeSpacing;
var endAngle = Math.PI * 2;
var formatText = d3.format('.0%');
var boxSize = radius * 2;
var count = end;
var progress = start;
var step = end < start ? -0.01 : 0.01;
//Define the circle
var circle = d3.arc()
.startAngle(0)
.innerRadius(radius)
.outerRadius(radius - border)
.cornerRadius(12);
//setup SVG wrapper
var svg = d3.select(el)
.append('svg')
.attr('width', boxSize)
.attr('height', boxSize);
// ADD Group container
var g = svg.append('g')
.attr('transform', 'translate(' + boxSize / 2 + ',' + boxSize / 2 + ')');
//Setup track
var track = g.append('g').attr('class', 'radial-progress');
track.append('path')
.attr('class', 'radial-progress__background')
.attr('fill', background)
.attr('stroke', colours.stroke)
.attr('stroke-width', '0px')
.attr('d', (end/100)*360);
//.attr('d', circle.endAngle(endAngle));
//Add colour fill
var value = track.append('path')
.attr('class', 'radial-progress__value')
.attr('fill', colours.fill)
.attr('stroke', colours.stroke)
.attr('stroke-width', strokeSpacing + 'px');
//This circle is just created to give the effect of border over circle
var numberText = track.append('circle')
.attr('fill', "#DBDBDB") // This colr is constant in bot the control design
.attr('dy', '.5rem')
.attr('r', 51);
//Add circle
var numberText = track.append('circle')
.attr('fill', "white") //This
.attr('dy', '.5rem')
.attr('r', 48);
//Add text value
var numberText = track.append('text')
.attr('class', 'radial-progress__text')
.attr('fill', colours.text)
.text( function (d) {
return actual_count.toString() +"/" +total_count.toString();
})//Pass the value that should be at the centre
.attr('font-size', '30px')
.attr('text-anchor', 'middle')
.attr('dy', '.5rem');
function update(progress) {
//update position of endAngle
value.attr('d', circle.endAngle(-(endAngle * progress)));// -ve so that it goes left to right
//update text value
//numberText.text(formatText(progress));
//numberText.text(actual_count.toString() +"/" +total_count.toString()); //Puts the text in the center
};
function iterate() {
//call update to begin animation
update(progress);
if (count > 0) {
//reduce count till it reaches 0
count--;
//increase progress
progress += step;
//Control the speed of the fill
setTimeout(iterate, 10);
}
};
iterate();
}
Array.prototype.slice.call(document.querySelectorAll('.progress')).forEach(el => {
renderProgress(el,'#2E98C5');
});
</script>

Multiple progress bar circles on single row

I am using this example to create a circle progress bar:
https://codepen.io/shellbryson/pen/KzaKLe
However, I would like to have multiple elements together on a row. I have tried this for two elements by adding two separate divs and two separate css IDs (both with display: inline-block;).
This successfully creates two progress bars together, but the first stays at 0%. In this case, the first should go to 90% and the second to 95% (as defined with var end in the js).
var wrapper = document.getElementById('progress1');
var start = 0;
var end = 90;
var colours = {
fill: '#' + wrapper.dataset.fillColour,
track: '#' + wrapper.dataset.trackColour,
text: '#' + wrapper.dataset.textColour,
stroke: '#' + wrapper.dataset.strokeColour,
}
var radius = 100;
var border = wrapper.dataset.trackWidth;
var strokeSpacing = wrapper.dataset.strokeSpacing;
var endAngle = Math.PI * 2;
var formatText = d3.format('.0%');
var boxSize = radius * 2;
var count = end;
var progress = start;
var step = end < start ? -0.01 : 0.01;
//Define the circle
var circle = d3.arc()
.startAngle(0)
.innerRadius(radius)
.outerRadius(radius - border);
//setup SVG wrapper
var svg = d3.select(wrapper)
.append('svg')
.attr('width', boxSize)
.attr('height', boxSize);
// ADD Group container
var g = svg.append('g')
.attr('transform', 'translate(' + boxSize / 2 + ',' + boxSize / 2 + ')');
//Setup track
var track = g.append('g').attr('class', 'radial-progress');
track.append('path')
.attr('class', 'radial-progress__background')
.attr('fill', colours.track)
.attr('stroke', colours.stroke)
.attr('stroke-width', strokeSpacing + 'px')
.attr('d', circle.endAngle(endAngle));
//Add colour fill
var value = track.append('path')
.attr('class', 'radial-progress__value')
.attr('fill', colours.fill)
.attr('stroke', colours.stroke)
.attr('stroke-width', strokeSpacing + 'px');
//Add text value
var numberText = track.append('text')
.attr('class', 'radial-progress__text')
.attr('fill', colours.text)
.attr('font-size', '30px')
.attr('text-anchor', 'middle')
.attr('dy', '.5rem');
function update(progress) {
//update position of endAngle
value.attr('d', circle.endAngle(endAngle * progress));
//update text value
numberText.text(formatText(progress));
}
(function iterate() {
//call update to begin animation
update(progress);
if (count > 0) {
//reduce count till it reaches 0
count--;
//increase progress
progress += step;
//Control the speed of the fill
setTimeout(iterate, 10);
}
})();
var wrapper = document.getElementById('progress2');
var start = 0;
var end = 95;
var colours = {
fill: '#' + wrapper.dataset.fillColour,
track: '#' + wrapper.dataset.trackColour,
text: '#' + wrapper.dataset.textColour,
stroke: '#' + wrapper.dataset.strokeColour,
}
var radius = 100;
var border = wrapper.dataset.trackWidth;
var strokeSpacing = wrapper.dataset.strokeSpacing;
var endAngle = Math.PI * 2;
var formatText = d3.format('.0%');
var boxSize = radius * 2;
var count = end;
var progress = start;
var step = end < start ? -0.01 : 0.01;
//Define the circle
var circle = d3.arc()
.startAngle(0)
.innerRadius(radius)
.outerRadius(radius - border);
//setup SVG wrapper
var svg = d3.select(wrapper)
.append('svg')
.attr('width', boxSize)
.attr('height', boxSize);
// ADD Group container
var g = svg.append('g')
.attr('transform', 'translate(' + boxSize / 2 + ',' + boxSize / 2 + ')');
//Setup track
var track = g.append('g').attr('class', 'radial-progress');
track.append('path')
.attr('class', 'radial-progress__background')
.attr('fill', colours.track)
.attr('stroke', colours.stroke)
.attr('stroke-width', strokeSpacing + 'px')
.attr('d', circle.endAngle(endAngle));
//Add colour fill
var value = track.append('path')
.attr('class', 'radial-progress__value')
.attr('fill', colours.fill)
.attr('stroke', colours.stroke)
.attr('stroke-width', strokeSpacing + 'px');
//Add text value
var numberText = track.append('text')
.attr('class', 'radial-progress__text')
.attr('fill', colours.text)
.attr('font-size', '30px')
.attr('text-anchor', 'middle')
.attr('dy', '.5rem');
function update(progress) {
//update position of endAngle
value.attr('d', circle.endAngle(endAngle * progress));
//update text value
numberText.text(formatText(progress));
}
(function iterate() {
//call update to begin animation
update(progress);
if (count > 0) {
//reduce count till it reaches 0
count--;
//increase progress
progress += step;
//Control the speed of the fill
setTimeout(iterate, 10);
}
})();
#progress1 {
display: inline-block;
padding: 20px;
}
#progress2 {
display: inline-block;
padding: 20px;
}
.radial-progress {
&__text {
font-family: Arial, sans-serif;
font-size: 2rem;
font-weight: bold;
}
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div id="progress1"
data-track-width="15"
data-track-colour="555555"
data-fill-colour="228B22"
data-text-colour="383838"
data-stroke-colour="FFFFFF"
data-stroke-spacing="1">
</div>
<div id="progress2"
data-track-width="15"
data-track-colour="555555"
data-fill-colour="228B22"
data-text-colour="383838"
data-stroke-colour="FFFFFF"
data-stroke-spacing="1">
</div>
I have tried re-naming variables in the js to be unique for each element (i.e. wrapper1 and wrapper2 or end1 and end2), but I cannot seem to get the first element to pay attention to the 90% end setting.
Why so much code duplication? You could simply generalize the JS into a function and then use it on any amount of progress circles, just add another data attribute for the progress, e.g data-progress:
var renderProgress = function(el) {
var start = 0;
var end = el.dataset.progress;
var colours = {
fill: '#' + el.dataset.fillColour,
track: '#' + el.dataset.trackColour,
text: '#' + el.dataset.textColour,
stroke: '#' + el.dataset.strokeColour,
}
var radius = 100;
var border = el.dataset.trackWidth;
var strokeSpacing = el.dataset.strokeSpacing;
var endAngle = Math.PI * 2;
var formatText = d3.format('.0%');
var boxSize = radius * 2;
var count = end;
var progress = start;
var step = end < start ? -0.01 : 0.01;
//Define the circle
var circle = d3.arc()
.startAngle(0)
.innerRadius(radius)
.outerRadius(radius - border);
//setup SVG wrapper
var svg = d3.select(el)
.append('svg')
.attr('width', boxSize)
.attr('height', boxSize);
// ADD Group container
var g = svg.append('g')
.attr('transform', 'translate(' + boxSize / 2 + ',' + boxSize / 2 + ')');
//Setup track
var track = g.append('g').attr('class', 'radial-progress');
track.append('path')
.attr('class', 'radial-progress__background')
.attr('fill', colours.track)
.attr('stroke', colours.stroke)
.attr('stroke-width', strokeSpacing + 'px')
.attr('d', circle.endAngle(endAngle));
//Add colour fill
var value = track.append('path')
.attr('class', 'radial-progress__value')
.attr('fill', colours.fill)
.attr('stroke', colours.stroke)
.attr('stroke-width', strokeSpacing + 'px');
//Add text value
var numberText = track.append('text')
.attr('class', 'radial-progress__text')
.attr('fill', colours.text)
.attr('font-size', '30px')
.attr('text-anchor', 'middle')
.attr('dy', '.5rem');
function update(progress) {
//update position of endAngle
value.attr('d', circle.endAngle(endAngle * progress));
//update text value
numberText.text(formatText(progress));
};
function iterate() {
//call update to begin animation
update(progress);
if (count > 0) {
//reduce count till it reaches 0
count--;
//increase progress
progress += step;
//Control the speed of the fill
setTimeout(iterate, 10);
}
};
iterate();
}
Array.prototype.slice.call(document.querySelectorAll('.progress')).forEach(el => {
renderProgress(el);
});
.progress {
display: inline-block;
padding: 20px;
}
.radial-progress {
&__text {
font-family: Arial, sans-serif;
font-size: 2rem;
font-weight: bold;
}
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<div
data-progress="90"
data-track-width="15"
data-track-colour="555555"
data-fill-colour="228B22"
data-text-colour="383838"
data-stroke-colour="FFFFFF"
data-stroke-spacing="1"
class="progress">
</div>
<div
data-progress="95"
data-track-width="15"
data-track-colour="555555"
data-fill-colour="228B22"
data-text-colour="383838"
data-stroke-colour="FFFFFF"
data-stroke-spacing="1"
class="progress">
</div>

Multi donut graph with curved borders

I am trying to create a doughnut graph with curved ends. This graph has multiple series in it.
I have also found a fiddle which is similar to my needs: jsfiddle.net/freezystem/henr4ozn/
But this is in v3 and I have been trying to convert it to v4, but failing to render the graph.
As per the comments and complexity I have dropped the conversion of above jsfiddle to v4 and I am trying to convert this jsfiddle.net/minnie_mouse/033jrrz8/ to v4
But still I am not getting the same rendered image.
const d3 = this.d3;
const arcTween = function (transition, newAngle, arc) {
transition.attrTween('d', function (d) {
const interpolate = d3.interpolate(d.endAngle, newAngle);
return function (t) {
d.endAngle = interpolate(t);
return arc(d);
};
});
};
const createCircle = function (svg, outerRadius, innerRadius, color, percent) {
debugger;
const ratio = percent / 100;
const arcBackground = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(0)
.endAngle(2 * Math.PI);
const pathBackground = svg.append('path')
.attr('d', arcBackground)
.style({
fill: '#585e65',
opacity: .2
});
const arcForeground = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.cornerRadius(20)
.startAngle(-0.05);
const pathForeground = svg.append('path')
.datum({ endAngle: 0 })
.attr('d', arcBackground)
.style({
fill: color
}).append('text')
.attr('transform', 'translate(10,10)')
.text('hi')
.attr('fill', 'white')
.attr('font-size', '20px');
pathForeground.transition()
.duration(1500)
.ease('elastic')
.call(arcTween, ((2 * Math.PI)) * ratio, arcForeground);
const chart = { path: pathForeground, arc: arcForeground };
return chart;
};
const addStartImage = function (svg, percent, outerRadius) {
svg.append('text')
.text(percent + '%')
.attr('font-size', '10px')
.attr('font-weight', 'bold')
.attr('fill', 'white')
.attr({
width: 20,
height: 20,
transform: 'translate(4,' + (-outerRadius + 20) + ')'
});
};
const w = 300, h = 300;
let outerRadius = (w / 2);
const width = 30, gap = 14;
const innerRadius = outerRadius - 30;
const color = ['#e90b3a', '#a0ff03', '#1ad5de'];
const svg = d3.select('#chart')
.append('svg')
.attr('width', w)
.attr('height', h)
.attr('class', 'shadow')
.append('g')
.attr('transform', 'translate(' + w / 2 + ',' + h / 2 + ')'
);
const circles = [
{ name: 'activity1', percent: 50, color: '#3e4eb7' },
{ name: 'activity2', percent: 66, color: '#50a7ff' }];
for (let i = 0; i < circles.length; ++i) {
if (i > 0) {
outerRadius = innerRadius - gap;
}
circles[i]['chart'] = createCircle(svg, outerRadius, innerRadius, circles[i].color, circles[i].percent);
addStartImage(svg, circles[i].percent, outerRadius);
}
Atlast I got it to run successfully.Here is the code
const d3 = this.d3;
const arcTween = function (transition, newAngle, arc) {
transition.attrTween('d', function (d) {
const interpolate = d3.interpolate(d.endAngle, newAngle);
return function (t) {
d.endAngle = interpolate(t);
return arc(d);
};
});
};
const createCircle = function (svg, outerRadius, innerRadius, color, percent) {
const ratio = percent / 100;
const arcBackground = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(0)
.endAngle(2 * Math.PI);
const pathBackground = svg.append('path')
.attr('d', arcBackground)
.style({
fill: '#585e65',
opacity: .2
});
const arcForeground = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.cornerRadius(20)
.startAngle(-0.05);
const pathForeground = svg.append('path')
.datum({ endAngle: 0 })
.attr('d', arcBackground)
.style('fill', color);
pathForeground.append('text')
.attr('transform', 'translate(10,10)')
.text('hi')
.attr('fill', 'white')
.attr('font-size', '20px');
pathForeground.transition()
.duration(2000)
.transition()
.ease(d3.easeElastic)
.call(arcTween, ((2 * Math.PI)) * ratio, arcForeground);
const chart = { path: pathForeground, arc: arcForeground };
return chart;
};
const addStartImage = function (svg, percent, outerRadius) {
svg.append('text')
.text(percent + '%')
.attr('font-size', '10px')
.attr('font-weight', 'bold')
.attr('fill', 'white')
.attr({
width: 20,
height: 20,
transform: 'translate(4,' + (-outerRadius + 20) + ')'
});
};
const w = 300, h = 300;
let outerRadius = (w / 2);
const width = 30, gap = 14;
const innerRadius = outerRadius - 30;
const color = ['#e90b3a', '#a0ff03', '#1ad5de'];
const svg = d3.select('#chart')
.append('svg')
.attr('width', w)
.attr('height', h)
.attr('class', 'shadow')
.append('g')
.attr('transform', 'translate(' + w / 2 + ',' + h / 2 + ')'
);
const circles = [
{ name: 'activity1', percent: 50, color: '#3e4eb7' },
{ name: 'activity2', percent: 66, color: '#50a7ff' }];
for (let i = 0; i < circles.length; ++i) {
if (i > 0) {
outerRadius = innerRadius - gap;
}
circles[i]['chart'] = createCircle(svg, outerRadius, innerRadius, circles[i].color, circles[i].percent);
addStartImage(svg, circles[i].percent, outerRadius);
}

Modify D3.js Circle Progress Graph to show range of percentages

I am using d3.js to display a circle progress graph. It's working great but I'd like to show a specific percentage range on the graph in a different color. I'd like to show 75% - 90% as a different color on the graph. How can I achieve this? I looked at the donut chart to accomplish this but I like that the circle graph is animated so I'd like to stick with it and modify it to achieve my needs.
Goal:
Add 75%-90% percentage range in a different line color on the graph. The graph exists to show a 75%-90% "accuracy rating".
Bonus:
Add "75% to 90%" label like shown in this image:
JS:
var colors = {
'pink': '#ffffff',
'yellow': '#f0ff08',
'green': '#47e495'
};
var color = colors.pink;
var line_two_color = colors.green;
var radius = 50;
var border = 3;
var padding = 10;
var startPercent = 0;
var endPercent = 0.90;
var twoPi = Math.PI * 2;
var formatPercent = d3.format('.0%');
var boxSize = 130;
var count = Math.abs((endPercent - startPercent) / 0.01);
var step = endPercent < startPercent ? -0.01 : 0.01;
var arc = d3.svg.arc()
.startAngle(0)
.innerRadius(radius)
.outerRadius(radius - border);
var parent = d3.select('div#circle_graph');
var svg = parent.append('svg')
.attr('width', boxSize)
.attr('height', boxSize);
var defs = svg.append('defs');
var g = svg.append('g')
.attr('transform', 'translate(' + boxSize / 2 + ',' + boxSize / 2 + ')');
var meter = g.append('g')
.attr('class', 'progress-meter');
meter.append('path')
.attr('class', 'background')
.attr('fill', '#ccc')
.attr('fill-opacity', 0.5)
.attr('d', arc.endAngle(twoPi));
var foreground = meter.append('path')
.attr('class', 'foreground')
.attr('fill', color)
.attr('fill-opacity', 1)
.attr('stroke', color)
.attr('stroke-width', 5)
.attr('stroke-opacity', 1)
var front = meter.append('path')
.attr('class', 'foreground')
.attr('fill', color)
.attr('fill-opacity', 1);
var numberText = meter.append('text')
.attr('fill', '#fff')
.attr('text-anchor', 'middle')
.attr('dy', '.35em');
function updateProgress(progress) {
foreground.attr('d', arc.endAngle(twoPi * progress));
front.attr('d', arc.endAngle(twoPi * progress));
numberText.text(formatPercent(progress));
}
var progress = startPercent;
(function loops() {
updateProgress(progress);
if (count > 0) {
count--;
progress += step;
setTimeout(loops, 10);
}
})();
CSS:
.progress-meter text {
font-family:"Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 24px;
font-weight: bold;
}
HTML:
<div id="circle_graph"></div>
Hopefully I'm understanding your question correctly!
Also note, if your data is in % (rather than radians), you'll have to add a d3.scale to convert [0,100] domain to [0, 2pi].
The below code mimics a single progress arc with two separate arcs. One for the 0-75% range and a second for above 75%. Both arcs draw based on the same data but the key is using min and max functions to clamp the data as it passes the 75% threshold.
For the first bar, the end angle is stopped when the progress passes 75%...
.attr('d', function(d){
progressArc.startAngle(0)
return progressArc.endAngle( Math.min(d,(3/2)*Math.PI) )();
})
Math.min(d,(3/2)*Math.PI)
while for the second bar, the end angle only begins to change after the data passes 75%...
.attr('d', function(d){
progressArc.startAngle((3/2)*Math.PI)
return progressArc.endAngle( Math.max(d,(3/2)*Math.PI ))();
})
Math.max(d,(3/2)*Math.PI
The end result looks like one bar changing color as it passes a threshold.
var height = 20,
width = 70,
progress = 3;
d3.select('div').append('svg')
.attr('width','100%')
.attr('viewBox','0 0 ' + width + ' ' + height)
d3.select('svg').append('g')
.attr('transform','translate('+width/2+','+height/2+')')
.attr('id','main')
var progressArc = d3.svg.arc()
.innerRadius(7)
.outerRadius(9)
.startAngle(0)
.endAngle(2*Math.PI)
var progressBar1 = d3.select('#main').append('g').attr('class','progressBar1'),
progressBar2 = d3.select('#main').append('g').attr('class','progressBar2');
progressBar1.selectAll('path')
.data([progress])
.enter()
.append('path')
.attr('d', function(d){
progressArc.startAngle(0)
return progressArc.endAngle( Math.min(d,(3/2)*Math.PI) )();
})
progressBar2.selectAll('path')
.data([progress])
.enter()
.append('path')
.attr('fill','red')
.attr('d', function(d){
progressArc.startAngle((3/2)*Math.PI)
return progressArc.endAngle( Math.max(d,(3/2)*Math.PI ))();
})
var update = function(){
progress = progress >= 2*Math.PI ? 0 : progress + Math.random()*(1/200)*Math.PI;
console.log(progress)
progressBar1.selectAll('path')
.data([progress])
.attr('d', function(d){
progressArc.startAngle(0)
return progressArc.endAngle( Math.min(d,(3/2)*Math.PI) )();
})
progressBar2.selectAll('path')
.data([progress])
.attr('d', function(d){
progressArc.startAngle((3/2)*Math.PI)
return progressArc.endAngle( Math.max(d,(3/2)*Math.PI ))();
})
}
setInterval( update, 12);
svg{
border: solid green 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div></div>

Categories