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.
- Base dial with point markers.
- A needle, anchored at center that points the progress.
- 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,
- Defined a
SVG
element with width and height as 120px. We have also set theviewBox
as0 0 120 120
so that our SVG elements are scaled correctly (more about viewBox). - We defined a
#tick
element inside<defs>
and reused it multiple times, rotating them usingtransform=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. - 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;
- The pointer is drawn using
polygon
and it's placed the center of the gauge cirle. - 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.
In the next post, I will show how to build this SVG Gauge in React component.
undefined