Veera / Blog

Building a radial gauge with SVG and React - Part 1

While building the Trader App, I used SVG in React to build the radial gauge UI widget that displays the profit percentage in a visually appealing manner. The final product looks something like below (you can view the actual widget at https://veerasundar.com/trader)

Final output

As you can see, the gauge has several components.

  1. Base dial with point markers.
  2. A needle, anchored at center that points the progress.
  3. A progress bar, either in red or green dependening on the progress value is in negative or positive.

For simplicity, let's assume the gauge has a width and height of 120px. With this, let's start building the components one by one.

SVG Gauge - CSS

We will be using below CSS to style our SVG gauge. Also note that we have rotated the SVG gauge by negative 90 degree using transform: rotate(-90deg), as it is needed to properly position the Gauge.

.radial-gauge {
	background-color: #fff;
	text-align: center;
	padding: 20px 0;
	margin: 20px 0;
}
.radial-progress {
    transform: rotate(-90deg);
    font-family: Verdana, Sans-Serif;
    margin: 0 20px;
}

.radial-progress .tick {
    stroke: #4b5156;
}

.radial-progress .tick.quarterTick {
    stroke: #2b2f32;
    stroke-width: 2;
    opacity: 1;
}

.radial-progress .tick-labels {
    font-size: .6rem;
    fill: #5b6268;
}

.radial-track {
    stroke: #33373b;
    stroke-width: 12;
}

.radial-progress-bar {
    stroke-width: 12;
}

.radial-progress-bar.up {
    stroke: #43A047;
}

.radial-progress-bar.down {
    stroke: #e53935;
}

.needle .point,
.needle .center {
    fill: #2b2f32;
}

SVG Gauge - Base dial

As a first step, we will be building the base dial and the tick markers / labels for our SVG gauge. The output can be seen below.

Code for the base dial

<svg class="radial-progress" width="120" height="120" viewBox="0 0 120 120">
    <defs>
        <line id="tick" x1="104" y1="60" x2="110" y2="60" stroke-linecap="round"></line>
    </defs>
    <g id="ticks">
        <use class="tick quarterTick" href="#tick" transform="rotate(0 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(10 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(20 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(30 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(40 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(50 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(60 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(70 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(80 60 60)"></use>
        <use class="tick quarterTick" href="#tick" transform="rotate(90 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(100 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(110 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(120 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(130 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(140 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(150 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(160 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(170 60 60)"></use>
        <use class="tick quarterTick" href="#tick" transform="rotate(180 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(190 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(200 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(210 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(220 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(230 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(240 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(250 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(260 60 60)"></use>
        <use class="tick quarterTick" href="#tick" transform="rotate(270 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(280 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(290 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(300 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(310 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(320 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(330 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(340 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(350 60 60)"></use>
        <use class="tick quarterTick" href="#tick" transform="rotate(360 60 60)"></use>
    </g>
    <g id="tickLabels" class="tick-labels">
        <text x="85" y="65" text-anchor="middle" transform="rotate(90 90,65)">0</text>
        <text x="45" y="33" text-anchor="middle" transform="rotate(90 53,35)">50</text>
        <text x="15" y="65" text-anchor="middle" transform="rotate(90 20,65)">100</text>
        <text x="50" y="93" text-anchor="middle" transform="rotate(90 53,95)">50</text>
    </g>
    <circle class="radial-track" cx="60" cy="60" r="54" fill="none"></circle>
</svg>

As you can see, we have,

  1. Defined a SVG element with width and height as 120px. We have also set the viewBox as 0 0 120 120 so that our SVG elements are scaled correctly (more about viewBox).
  2. We defined a #tick element inside <defs> and reused it multiple times, rotating them using transform=rotate() according to their corresponding position in the dial. We also placed the labels of ticks using <text> and rotated them accordingly as we did for the tick markers.
  3. Finally, we drew the base dial using the <circle> element.

SVG Gauge - Progress Bar

Next part would be to add the progress bar, indicating how much profit one have made (both positive / negative).

For example, in above gauges, the first gauge has made a negative -15% profit and the second one made a postive 59% profit. In order to visualize this, we will be using stroke-dasharray and stroke-dashoffset properties of circle element.

Code for drawing progress

<circle
	class="radial-progress-bar down"
	cx="60"
	cy="60"
	r="54"
	fill="none"
	stroke-dasharray="339.292"
	stroke-dashoffset="364.73889999999994" />

You can see I have used the values stroke-dasharray="339.292" and stroke-dashoffset="364.73889999999994". To calculate the values, I used the following formuales.

let radius = 54; // radius of our circle
	let progress = 15; // 0-100 range

	strokeDasharray = 2 * Math.PI * radius;
	strokeDashOffset = strokeDashArray * (1 - (progress/200));

As you can see the stroke-dasharray is nothing but the circumference of the circle and stroke-dashoffet is how much gap should be between each strokes. We use this gap to show the progress.

In my case, I needed half the circle to show positive progress and the other half to show negative progress, I divided the progress value by 200 to map it correctly.

SVG Gauge - Pointer Needles

Next we will be adding the pointer needles to exatly show the progress value inside the gauge (hey, what's a gauge if it doesn't have a needle!?).

Code to construct the needle

<g id="needle" class="needle">
    <polygon class="point" points="60,50 60,70 120,60" transform="rotate(106.2 60 60)"/>
    <circle class="center" cx="60" cy="60" r="23"></circle>
</g>

The needle has two components;

  1. The pointer is drawn using polygon and it's placed the center of the gauge cirle.
  2. And a center cirlce, acting like a base of the needle.

We just need to calculate the rotation angle of our needle and rotate it using center of our SVG as origin (which is 60,60).

let progress = 15; // 0-100 range.
let needleAngle = (180 * progress) / 100;

/* transform = rotate(needleAngle centerX centerY) */

Note: I am using 180 below because I need to map the progress to only half the circle angle. If I wanted to map it to full circle, I would have used 360 here.

If you want to show your needle moving from 0 to the progress position, you can use animateTransform.

<polygon class="point" points="60,50 60,70 120,60" transform="rotate(106.2 60 60)">
	<animateTransform
		attributeName="transform" type="rotate" from="0 60 60" to="106.2 60 60" dur=".5s"
                              fill="freeze"></animateTransform>
</polygon>

SVG Gauge - Assembly

Putting it all together, we will end up with our assembled SVG gauge.

Full code

<svg class="radial-progress" width="120" height="120" viewBox="0 0 120 120">
    <defs>
        <line id="tick" x1="104" y1="60" x2="110" y2="60" stroke-linecap="round"></line>
        <radialGradient id="radialCenter" cx="50%" cy="50%" r="50%">
            <stop stop-color="#dc3a79" offset="0"></stop>
            <stop stop-color="#241d3b" offset="1"></stop>
        </radialGradient>
    </defs>
    <g id="ticks">
        <use class="tick quarterTick" href="#tick" transform="rotate(0 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(10 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(20 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(30 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(40 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(50 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(60 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(70 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(80 60 60)"></use>
        <use class="tick quarterTick" href="#tick" transform="rotate(90 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(100 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(110 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(120 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(130 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(140 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(150 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(160 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(170 60 60)"></use>
        <use class="tick quarterTick" href="#tick" transform="rotate(180 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(190 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(200 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(210 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(220 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(230 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(240 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(250 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(260 60 60)"></use>
        <use class="tick quarterTick" href="#tick" transform="rotate(270 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(280 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(290 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(300 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(310 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(320 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(330 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(340 60 60)"></use>
        <use class="tick" href="#tick" transform="rotate(350 60 60)"></use>
        <use class="tick quarterTick" href="#tick" transform="rotate(360 60 60)"></use>
    </g>
    <g id="tickLabels" class="tick-labels">
        <text x="85" y="65" text-anchor="middle" transform="rotate(90 90,65)">0</text>
        <text x="45" y="33" text-anchor="middle" transform="rotate(90 53,35)">50</text>
        <text x="15" y="65" text-anchor="middle" transform="rotate(90 20,65)">100</text>
        <text x="50" y="93" text-anchor="middle" transform="rotate(90 53,95)">50</text>
    </g>
    <circle class="radial-track" cx="60" cy="60" r="54" fill="none"></circle>
    <circle class="radial-progress-bar up" cx="60" cy="60" r="54" fill="none" stroke-dasharray="339.292"
            stroke-dashoffset="239.20086"></circle>
    <g id="needle" class="needle">
        <polygon class="point" points="60,50 60,70 120,60" transform="rotate(106.2 60 60)">
            <animateTransform attributeName="transform" type="rotate" from="0 60 60" to="106.2 60 60" dur=".5s"
                              fill="freeze"></animateTransform>
        </polygon>
        <circle class="center" cx="60" cy="60" r="23"></circle>
    </g>
</svg>

SVG Gauge in production

Here's the screenshot from Trader app where this gauge is used.

SVG Gauge

In the next post, I will show how to build this SVG Gauge in React component.

undefined