How can I draw a pie chart only using stroke-dasharray, not stroke-dashoffset

1.6k views Asked by At

I am trying to draw a pie chart only using stroke-dasharray and other things like rotate and translate, I am not allowed to use stroke-dashoffset since it is not supported by wkhtmltopdf 0.12.5. I have tried to do something similar to the code below

<svg height="20%" width="20%" viewBox="0 0 20 20" style="border:1px solid gray; ">
  <circle r="10" cx="10" cy="10" fill="white" />
  <circle r="5" cx="10" cy="10" fill="bisque"
          stroke="tomato"
          stroke-width="10"
          stroke-dasharray="10.99 31.4"
          transform="rotate(-90) translate(-20)"/>
</svg>

Where 31.4 is the circumference of the circle and 10.99 is 35% of the circumference. This is drawing a slice representing 35% of the pie. How can I draw more slices (for example one respresenting 40% and another for 13%) after this one without using stroke-dashoffset, I could not figure this out. Thanks a lot for the help guys.

2

There are 2 answers

0
Paul LeBeau On BEST ANSWER

Creating pie charts that way is not really recommended. By "that way", I am referring to making circles where the stroke width matches the radius of the circle. To be precise, the stroke width is double the circle radius.

Some browsers (or browser versions) and rendering libraries have had bugs rendering circles of that form. The recommended way would be to create a path for each pie chart segment.

However, assuming you want to continue with this method, then here is what you need to know.

  • Stroke patterns on <circle> elements are rendered starting at 3 o'clock, and proceed clockwise around the circle.

    That's why you have the rotate(-90) in your example above. The -90 rotation is rotating the circle -90deg so that the stroke starts at the top (12 o'clock).

  • The two numbers in the dash pattern are <length of dash> <length of gap>. The pattern then repeats.

Okay. Let's update your SVG to add the extra segments you requested.

Firstly, I would suggest a couple of changes:

  1. Move the rotate(-90) to a parent group so that you don't have to worry about it when calculating the rotations for your new slices.
  2. You'll find it a lot easier if you use the version of rotate() that takes a centre of rotaion: rotate(angle, centreX, centreY).
  3. We need to add the fill colour (fill="bisque") to a separate circle. Otherwise the fill of each new segment you add will overlap the previous segments.

So our new starting point is this:

<svg height="20%" width="20%" viewBox="0 0 20 20" style="border:1px solid gray; ">
  <circle r="5" cx="10" cy="10" fill="bisque" />
  <g transform="rotate(-90, 10,10)" fill="none" stroke-width="10">
    <circle r="5" cx="10" cy="10"
            stroke="tomato"
            stroke-dasharray="10.99 31.4"/>
  </g>
</svg>

Add a 40% segment

The stroke length you need will be 40% of 31.4 = 12.56.

To rotate it so that it starts at the end of the first segment, you'll need to rotate it by an angle equal to (10.99 / 31.4) * 360deg = 126deg.

<svg height="20%" width="20%" viewBox="0 0 20 20" style="border:1px solid gray; ">
  <circle r="5" cx="10" cy="10" fill="bisque" />
  <g transform="rotate(-90, 10,10)" fill="none" stroke-width="10">
    <circle r="5" cx="10" cy="10"
            stroke="tomato"
            stroke-dasharray="10.99 31.4"/>
    <circle r="5" cx="10" cy="10"
            stroke="goldenrod"
            stroke-dasharray="12.56 31.4"
            transform="rotate(126, 10,10)"/>
  </g>
</svg>

Add a 13% segment

The stroke length you need will be 13% of 31.4 = 4.082.

To rotate it so that it starts at the end of the previous segment, you'll need to sum the lengths of the first two segments, and convert that to an angle.

((10.99 + 12.56) / 31.4) * 360deg = 0.75 * 360 = 270deg`.

<svg height="20%" width="20%" viewBox="0 0 20 20" style="border:1px solid gray; ">
  <circle r="5" cx="10" cy="10" fill="bisque" />
  <g transform="rotate(-90, 10,10)" fill="none" stroke-width="10">
    <circle r="5" cx="10" cy="10"
            stroke="tomato"
            stroke-dasharray="10.99 31.4"/>
    <circle r="5" cx="10" cy="10"
            stroke="goldenrod"
            stroke-dasharray="12.56 31.4"
            transform="rotate(126, 10,10)"/>
    <circle r="5" cx="10" cy="10"
            stroke="cornflowerblue"
            stroke-dasharray="4.082 31.4"
            transform="rotate(270, 10,10)"/>
  </g>
</svg>

0
Danny '365CSI' Engelman On

I wrapped Paul his answer in a Custom Element <svg-pie-chart> (supported in all modern Browsers)

By using unknown element behaviour, it simplifies usage in an HTML document:

<svg-pie-chart>
  <circle r="25%" cx="50%" cy="50%" fill="bisque" />
  <segment percent="35" stroke="tomato" />
  <segment percent="40" stroke="goldenrod" />
  <segment percent="13" stroke="cornflowerblue" />
  <circle r="2" cx="10" cy="10" fill="green" />
</svg-pie-chart>

<style>
  svg-pie-chart svg {
    width: 180px;
    background: grey;
  }
</style>
<script>
  customElements.define("svg-pie-chart", class extends HTMLElement {
    connectedCallback() {
      setTimeout(() => { // wait till all children are (unknown) Elements
        let rotate = 0;
        let svg = [...this.querySelectorAll("*")].map(el => {
          let elsvg = el.outerHTML;
          if (el.nodeName == "SEGMENT") {
            let [percent, stroke, deg = percent.value * .3142] = el.attributes;
            elsvg = `<circle r='5' cx='10' cy='10' stroke='${stroke.value}'` +
              ` stroke-dasharray='${deg} 31.42' transform="rotate(${rotate} 10 10)"/>`;
            rotate += (deg / 31.42) * 360;
          }
          return elsvg;
        });
        this.innerHTML=`<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'>` +
          `<g transform='rotate(-90 10 10)' fill='none' stroke-width='10'></g></svg>`;
        this.querySelector("g").innerHTML = svg.join``;
      })}});
</script>