Conic Gradient Graph
by Grzegorz Matyszewski
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<div id="graph">
<div id="gradient"></div>
<svg id="svg" viewbox="0 0 100 100" fill="none">
<circle cx="50" cy="50" r="46"></circle>
<circle cx="50" cy="50" r="46"></circle>
<circle cx="50" cy="50" r="46"></circle>
<circle cx="50" cy="50" r="46"></circle>
</svg>
<div id="hover">
<div id="hover-1">First Option: <output></output></div>
<div id="hover-2">Second Option: <output></output></div>
<div id="hover-3">Third Option: <output></output></div>
<div id="hover-4">Fourth Option: <output></output></div>
</div>
</div>
<form>
<label for="1">+</label>
<label for="2">+</label>
<label for="3">+</label>
<label for="4">+</label>
<input type="number" name="1" value="0" min="0" max="9" id="1">
<input type="number" name="2" value="0" min="0" max="9" id="2">
<input type="number" name="3" value="0" min="0" max="9" id="3">
<input type="number" name="4" value="0" min="0" max="9" id="4">
<output id="output-1"></output>
<output id="output-2"></output>
<output id="output-3"></output>
<output id="output-4"></output>
</form>
SCSS
body {
background: #eaeaea;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
position: fixed;
width: 100vw;
}
#graph {
--percentage-1: 25%;
--percentage-2: 25%;
--percentage-3: 25%;
--percentage-4: 25%;
--step-1: 25%;
--step-2: 50%;
--step-3: 75%;
width: 50vw;
height: 50vw;
position: relative;
margin: auto;
border-radius: 50%;
background: #fff;
}
#gradient {
position: absolute;
inset: 0;
border-radius: inherit;
overflow: hidden;
background: conic-gradient(
from 350deg at 50% 50%,
#7a253c 0%,
#2cedb9 5%,
#2cedb9 var(--step-1),
#217774 calc(5% + var(--step-1)),
#217774 var(--step-2),
#fcc024 calc(5% + var(--step-2)),
#fcc024 var(--step-3),
#7a253c calc(5% + var(--step-3)),
#7a253c 100%
);
&::before,
&::after {
content: "";
display: block;
position: absolute;
inset: 8%;
background: #eaeaea;
border-radius: inherit;
}
&::after {
background: #fff;
inset: 10%;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
}
}
form {
position: absolute;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
}
form output,
form input {
width: 40px;
text-align: center;
font-family: sans-serif;
display: inline-block;
}
form input {
font-size: 20px;
}
form output {
font-size: 10px;
}
label {
text-align: center;
cursor: pointer;
}
#svg {
position: absolute;
circle {
stroke: red;
stroke-width: 8;
opacity: 0;/* debug:
transition: opacity 0.1s;
&:hover { opacity: 1; } */
$circ: 2 * 3.14 * 46; /* 2 Pi R */
&:nth-child(1) {
stroke-dasharray: calc(#{$circ} * var(--percentage-1) / 100) calc(#{$circ} * (100% - var(--percentage-1)) / 100);
stroke-dashoffset: calc(#{$circ} * 1.25);
}
&:nth-child(2) {
stroke-dasharray: calc(#{$circ} * var(--percentage-2) / 100) calc(#{$circ} * (100% - var(--percentage-2)) / 100);
...
TypeScript
const graph = document.getElementById('graph');
const inputs = [...document.querySelectorAll('form input')];
const outputs = [...document.querySelectorAll('form output')];
const hovers = [...document.querySelectorAll('#hover output')];
const labels = [...document.querySelectorAll('label')];
const data = [...Array(inputs.length)]
.map((_,i)=>({
percentage: 100/inputs.length, // real percentage
step: 100/inputs.length * (i+1), // gradient step
}));
function update {
const base = 1;
const sum = inputs.reduce((a,c) => {
return a + (parseInt(c.value, 10) + base);
}, 0);
const percentages = inputs.map(input => {
return (parseInt(input.value, 10) + base) / sum * 100;
});
const timeline = gsap.timeline({
onUpdate: () => {
data.forEach(({percentage,step},i) => {
// set gradient step into css variable:
graph.style.setProperty(`--step-${i+1}`, `${ step }%`);
graph.style.setProperty(`--percentage-${i+1}`, `${ percentage }%`);
// display current percentage:
outputs[i].value = `${ Math.round(percentage) }%`;
hovers[i].value = `${ Math.round(percentage) }%`;
});
}
});
const step = 0;
percentages.forEach((percentage, index) => {
step += percentage;
// animate all the values:
timeline.to(data[index], { percentage, step, duration: 0.5 }, 0);
});
}
inputs.forEach(input => input.addEventListener('change', () => update()));
update();
labels.forEach(label => label.addEventListener('click', (e) => {
const input = document.getElementById(e.currentTarget.getAttribute('for'));
input.value = Math.min(parseInt(input.max, 10), parseInt(input.value, 10) + 1);
input.dispatchEvent(new Event('change'));
}));
graph.addEventListener('mousemove', e => {
gsap.to('#hover', { x: e.offsetX, y: e.offsetY, duration: 0.2 });
});