Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3 How to append needle at a certain point on a curve

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%.

enter image description here

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:

enter image description here

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>

2 Answers

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.

like image 69
Joseph Avatar answered Nov 27 '25 18:11

Joseph


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>
like image 35
Prasanna Avatar answered Nov 27 '25 19:11

Prasanna



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!