Let's embark on a journey.
At the end of this journey, we'll have created a gauge component in React.js.
A gauge is a simple diagram that visualizes a simple number. The shape and design is based on physical gauges, used in control panels to show the measurement of a specific metric.
I like to use them in digital interfaces to show context around a number -- a user can quickly see whether a value is lower or higher than expected. However, while our goal is to create a gauge React.js component, we'll learn concepts and tricks along the way that can help to create many other things. As our good friend Ursula Le Guin puts it,
It is good to have an end to journey toward; but it is the journey that matters, in the end.Ursula K. Le Guin, The Left Hand of Darkness
On this journey, we'll defeat dragons, monsters, and the most powerful foe of all: drawing arcs in the browser.
import React from "react"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
return (
<div>
</div>
)
}
export default Gauge
The beginning of our journey finds us in a Gauge.jsx
file, with a simple functional React component.
import React from "react"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
return (
<div>
</div>
)
}
export default Gauge
Whenever we create a new <Gauge>
, we'll be able to customize it with five props:
50
,0
,100
,Underneath our code, we'll show the output of our component.
Let's start by adding an <svg>
component, with a nice pink border so we can see its dimensions.
import React from "react"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
return (
<div>
<svg style={{
border: "1px solid pink"
}}>
</svg>
</div>
)
}
export default Gauge
As we can see, the default size of a <svg>
component is 300 pixels wide by 150 pixels tall.
We want our gauge to be 9em wide. This will let our gauge scale with our text, so it will never look disproportionate with the labels that we'll add later.
import React from "react"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
return (
<div>
<svg
width="9em"
style={{
border: "1px solid pink"
}}>
</svg>
</div>
)
}
export default Gauge
We can think about the <svg>
element as a telescope into another world. Our telescope defaults to a normal zoom level: one unit is the same as one pixel.
But we can zoom our telescope in or out. To set the zoom on our telescope, we'll use the viewBox
property.
viewBox
takes four arguments:
x
and y
set the position of the top, left corner of our view box. Changing these values will pan our view.width
and height
set the number of "units" that are visible inside of our view box. Changing these values will zoom our view.Let's see what different telescope settings would look like for a simple svg
with a purple circle at [50, 50]
, with a radius of 40
.
For our gauge, let's simplify our math and focus our telescope on a simple 2 by 1 grid:
import React from "react"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
return (
<div>
<svg
width="9em"
viewBox={[
-1, -1,
2, 1,
].join(" ")}
style={{
border: "1px solid pink"
}}>
</svg>
</div>
)
}
export default Gauge
If this doesn't make much sense yet, stay tuned and you'll start seeing the benefits very soon.
Alright, let's begin the next phase of our journey:
Drawing arcs in SVG is a complex feat, but we can use our trusty weapon d3.arc()
, from the d3-shape library.
d3.arc()
will create an arc generator. We can configure our arc generator by calling different methods on it.
const arcGenerator = d3.arc()
.innerRadius(25)
.outerRadius(40)
.startAngle(0)
.endAngle(5.5)
.padAngle(0)
.cornerRadius(20)
const arcPath = arcPathGenerator()
** HTML **
<svg width="100" height="100">
<path
fill="cornflowerblue"
d={arcPath}
style="transform: translate(50%, 50%)"
/>
</svg>
Let's look at the parameters we want to use for our arc:
import React from "react"
import { arc } from "d3-shape"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
const backgroundArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2)
.cornerRadius(1)
return (
<div>
<svg
width="9em"
viewBox={[
-1, -1,
2, 1,
].join(" ")}
style={{
border: "1px solid pink"
}}>
</svg>
</div>
)
}
export default Gauge
0.65
and end at 1
, ending up 0.35
units wide-Math.PI / 2
(one quarter turn counterclockwise), and extend to Math.PI / 2
(one quarter turn clockwise)1
,To use our arc generator, we simply need to call it.
Let's see what that looks like for our gauge. To start, we'll create a grey background arc, to show the full range of our gauge.
import React from "react"
import { arc } from "d3-shape"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
const backgroundArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2)
.cornerRadius(1)
()
return (
<div>
<svg
width="9em"
viewBox={[
-1, -1,
2, 1,
].join(" ")}
style={{
border: "1px solid pink"
}}>
</svg>
</div>
)
}
export default Gauge
Great! Calling an arc generator, with these settings spits out a string:
"M-0.8062257748298549,-8.326672684688674e-17A0.175,0.175,0,0,1,-0.9772433634301272,-0.2121212121212122A1,1,0,0,1,0.9772433634301272,-0.21212121212121218A0.175,0.175,0,0,1,0.8062257748298549,-2.7755575615628914e-17L0.8062257748298549,-2.7755575615628914e-17A0.175,0.175,0,0,1,0.6352081862295826,-0.13787878787878788A0.65,0.65,0,0,0,-0.6352081862295826,-0.13787878787878793A0.175,0.175,0,0,1,-0.8062257748298549,-8.326672684688674e-17Z"
This incantation is a set of instructions that can be passed to an SVG <path>
element.
In our render method, let's create a new <path>
element and pass these magic words to the d
attribute.
import React from "react"
import { arc } from "d3-shape"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
const backgroundArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2)
.cornerRadius(1)
()
return (
<div>
<svg style={{overflow: "visible"}}
width="9em"
viewBox={[
-1, -1,
2, 1,
].join(" ")}>
<path
d={backgroundArc}
fill="#dbdbe7"
/>
</svg>
</div>
)
}
Perfect!
Note how our arc is perfectly centered in our <svg>
. This is because d3.arc()
, by default, centers the arc around [0, 0]
.
If we revisit our grid figure, we can see that the [0, 0]
point is at the bottom, center of the viewBox
. This perfectly frames the top half of our arc, which is the only part that we're interested in.
Now that we've drawn the background arc which represents the range of possible values, we'll want to draw the filled arc that shows the current value.
0 - 100
, a value of 50
would be 50%
through our possible range.percentScale
which transforms value
s into a percent
(between 0
and 1
).Our percentScale
will use a scaleLinear
from d3-scale.angleScale
that maps a percent
into our possible angles (remember these values from our backgroundArc
's innerRadius
and outerRadius
?)d
attribute.import React from "react"
import { arc } from "d3-shape"
import { scaleLinear } from "d3-scale"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
const backgroundArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2)
.cornerRadius(1)
()
const percentScale = scaleLinear()
.domain([min, max])
.range([0, 1])
const percent = percentScale(value)
const angleScale = scaleLinear()
.domain([0, 1])
.range([-Math.PI / 2, Math.PI / 2])
.clamp(true)
const angle = angleScale(percent)
const filledArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(angle)
.cornerRadius(1)
()
return (
<div>
<svg style={{overflow: "visible"}}
width="9em"
viewBox={[
-1, -1,
2, 1,
].join(" ")}>
<path
d={backgroundArc}
fill="#dbdbe7"
/>
<path
d={filledArc}
fill="#9980FA"
/>
</svg>
</div>
)
}
Perfect! Play around with the slider to see how our filled arc looks with different value
s.
One thing you might notice when playing with our gauge's value
is that most states feel kind of... the same.
Let's reinforce how far the current value is to the left of right by filling our filled arc with a light-to-dark gradient.
import React from "react"
import { arc } from "d3-shape"
import { scaleLinear } from "d3-scale"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
const backgroundArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2)
.cornerRadius(1)
()
const percentScale = scaleLinear()
.domain([min, max])
.range([0, 1])
const percent = percentScale(value)
const angleScale = scaleLinear()
.domain([0, 1])
.range([-Math.PI / 2, Math.PI / 2])
.clamp(true)
const angle = angleScale(percent)
const filledArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(angle)
.cornerRadius(1)
()
const colorScale = scaleLinear()
.domain([0, 1])
.range(["#dbdbe7", "#4834d4"])
const gradientSteps = colorScale.ticks(10)
.map(value => colorScale(value))
return (
<div>
<svg style={{overflow: "visible"}}
width="9em"
viewBox={[
-1, -1,
2, 1,
fill="#9980FA"
].join(" ")}>
<path
d={backgroundArc}
fill="#dbdbe7"
/>
<path
d={filledArc}
/>
</svg>
</div>
)
}
colorScale
's .ticks()
method to output an array of equally-spaced values across the output range.0 - 1
to the range 0 - 100
:const scale = scaleLinear()
.domain([0, 1])
.range([0, 100])
const ticks = scale.ticks()
alert(ticks)
Now that we have our gradient's colors, we can apply them!
On the web, we might be used to using a simple background CSS gradient, but those won't work in SVG land. Let's see what we need to do instead.
import React from "react"
import { arc } from "d3-shape"
import { scaleLinear } from "d3-scale"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
const backgroundArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2)
.cornerRadius(1)
()
const percentScale = scaleLinear()
.domain([min, max])
.range([0, 1])
const percent = percentScale(value)
const angleScale = scaleLinear()
.domain([0, 1])
.range([-Math.PI / 2, Math.PI / 2])
.clamp(true)
const angle = angleScale(percent)
const filledArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(angle)
.cornerRadius(1)
()
const colorScale = scaleLinear()
.domain([0, 1])
.range(["#dbdbe7", "#4834d4"])
const gradientSteps = colorScale.ticks(10)
.map(value => colorScale(value))
return (
<div>
<svg style={{overflow: "visible"}}
width="9em"
viewBox={[
-1, -1,
2, 1,
].join(" ")}>
<defs>
<linearGradient
id="Gauge__gradient"
gradientUnits="userSpaceOnUse"
x1="-1"
x2="1"
y2="0">
</linearGradient>
</defs>
<path
d={backgroundArc}
fill="#dbdbe7"
/>
<path
d={filledArc}
fill="url(#Gauge__gradient)"
/>
</svg>
</div>
)
}
<defs>
(ie. definitions) element. Everything inside of a <defs>
element will not be rendered, but we can reference them elsewhere. This is great for elements that need to be defined before being used.<linearGradient>
element.<linearGradient>
won't render anything by itself, but we'll give it an id
attribute to use as a handle to reference later.gradientUnits
attribute. This attribute has two options:userSpaceOnUse
(the default), which uses the parent <svg>
element's coordinate system (set in its viewBox
)objectBoundingBox
, which uses the coordinate system from the element the gradient is applied togradientUnits
to userSpaceOnUse
, which is the default, but it's nice to be explicit.[-1, 0]
and end at [1, 0]
.Next, we'll need to create gradient stops, which tell the gradient where each color should fall. The gradient itself will do the hard work of blending between each of these colors.
import React from "react"
import { arc } from "d3-shape"
import { scaleLinear } from "d3-scale"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
const backgroundArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2)
.cornerRadius(1)
()
const percentScale = scaleLinear()
.domain([min, max])
.range([0, 1])
const percent = percentScale(value)
const angleScale = scaleLinear()
.domain([0, 1])
.range([-Math.PI / 2, Math.PI / 2])
.clamp(true)
const angle = angleScale(percent)
const filledArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(angle)
.cornerRadius(1)
()
const colorScale = scaleLinear()
.domain([0, 1])
.range(["#dbdbe7", "#4834d4"])
const gradientSteps = colorScale.ticks(10)
.map(value => colorScale(value))
return (
<div>
<svg style={{overflow: "visible"}}
width="9em"
viewBox={[
-1, -1,
2, 1,
].join(" ")}>
<defs>
<linearGradient
id="Gauge__gradient"
gradientUnits="userSpaceOnUse"
x1="-1"
x2="1"
y2="0">
{gradientSteps.map((color, index) => (
<stop
key={color}
stopColor={color}
offset={`${
index
/ (gradientSteps.length - 1)
}`}
/>
))}
</linearGradient>
</defs>
<path
d={backgroundArc}
fill="#dbdbe7"
/>
<path
d={filledArc}
fill="url(#Gauge__gradient)"
/>
</svg>
</div>
)
}
gradientSteps
array, grabbing each color and its index in the array<stop>
elementkey
for the outmost elements in a loop, to distinguish between them and help with re-render decisionsstopColor
attribute as our color, andoffset
attribute as the percent (0 - 100}
through the gradient. We could create a scale to convert the index
into a percent, but it's easier here to just divide the index
by the maximum index (or, one less than the number of gradientSteps
).fill
attribute of our filled arc.Another contextual clue we can add to our gauge is a sense of what is a normal value. When a value
is close to the middle of our range, we might want to know whether or not it's above the mid-point.
One simple solution would be to draw a <line>
element across the middle of our gauge, splitting it in two horizontally. If we make this line the same color as the background of our page (white), we can prevent it from stealing too much visual attention.
Note that we drew our line from [0, -1]
to [0, -0.65]
to prevent it from extending below our arc, whose inner radius starts at 0.65
.
import React from "react"
import { arc } from "d3-shape"
import { scaleLinear } from "d3-scale"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
const backgroundArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2)
.cornerRadius(1)
()
const percentScale = scaleLinear()
.domain([min, max])
.range([0, 1])
const percent = percentScale(value)
const angleScale = scaleLinear()
.domain([0, 1])
.range([-Math.PI / 2, Math.PI / 2])
.clamp(true)
const angle = angleScale(percent)
const filledArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(angle)
.cornerRadius(1)
()
const colorScale = scaleLinear()
.domain([0, 1])
.range(["#dbdbe7", "#4834d4"])
const gradientSteps = colorScale.ticks(10)
.map(value => colorScale(value))
return (
<div>
<svg style={{overflow: "visible"}}
width="9em"
viewBox={[
-1, -1,
2, 1,
].join(" ")}>
<defs>
<linearGradient
id="Gauge__gradient"
gradientUnits="userSpaceOnUse"
x1="-1"
x2="1"
y2="0">
{gradientSteps.map((color, index) => (
<stop
key={color}
stopColor={color}
offset={`${
index
/ (gradientSteps.length - 1)
}`}
/>
))}
</linearGradient>
</defs>
<path
d={backgroundArc}
fill="#dbdbe7"
/>
<path
d={filledArc}
fill="url(#Gauge__gradient)"
/>
<line
y1="-1"
y2="-0.65"
stroke="white"
strokeWidth="0.027"
/>
</svg>
</div>
)
}
We might also notice that the end of our filled arc can get a little lost, especially with the rounded corner. But this is supposed to be the main point of our diagram!
Let's add a bubble where our arc ends, to give it more visual importance.
import React from "react"
import { arc } from "d3-shape"
import { scaleLinear } from "d3-scale"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
const backgroundArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2)
.cornerRadius(1)
()
const percentScale = scaleLinear()
.domain([min, max])
.range([0, 1])
const percent = percentScale(value)
const angleScale = scaleLinear()
.domain([0, 1])
.range([-Math.PI / 2, Math.PI / 2])
.clamp(true)
const angle = angleScale(percent)
const filledArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(angle)
.cornerRadius(1)
()
const colorScale = scaleLinear()
.domain([0, 1])
.range(["#dbdbe7", "#4834d4"])
const gradientSteps = colorScale.ticks(10)
.map(value => colorScale(value))
const markerLocation = getCoordsOnArc(
angle,
1 - ((1 - 0.65) / 2),
)
return (
<div>
<svg style={{overflow: "visible"}}
width="9em"
viewBox={[
-1, -1,
2, 1,
].join(" ")}>
<defs>
<linearGradient
id="Gauge__gradient"
gradientUnits="userSpaceOnUse"
x1="-1"
x2="1"
y2="0">
{gradientSteps.map((color, index) => (
<stop
key={color}
stopColor={color}
offset={`${
index
/ (gradientSteps.length - 1)
}`}
/>
))}
</linearGradient>
</defs>
<path
d={backgroundArc}
fill="#dbdbe7"
/>
<path
d={filledArc}
fill="url(#Gauge__gradient)"
/>
<line
y1="-1"
y2="-0.65"
stroke="white"
strokeWidth="0.027"
/>
<circle
cx={markerLocation[0]}
cy={markerLocation[1]}
r="0.2"
stroke="#2c3e50"
strokeWidth="0.01"
fill={colorScale(percent)}
/>
</svg>
</div>
)
}
const getCoordsOnArc = (angle, offset=10) => [
Math.cos(angle - (Math.PI / 2)) * offset,
Math.sin(angle - (Math.PI / 2)) * offset,
]
getCoordsOnArc()
function (defined in the next bullet point!). We can pass it our value's angle
and the distance from the center of our arc. This will be half the distance the inner radius (0.65
) is from the outer radius (1
).getCoordsOnArc()
function. This is something I usually have lying around, because it comes in handy when doing radial layouts.<circle>
element, setting its center ([cx, cy]
) at our markerLocation
. A radius (r
) of 0.2
feels right (a little larger than the arc itself), and we'll give it a slight border (stroke
) to make it pop a bit.fill
to match the color of the filled arc's gradient. Even though the gradient is partially covered by the bubble, this double encoding of the value
helps the user focus on our main message: how far through the range is our value
? Play around with the sliders to see how the changing color feels.Our last bob is a little arrow that points to our bubble. This is our third redundancy for the current value
- might as well throw up a neon sign, am I right?
Maybe it's overkill, but the arrow has another benefit: it further emphasizes the visual metaphor to an analog gauge, like one you would find in a pilot's dashboard. Let's implement it, and see how we like it.
import React from "react"
import { arc } from "d3-shape"
import { scaleLinear } from "d3-scale"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
const backgroundArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2)
.cornerRadius(1)
()
const percentScale = scaleLinear()
.domain([min, max])
.range([0, 1])
const percent = percentScale(value)
const angleScale = scaleLinear()
.domain([0, 1])
.range([-Math.PI / 2, Math.PI / 2])
.clamp(true)
const angle = angleScale(percent)
const filledArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(angle)
.cornerRadius(1)
()
const colorScale = scaleLinear()
.domain([0, 1])
.range(["#dbdbe7", "#4834d4"])
const gradientSteps = colorScale.ticks(10)
.map(value => colorScale(value))
const markerLocation = getCoordsOnArc(
angle,
1 - ((1 - 0.65) / 2),
)
return (
<svg style={{overflow: "visible"}}
<div>
width="9em"
viewBox={[
-1, -1,
2, 1,
].join(" ")}>
<defs>
<linearGradient
id="Gauge__gradient"
gradientUnits="userSpaceOnUse"
x1="-1"
x2="1"
y2="0">
{gradientSteps.map((color, index) => (
<stop
key={color}
stopColor={color}
offset={`${
index
/ (gradientSteps.length - 1)
}`}
/>
))}
</linearGradient>
</defs>
<path
d={backgroundArc}
fill="#dbdbe7"
/>
<path
d={filledArc}
fill="url(#Gauge__gradient)"
/>
<line
y1="-1"
y2="-0.65"
stroke="white"
strokeWidth="0.027"
/>
<circle
cx={markerLocation[0]}
cy={markerLocation[1]}
r="0.2"
stroke="#2c3e50"
strokeWidth="0.01"
fill={colorScale(percent)}
/>
<path
d="M0.136364 0.0290102C0.158279 -0.0096701 0.219156 -0.00967009 0.241071 0.0290102C0.297078 0.120023 0.375 0.263367 0.375 0.324801C0.375 0.422639 0.292208 0.5 0.1875 0.5C0.0852272 0.5 -1.8346e-08 0.422639 -9.79274e-09 0.324801C0.00243506 0.263367 0.0803571 0.120023 0.136364 0.0290102ZM0.1875 0.381684C0.221591 0.381684 0.248377 0.356655 0.248377 0.324801C0.248377 0.292947 0.221591 0.267918 0.1875 0.267918C0.153409 0.267918 0.126623 0.292947 0.126623 0.324801C0.126623 0.356655 0.155844 0.381684 0.1875 0.381684Z"
transform={`rotate(${
angle * (180 / Math.PI)
}) translate(-0.2, -0.33)`}
fill="#6a6a85"
/>
</svg>
</div>
)
}
const getCoordsOnArc = (angle, offset=10) => [
Math.cos(angle - (Math.PI / 2)) * offset,
Math.sin(angle - (Math.PI / 2)) * offset,
]
<path>
element.<path>
's d
attribute and pasted it in here.rotate
unit is degrees.transform
to move our arrow left and up, to rotate around the middle dot, instead of our arrow's top, left corner.Phew! That was quite a large hill to climb!
Now that we're happy with our gauge diagram, let's add in some context to surface the actual data to users. We'll add the current value
, and two optional labels: the name of the metric, and the unit we're working in. I find it's good to have these prompts when creating a new Gauge.
This step might feel a little "Draw the rest of the owl". I won't walk through every single change, but feel free to go through all of the changes (highlighted), it's pretty straightforward!
Let's look at a few highlights, though.
import React from "react"
import { arc } from "d3-shape"
import { scaleLinear } from "d3-scale"
import { format } from "d3-format"
const Gauge = ({
value=50,
min=0,
max=100,
label,
units,
}) => {
const backgroundArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(Math.PI / 2)
.cornerRadius(1)
()
const percentScale = scaleLinear()
.domain([min, max])
.range([0, 1])
const percent = percentScale(value)
const angleScale = scaleLinear()
.domain([0, 1])
.range([-Math.PI / 2, Math.PI / 2])
.clamp(true)
const angle = angleScale(percent)
const filledArc = arc()
.innerRadius(0.65)
.outerRadius(1)
.startAngle(-Math.PI / 2)
.endAngle(angle)
.cornerRadius(1)
()
const colorScale = scaleLinear()
.domain([0, 1])
.range(["#dbdbe7", "#4834d4"])
const gradientSteps = colorScale.ticks(10)
.map(value => colorScale(value))
const markerLocation = getCoordsOnArc(
angle,
1 - ((1 - 0.65) / 2),
)
return (
<div
style={{
textAlign: "center",
}}>
<svg style={{overflow: "visible"}}
width="9em"
viewBox={[
-1, -1,
2, 1,
].join(" ")}>
<defs>
<linearGradient
id="Gauge__gradient"
gradientUnits="userSpaceOnUse"
x1="-1"
x2="1"
y2="0">
{gradientSteps.map((color, index) => (
<stop
key={color}
stopColor={color}
offset={`${
index
/ (gradientSteps.length - 1)
}`}
/>
))}
</linearGradient>
</defs>
<path
d={backgroundArc}
fill="#dbdbe7"
/>
<path
d={filledArc}
fill="url(#Gauge__gradient)"
/>
<line
y1="-1"
y2="-0.65"
stroke="white"
strokeWidth="0.027"
/>
<circle
cx={markerLocation[0]}
cy={markerLocation[1]}
r="0.2"
stroke="#2c3e50"
strokeWidth="0.01"
fill={colorScale(percent)}
/>
<path
d="M0.136364 0.0290102C0.158279 -0.0096701 0.219156 -0.00967009 0.241071 0.0290102C0.297078 0.120023 0.375 0.263367 0.375 0.324801C0.375 0.422639 0.292208 0.5 0.1875 0.5C0.0852272 0.5 -1.8346e-08 0.422639 -9.79274e-09 0.324801C0.00243506 0.263367 0.0803571 0.120023 0.136364 0.0290102ZM0.1875 0.381684C0.221591 0.381684 0.248377 0.356655 0.248377 0.324801C0.248377 0.292947 0.221591 0.267918 0.1875 0.267918C0.153409 0.267918 0.126623 0.292947 0.126623 0.324801C0.126623 0.356655 0.155844 0.381684 0.1875 0.381684Z"
transform={`rotate(${
angle * (180 / Math.PI)
}) translate(-0.2, -0.33)`}
fill="#6a6a85"
/>
</svg>
<div style={{
marginTop: "0.4em",
fontSize: "3em",
lineHeight: "1em",
fontWeight: "900",
fontFeatureSettings: "'zero', 'tnum' 1",
}}>
{ format(",")(value) }
</div>
{!!label && (
<div style={{
color: "#8b8ba7",
marginTop: "0.6em",
fontSize: "1.3em",
lineHeight: "1.3em",
fontWeight: "700",
}}>
{ label }
</div>
)}
{!!units && (
<div style={{
color: "#8b8ba7",
lineHeight: "1.3em",
fontWeight: "300",
}}>
{ units }
</div>
)}
</div>
)
}
const getCoordsOnArc = (angle, offset=10) => [
Math.cos(angle - (Math.PI / 2)) * offset,
Math.sin(angle - (Math.PI / 2)) * offset,
]
font-feature-settings
to our displayed value
. Turning on the "zero"
flag gives any zero a slash through it, which is a nice distinction from a capital O."tnum"
flag. This will ensure that each character takes up the same amount of space - wonderful for keeping our numbers from jumping around as the value
changes.label
or unit
, if they are truthy.value
is the most important part of this gauge, let's use a lighter color for our secondary labels.We've reached the end of our long journey. And we should be proud of ourselves! We had some help along the way from our SVG element friends <circle>
, <line>
, and <path>
, and overcame foes scary monsters of trigonometry, SVG gradients, and scaling data.
I hope you enjoyed our journey. You are a very fine person, reader, and I am very fond of you.