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 });
});