I'm trying to create a circular graph using only CSS and HTML, where each segment has a border with rounded ends at both the start and end. However, I'm struggling with positioning the ::before
and ::after
pseudo-elements to correctly mark the start and end of each segment's border.
Here's what I'm currently working with:
conic-gradient
to fill the circle based on a percentage (--value
).::before
) and end (::after
) of the border for each segment.I'm using calc() along with cos()
and sin()
to try to calculate the position of the ::after
pseudo-element based on the percentage of the circle filled (--value
). However, it doesn't seem to be working correctly.
How can I accurately position the ::after pseudo-element at the end of the filled segment on the circular graph, ensuring that it aligns with the edge of the filled arc?
Thank you for your help!
This is a simplified version of my CSS:
.circular {
&__graph {
grid-area: b;
max-width: 100%;
width: 100%;
margin-top: 36px;
justify-content: center;
display: flex;
flex-direction: column;
padding-left: 65px;
&-chart{
display: flex;
width: 100%;
height: 500px;
}
}
&__circles{
display: flex;
align-content: center;
justify-content: center;
align-items: center;
position: relative;
width: 100%;
}
&__dial-circle {
--size: 320px;
--thickness: 12px;
width: var(--size);
height: var(--size);
border-radius: 60%;
background: conic-gradient(var(--color, #1e90ff) calc(var(--value)* 1%), #F9F9F9 0);
mask: radial-gradient(farthest-side, transparent calc(100% - var(--thickness)), white calc(100% - var(--thickness) + 1px));
position: absolute;
//transform: rotate(180deg);
&::before, &::after {
content: ' ';
width: 12px;
height: 12px;
position: absolute;
background: var(--color, #1e90ff);
left: calc(50% - 7px);
top: 0;
border-radius: 50%;
}
&::after{
border: 1px solid red;
--angle: calc(var(--value) * 3.6);
--radius: calc((var(--size) / 2) - var(--thickness));
left: calc(50% + var(--radius) * cos(var(--angle) * (pi / 180)) - 6px);
top: calc(50% + var(--radius) * sin(var(--angle) * (pi / 180)) - 6px);
}
}
}
Here's my HTML for this example:
<div class="circular__graph-chart">
<div class="circular__circles">
<div class="circular__dial-circle" style="--color: rgb(39, 63, 82);--value: 79.5;--size: 314px;"></div>
<div class="circular__dial-circle" style="--color: rgb(36, 91, 120);--value: 72.8;--size: 282px"></div>
<div class="circular__dial-circle" style="--color: rgb(105, 129, 142);--value: 86;--size: 250px"></div>
<div class="circular__dial-circle" style="--color: rgb(189, 200, 201);--value: 68.2;--size: 217px"></div>
<div class="circular__dial-circle" style="--color: rgb(231, 195, 184);--value: 79.1;--size: 185px"></div>
<div class="circular__dial-circle" style="--color: rgb(195, 175, 180);--value: 86.8;--size: 152px"></div>
</div>
</div>
Here's a live example: https://codepen.io/JonJonCS/pen/BaXopEE
I tried using the cos()
and sin()
functions in calc()
to position the ::after
element at the end of the filled segment. I expected the ::after
element to appear exactly at the edge of the filled arc, but instead, it's positioned incorrectly and doesn't align with the edge of the segment. I was hoping the red circle would mark the exact end of the border.
With visual debugging, I noticed that all "end caps" were off by 90 degrees. Also, their radius was incorrect.
After adjusting --angle
by minus 90 (degrees) and --radius
by minus half (instead of all) its thickness in .circular__dial-circle::after
, the end cap was properly aligned:
.circular__graph {
grid-area: b;
max-width: 100%;
width: 100%;
margin-top: 36px;
justify-content: center;
display: flex;
flex-direction: column;
padding-left: 65px;
}
.circular__graph-chart {
display: flex;
width: 100%;
height: 500px;
}
.circular__circles {
display: flex;
align-content: center;
justify-content: center;
align-items: center;
position: relative;
width: 100%;
}
.circular__dial-circle {
--size: 320px;
--thickness: 12px;
width: var(--size);
height: var(--size);
border-radius: 60%;
background: conic-gradient(var(--color, #1e90ff) calc(var(--value)* 1%), #F9F9F9 0);
mask: radial-gradient(farthest-side, transparent calc(100% - var(--thickness)), white calc(100% - var(--thickness) + 1px));
position: absolute;
/*transform: rotate(180deg);*/
}
.circular__dial-circle::before,
.circular__dial-circle::after {
content: ' ';
width: 12px;
height: 12px;
position: absolute;
background: var(--color, #1e90ff);
left: calc(50% - 7px);
top: 0;
border-radius: 50%;
}
.circular__dial-circle::after {
/*border: 1px solid red;*/ /* Commented out for now */
--angle: calc(var(--value) * 3.6 - 90); /* Added: "- 90" */
--radius: calc((var(--size) / 2) - var(--thickness) / 2); /* Added: The last "/ 2" */
left: calc(50% + var(--radius) * cos(var(--angle) * (pi / 180)) - 6px);
top: calc(50% + var(--radius) * sin(var(--angle) * (pi / 180)) - 6px);
}
<div class="circular__graph-chart">
<div class="circular__circles">
<div class="circular__dial-circle" style="--color: rgb(39, 63, 82);--value: 79.5;--size: 314px;"></div>
<div class="circular__dial-circle" style="--color: rgb(36, 91, 120);--value: 72.8;--size: 282px"></div>
<div class="circular__dial-circle" style="--color: rgb(105, 129, 142);--value: 86;--size: 250px"></div>
<div class="circular__dial-circle" style="--color: rgb(189, 200, 201);--value: 68.2;--size: 217px"></div>
<div class="circular__dial-circle" style="--color: rgb(231, 195, 184);--value: 79.1;--size: 185px"></div>
<div class="circular__dial-circle" style="--color: rgb(195, 175, 180);--value: 86.8;--size: 152px"></div>
</div>
</div>
Personally, I find using polar coordinates (angle and radius) with rotate()
more intuitive:
.circular__graph {
grid-area: b;
max-width: 100%;
width: 100%;
margin-top: 36px;
justify-content: center;
display: flex;
flex-direction: column;
padding-left: 65px;
}
.circular__graph-chart {
display: flex;
width: 100%;
height: 500px;
}
.circular__circles {
display: flex;
align-content: center;
justify-content: center;
align-items: center;
position: relative;
width: 100%;
}
.circular__dial-circle {
--size: 320px;
--thickness: 12px;
width: var(--size);
height: var(--size);
border-radius: 60%;
background: conic-gradient(var(--color, #1e90ff) calc(var(--value)* 1%), #F9F9F9 0);
mask: radial-gradient(farthest-side, transparent calc(100% - var(--thickness)), white calc(100% - var(--thickness) + 1px));
position: absolute;
/*transform: rotate(180deg);*/
}
.circular__dial-circle::before,
.circular__dial-circle::after {
content: ' ';
width: 12px;
height: 12px;
position: absolute;
background: var(--color, #1e90ff);
left: calc(50% - 7px);
top: 0;
border-radius: 50%;
}
.circular__dial-circle::after {
/*border: 1px solid red;*/ /* Commented out for now */
--angle: calc(var(--value) * 3.6deg); /* Same as your `* 3.6`, but with unit */
--radius: calc((var(--size) - var(--thickness)) / 2); /* Added: The last "/ 2" */
left: 50%;
top: 50%;
transform:
translate(-50%, -50%) /* With `left`, `top`: Place centered */
rotate(var(--angle)) /* Use `--angle` */
translateY(calc(-1 * var(--radius))); /* Move outwards (along rotation, back to initial) */
}
<div class="circular__graph-chart">
<div class="circular__circles">
<div class="circular__dial-circle" style="--color: rgb(39, 63, 82);--value: 79.5;--size: 314px;"></div>
<div class="circular__dial-circle" style="--color: rgb(36, 91, 120);--value: 72.8;--size: 282px"></div>
<div class="circular__dial-circle" style="--color: rgb(105, 129, 142);--value: 86;--size: 250px"></div>
<div class="circular__dial-circle" style="--color: rgb(189, 200, 201);--value: 68.2;--size: 217px"></div>
<div class="circular__dial-circle" style="--color: rgb(231, 195, 184);--value: 79.1;--size: 185px"></div>
<div class="circular__dial-circle" style="--color: rgb(195, 175, 180);--value: 86.8;--size: 152px"></div>
</div>
</div>
left
, top
and translate(-50%, -50%)
).Here, step 4 is redundant because a circle is similar to its rotation.
Graphics such as these are usually easier in SVG. Take a look at Paulie_D's answer.