I'm building a gauge in D3. It's basically a half moon and it fills a percentage based on a value. In the example below it fills 80%.

What I want to do is add a needle that starts where the progress ends and shows the numeric value (80% in this case). I want it look just like the example below:

How do I go about calculating the start and endpoint for my line? My code is below:
function drawBandwidthChart(target) {
const Gauge =function () {
const config = {
size: 200,
arcWidth: 12,
indicatorWidth: 26,
indicatorHeight: 4,
minValue: 0,
maxValue: 100,
minAngle: -90,
maxAngle: 90,
};
const gaugeObject = {};
const outerRadius = config.size / 2;
const innerRadius = config.size / 2 - config.arcWidth;
let foreground;
let arc;
let svg;
let current;
const deg2rad = (deg) => {
return deg * Math.PI / 180;
};
function render (){
arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(deg2rad(-90));
// create the SVG
svg = d3.select(target).append('svg')
.attr('width', config.size + config.indicatorWidth)
.attr('height', config.size / 2)
.append('g')
.attr('transform', `translate(${config.size / 2},${config.size / 2})`);
// append the gauge curve background
const background = svg.append('path')
.datum({ endAngle: deg2rad(90) })
.style('fill', '#737078')
.attr('d', arc);
// append the line on the right side of the chart
svg.append('line')
.style('stroke', '#737078')
.style('stroke-width', config.indicatorHeight)
.attr('x1', config.size / 2)
.attr('y1', 0)
.attr('x2', (config.size / 2) + config.indicatorWidth)
.attr('y2', 0);
// Display Current value
// append the gauge curve fill
foreground = svg.append('path')
.datum({ endAngle: deg2rad(-90) })
.style('fill', '#784bf5')
.attr('d', arc);
current = svg.append('text')
.attr('transform', 'translate(0,' + 0 + ')')
.attr('text-anchor', 'middle')
.style('fill', 'white')
.style('font-size', `12px`)
.style('font-family', 'IBM Plex Sans')
.text(current);
};
function update (value) {
const numPi = deg2rad(Math.floor(value * 180 / config.maxValue - 90));
// Display Current value
current.transition()
.text(value + '%');
// Arc Transition
foreground.transition()
.duration(750)
.styleTween('fill', () => d3.interpolate('#784bf5', '#784bf5'))
.call(arcTween, numPi);
};
// Update animation
function arcTween (transition, newAngle) {
transition.attrTween('d', d => {
const interpolate = d3.interpolate(d.endAngle, newAngle);
return t => {
d.endAngle = interpolate(t);
return arc(d);
};
});
};
render();
gaugeObject.update = update;
gaugeObject.configuration = config;
return gaugeObject;
};
const gauge = Gauge();
gauge.update(0);
gauge.update(80);
}
const container = document.getElementById('bandwidthChartContainer');
drawBandwidthChart(container);
body {
text-align: center;
padding-top: 10em;
background-color: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<body>
<div id="bandwidthChartContainer"></div>
</body>
You can add another arc that uses the same transition / tween that the existing arc does, instead of trying to work with rotating a line. The difference here is that the new arc has an inner/outer radius values the same as the main arc (just that the inner radius starts at the main arc's outer radius), but the amount of the arc shown is thin enough that it looks like a line coming out of the main arc.
I made a codepen here as an example.
I had to adjust the svg width and transform a little bit because the main arc was all the way to the left of the svg width and wouldn't show an indicator. I also added // ADDED and // CHANGED in the code where I added / changed some things.
Add in some translation and rotation to your line and you have what you want. Look for the comments
svg.append('line')
.style('stroke', 'white') // white just to show how it looks
.style('stroke-width', config.indicatorHeight)
.attr('x1', config.size / 2 - outerRadius)
.attr('y1', 0)
.attr('x2', (config.size / 2) + config.indicatorWidth - outerRadius)
.attr('y2', 0)
.attr('transform', (d) => {
// the angle you want to rotate
const angle = 180 - value/100*180
// translation in X
const tx = innerRadius * Math.cos(deg2rad(angle))
// translation in Y, but this is negated as the coordinate system in d3 has negative Y
const ty = innerRadius * Math.sin(deg2rad(angle))
return `translate(${tx}, -${ty}) rotate(-${angle})`
})
and same for the text
svg.append('text')
.text(`${value} %`)
.style('fill', 'white')
.attr('transform', (d) => {
const angle = 180 - value/100*180
const tx = innerRadius * Math.cos(deg2rad(angle))
const ty = innerRadius * Math.sin(deg2rad(angle))
return `translate(${tx+20}, -${ty+20})`
})
This can also be generalized and made a bit sensible. But I leave the OP for that task.
function drawBandwidthChart(target) {
const Gauge =function () {
const config = {
size: 200,
arcWidth: 12,
indicatorWidth: 26,
indicatorHeight: 4,
minValue: 0,
maxValue: 100,
minAngle: -90,
maxAngle: 90,
};
const gaugeObject = {};
const outerRadius = config.size / 2;
const innerRadius = config.size / 2 - config.arcWidth;
let foreground;
let arc;
let svg;
let current;
const deg2rad = (deg) => {
return deg * Math.PI / 180;
};
function render (){
arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(deg2rad(-90));
// create the SVG
svg = d3.select(target).append('svg')
.attr('width', config.size + config.indicatorWidth)
.attr('height', config.size / 2)
.append('g')
.attr('transform', `translate(${config.size / 2},${config.size / 2})`);
// append the gauge curve background
const background = svg.append('path')
.datum({ endAngle: deg2rad(90) })
.style('fill', '#737078')
.attr('d', arc);
// append the line on the right side of the chart
svg.append('line')
.style('stroke', '#737078')
.style('stroke-width', config.indicatorHeight)
.attr('x1', config.size / 2)
.attr('y1', 0)
.attr('x2', (config.size / 2) + config.indicatorWidth)
.attr('y2', 0);
// Display Current value
// append the gauge curve fill
foreground = svg.append('path')
.datum({ endAngle: deg2rad(-90) })
.style('fill', '#784bf5')
.attr('d', arc);
current = svg.append('text')
.attr('transform', 'translate(0,' + 0 + ')')
.attr('text-anchor', 'middle')
.style('fill', 'white')
.style('font-size', `12px`)
.style('font-family', 'IBM Plex Sans')
.text(current);
};
function update (value) {
const numPi = deg2rad(Math.floor(value * 180 / config.maxValue - 90));
// Display Current value
current.transition()
.text(value + '%');
// Arc Transition
foreground.transition()
.duration(750)
.styleTween('fill', () => d3.interpolate('#784bf5', '#784bf5'))
.call(arcTween, numPi);
svg.append('line')
.style('stroke', 'white')
.style('stroke-width', config.indicatorHeight)
.attr('x1', config.size / 2 - outerRadius)
.attr('y1', 0)
.attr('x2', (config.size / 2) + config.indicatorWidth - outerRadius)
.attr('y2', 0)
.attr('transform', (d) => {
const angle = 180 - value/100*180
const tx = innerRadius * Math.cos(deg2rad(angle))
const ty = innerRadius * Math.sin(deg2rad(angle))
return `translate(${tx}, -${ty}) rotate(-${angle})`
})
svg.append('text')
.text(`${value} %`)
.style('fill', 'white')
.attr('transform', (d) => {
const angle = 180 - value/100*180
const tx = innerRadius * Math.cos(deg2rad(angle))
const ty = innerRadius * Math.sin(deg2rad(angle))
return `translate(${tx+20}, -${ty+20})`
})
};
// Update animation
function arcTween (transition, newAngle) {
transition.attrTween('d', d => {
const interpolate = d3.interpolate(d.endAngle, newAngle);
return t => {
d.endAngle = interpolate(t);
return arc(d);
};
});
};
render();
gaugeObject.update = update;
gaugeObject.configuration = config;
return gaugeObject;
};
const gauge = Gauge();
gauge.update(80);
}
const container = document.getElementById('bandwidthChartContainer');
drawBandwidthChart(container);
body {
text-align: center;
padding-top: 10em;
background-color: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<body>
<div id="bandwidthChartContainer"></div>
</body>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With