skip to Main Content

I’m unable to figure out how to evenly space each slice on this donut chart.

Screenshot of Donut Chart

import { storyblokEditable } from "@storyblok/react";

import { type BlokType } from "../../../types/storyblok";

interface AnimatedPieBlokProps {
  blok: BlokType;
}

function AnimatedPieBlok({ blok }: AnimatedPieBlokProps) {
  const { pieSlices } = blok;
  const totalSlices = pieSlices.length;
  const calculatedSlices = pieSlices.map((slice: BlokType) => ({
    ...slice,
    value: 360 / totalSlices
  }));
  const total = calculatedSlices.reduce((sum: number, item: BlokType) => sum + item.value, 0);
  const spacing = 1; // Set the spacing between slices
  const sliceAngle = (360 - (spacing * totalSlices)) / totalSlices;
  let cumulativeValue = 0;


  return (
    <div {...storyblokEditable(blok)} key={blok._uid} className="flex flex-row justify-center">
      <svg width="450" height="450" viewBox="0 0 200 200" className="flex-1">
        {calculatedSlices.map((item: BlokType, index: number) => {
          const words = item.title.split(' ');
          const firstWord = words[0];
          const secondWord = words[1];
          const startAngle = cumulativeValue * (Math.PI / 180);
          const endAngle = (cumulativeValue + sliceAngle) * (Math.PI / 180);

          // Adjust the angles to create spacing
          const adjustedStartAngle = startAngle + (spacing / 100); // Adjust for spacing
          const adjustedEndAngle = endAngle - (spacing / 100); // Adjust for spacing

          const x1 = 100 + 100 * Math.cos(startAngle);
          const y1 = 100 + 100 * Math.sin(startAngle);
          const x2 = 100 + 100 * Math.cos(endAngle);
          const y2 = 100 + 100 * Math.sin(endAngle);

          // Calculate the midpoint for the label and image
          const midAngle = (adjustedStartAngle + adjustedEndAngle) / 2;
          const labelX = 100 + 65 * Math.cos(midAngle); // Adjust the radius for label position
          const labelY = 100 + 65 * Math.sin(midAngle); // Adjust the radius for label position
          const imageX = 100 + 40 * Math.cos(midAngle); // Adjust the radius for image position
          const imageY = 100 + 40 * Math.sin(midAngle); // Adjust the radius for image position

          cumulativeValue += sliceAngle + spacing;

          return (
            <g
            key={index}
            className="transition-transform duration-300 ease-in-out hover:scale-105 relative hover:z-10"
            >
              <path
                d={`M 100,100 L ${x1},${y1} A 100,100 0 ${item.value / total > 0.5 ? 1 : 0} 1 ${x2},${y2} Z`}
                fill={item.backgroundColor}
              />
              <image
                href={item.image.filename} // Use the imageUrl from the dataset
                x={imageX - 15} // Center the image (adjust as needed)
                y={imageY - 15} // Center the image (adjust as needed)
                width="30" // Set the width of the image
                height="30" // Set the height of the image
              />
              <text
                x={labelX}
                y={labelY}
                fill="white"
                fontSize="7px"
                textAnchor="middle"
                alignmentBaseline="middle"
              >
                <tspan>{firstWord}</tspan>
                {secondWord ? <tspan x={labelX} dy="1.2em">{secondWord}</tspan> : null}
              </text>
          </g>
        );
        })}
        <circle cx="100" cy="100" r="30" fill="white" />
        <circle cx="100" cy="100" r="29" fill="none" stroke="#140965" strokeWidth="3" />
      </svg>
    </div>
  );
}

export default AnimatedPieBlok;

I’ve tried using other AI tools along with other stackoverflow posts to try and resolve the issue but haven’t been able to get it to work properly.

2

Answers


  1. Key is to use pathLength, and then you do not need any Math

    Wrapped in a native JavaScript Web Component (JSWC) for ease of use
    with shadowDOM so <style> is scoped and does not leak out to the rest of the DOM page:

    <pie-chart gap="11"></pie-chart>
    <pie-chart slices="8" gap="8"></pie-chart>
    <pie-chart stroke="green"></pie-chart>
    

    <script>
    customElements.define( "pie-chart", class extends HTMLElement {
        connectedCallback() {
          const slices = ~~(this.getAttribute("slices") || 6);
          const stroke = this.getAttribute("stroke")||"blue";
          const strokeWidth = 20;
          const gap = ~~(this.getAttribute("gap") || 12);
          const sliceAngle = 360 / slices - gap;
          this.attachShadow({ mode: "open" }).innerHTML = `
            <style>
              :host { display: inline-block; width: 160px; background:grey }
              path  { fill: none; stroke: ${stroke}; stroke-width: ${strokeWidth}; 
                      transform:rotate(var(--rotation)); transform-origin: 50% 50%;
                      stroke-dashoffset:var(--offset); stroke-dasharray:var(--dash) 360 }
            </style>
            <svg viewBox="0 0 100 100">
              <circle cx="50" cy="50" r="40" stroke-width="${strokeWidth}" stroke="white" fill="none"></circle>
            </svg>`;
          const paths = Array.from({ length: slices }).map((_, idx) => {
            const slice = document.createElementNS("http://www.w3.org/2000/svg", "path");
            slice.setAttribute("pathLength", 360);
            slice.setAttribute("d", "M50,10 A40,40 0 1,1 10,50 A40,40 0 1,1 50,10");
            slice.style.setProperty("--dash", sliceAngle);
            slice.style.setProperty("--offset", gap);
            slice.style.setProperty("--rotation", `${idx * (sliceAngle + gap)}deg`);
            return slice;
          });
          this.shadowRoot.querySelector("svg").append(...paths)
        }});
    </script>
    <pie-chart gap="11"></pie-chart>
    <pie-chart slices="8" gap="8"></pie-chart>
    <pie-chart stroke="green"></pie-chart>
    Login or Signup to reply.
  2. You can use the stroke-dasharray combined with pathLength to create slices, and then move the slices a bit (for the two examples, two different values: translate(.5 0) and translate(1 0)). This will make the gap between the slices even. To make the inner and outer edge of the slices look nice, mask off all the slices with a mask that has circles in white and black.

    My examples are static — you can probably figure out the dynamic version yourself.

    <svg viewBox="0 0 100 100" width="300">
      <defs>
        <mask id="m1">
          <circle r="50" cx="50" cy="50" fill="white" />
          <circle r="15" cx="50" cy="50" fill="black" />
        </mask>
      </defs>
      <g mask="url(#m1)" font-family="sans-serif" font-size="6"
       dominant-baseline="middle" text-anchor="middle">
        <g transform="translate(50 50)" fill="none" stroke-width="100">
          <g transform="rotate(0) translate(.5 0) rotate(-60)">
            <circle r="50" stroke="red" stroke-dasharray="120 360" pathLength="360" />
            <text transform="rotate(60) translate(30 0)"
             fill="black">Lorem</text>
          </g>
          <g transform="rotate(120) translate(.5 0) rotate(-60)">
            <circle r="50" stroke="orange" stroke-dasharray="120 360" pathLength="360" />
            <text transform="rotate(60) translate(30 0) rotate(-120)"
              fill="black">Lorem</text>
          </g>
          <g transform="rotate(240) translate(.5 0) rotate(-60)">
            <circle r="50" stroke="green" stroke-dasharray="120 360" pathLength="360" />
            <text transform="rotate(60) translate(30 0) rotate(-240)"
              fill="black">Lorem</text>
          </g>
        </g>
      </g>
    </svg>
    
    <svg viewBox="0 0 100 100" width="300">
      <defs>
        <mask id="m2">
          <circle r="50" cx="50" cy="50" fill="white" />
          <circle r="15" cx="50" cy="50" fill="black" />
        </mask>
      </defs>
      <g mask="url(#m2)" font-family="sans-serif" font-size="5"
       dominant-baseline="middle" text-anchor="middle">
        <g transform="translate(50 50)" fill="none" stroke-width="100">
          <g transform="rotate(0) translate(1 0) rotate(-22.5)">
            <circle r="50" stroke="red" stroke-dasharray="45 360" pathLength="360" />
            <text transform="rotate(22.5) translate(30 0)"
             fill="black">Lorem</text>
          </g>
          <g transform="rotate(45) translate(1 0) rotate(-22.5)">
            <circle r="50" stroke="orange" stroke-dasharray="45 360" pathLength="360" />
            <text transform="rotate(22.5) translate(30 0) rotate(-45)"
              fill="black">Lorem</text>
          </g>
          <g transform="rotate(90) translate(1 0) rotate(-22.5)">
            <circle r="50" stroke="green" stroke-dasharray="45 360" pathLength="360" />
            <text transform="rotate(22.5) translate(30 0) rotate(-90)"
              fill="black">Lorem</text>
          </g>
          <g transform="rotate(135) translate(1 0) rotate(-22.5)">
            <circle r="50" stroke="tomato" stroke-dasharray="45 360" pathLength="360" />
            <text transform="rotate(22.5) translate(30 0) rotate(-135)"
              fill="black">Lorem</text>
          </g>
          <g transform="rotate(180) translate(1 0) rotate(-22.5)">
            <circle r="50" stroke="lightblue" stroke-dasharray="45 360" pathLength="360" />
            <text transform="rotate(22.5) translate(30 0) rotate(-180)"
              fill="black">Lorem</text>
          </g>
          <g transform="rotate(225) translate(1 0) rotate(-22.5)">
            <circle r="50" stroke="red" stroke-dasharray="45 360" pathLength="360" />
            <text transform="rotate(22.5) translate(30 0) rotate(-225)"
              fill="black">Lorem</text>
          </g>
          <g transform="rotate(270) translate(1 0) rotate(-22.5)">
            <circle r="50" stroke="orange" stroke-dasharray="45 360" pathLength="360" />
            <text transform="rotate(22.5) translate(30 0) rotate(-270)"
              fill="black">Lorem</text>
          </g>
          <g transform="rotate(315) translate(1 0) rotate(-22.5)">
            <circle r="50" stroke="green" stroke-dasharray="45 360" pathLength="360" />
            <text transform="rotate(22.5) translate(30 0) rotate(-315)"
              fill="black">Lorem</text>
          </g>
        </g>
      </g>
    </svg>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search