0

I have the following SVG element which was created using JS: https://akzhy.com/blog/create-animated-donut-chart-using-svg-and-javascript

 <div class="doughnut">
        <svg width="100%" height="100%" viewBox="0 0 100 100">
            <circle cx="50" cy="50" r="30" stroke="#80e080" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="141.372"  transform='rotate(-90 50 50)'/>
            <circle cx="50" cy="50" r="30" stroke="#4fc3f7" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="103.6728"  transform='rotate(0 50 50)'/>
            <circle cx="50" cy="50" r="30" stroke="#9575cd" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="169.6464"  transform='rotate(162 50 50)'/>
            <circle cx="50" cy="50" r="30" stroke="#f06292" stroke-width="15" fill="transparent" stroke-dasharray="188.496" stroke-dashoffset="150.7968"  transform='rotate(198 50 50)'/>
        </svg>
</div>

Is it possible to to get a path from the svg node?

enxaneta
  • 31,608
  • 5
  • 29
  • 42
Beakal
  • 134
  • 1
  • 4
  • 15
  • What do you mean by "get a path"? Or maybe rather: what is the objective? What are you going to use the path (whatever it is) for? – chrwahl Apr 10 '22 at 05:18

1 Answers1

0

Conversion via graphic app

Open you svg in an application like Illustrator/inkscape etc.
You could use path operations like "stroke-to-path" to convert stroke based chart segments (the visual colored segments are just dashed strokes applied to a full circle).

Use a pie/donut generator script returning solid paths

Based on this answer by @ray hatfield Pie chart using circle element you can calculate d properties based on the arc command.

Example json based pie chart generator

let pies = document.querySelectorAll('.pie-generate');
generatePies(pies);


function generatePies(pies) {
  if (pies.length) {
    pies.forEach(function(pie, i) {
      let data = pie.getAttribute('data-pie');
      if (data) {
        data = JSON.parse(data);
        w = data['width'];
        h = data['height'];
        let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('viewBox', '0 0 ' + w + ' ' + h);
        svg.setAttribute('width', w);
        svg.setAttribute('height', h);
        svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
        pie.appendChild(svg);
        addSegments(svg, data);
      }
    })
  }
}

function addSegments(svg, data) {
  let segments = data["segments"];
  let strokeWidth = data["strokeWidth"];
  let centerX = data["centerX"];
  let centerY = data["centerY"];
  let radius = data["radius"];
  let startingAngle = (data["startingAngle"] || data["startingAngle"] == 0) ? data["startingAngle"] : -90;
  let gap = data["gap"];
  let decimals = data["decimals"];
  let offset = 0;
  let output = "";

  // calculate auto percentages
  let total = 0;
  let calc = data["calc"] ? true : false;
  if (calc) {
    segments.forEach(function(segment, i) {
      total += segment[0];
    });
  }

  // prevent too large gaps
  let circumference = Math.PI * radius * 2;
  let circumferencePerc = (circumference / 100);
  let gapPercentOuter = 100 / circumference * gap;
  if (gapPercentOuter > circumferencePerc) {
    gap = gap / (gapPercentOuter / circumferencePerc)
  }

  segments.forEach(function(segment, i) {
    let percent = segment[0];
    // calc percentages
    let percentCalc = percent.toString().indexOf('/') != -1 ? segment[0].split('/') : [];
    percent = percentCalc.length ? percentCalc[0] / percentCalc[1] * 100 : +percent

    // calculate auto percentages to get 100% in total
    if (total) {
      percent = 100 / total * percent;
    }
    let percentRound = percent.toFixed(decimals);

    // auto fill color
    let segOptions = segment[1] ? segment[1] : '';
    let fill = segOptions ? 'fill="' + segOptions['color'] + '"' : "";
    if (!fill) {
      let hueCut = 0;
      let hueShift = 0;
      let hue = Math.abs((360 - hueCut) / 100 * (offset + percent)) + hueShift;
      let autoColor = hslToHex(hue.toFixed(0) * 1, 60, 50);
      fill = 'fill="' + autoColor + '"';
    }

    let className = segOptions['class'] ? segOptions['class'] : "";
    let classPercent = percentRound.toString().replaceAll('.', '_');
    let id = segOptions['id'] ? 'id="' + segOptions['id'] + '" ' : '';
    let d = getArcD(centerX, centerY, strokeWidth, offset, percent, radius, gap, decimals, startingAngle);
    output +=
      `\n<path d="${d}" ${fill} class="segment segment-${classPercent} segment-${(i+1)} ${className}" ${id} data-percent="${percentRound}"/>`;
    offset += percent;
  });
  svg.innerHTML = output;
}

function getArcD(centerX, centerY, strokeWidth, percentStart, percent, radiusOuter, gap, decimals = 3, startingAngle = -90) {
  let radiusInner = radiusOuter - strokeWidth;
  let circumference = Math.PI * radiusOuter * 2;
  let isPieChart = false;

  // if pie chart – stroke equals radius
  if (strokeWidth + gap >= radiusOuter) {
    isPieChart = true;
  }

  let circumferenceInner = Math.PI * radiusInner * 2;
  let gapPercentOuter = ((100 / circumference) * gap) / 2;
  let gapPercentInner = ((100 / circumferenceInner) * gap) / 2;

  //add offset from previous segments
  percentStart = percentStart;
  let percentEnd = percent + percentStart;

  // outer coordinates
  let [x1, y1] = getPosOnCircle(centerX, centerY, (percentStart + gapPercentOuter), radiusOuter, decimals, startingAngle);
  let [x2, y2] = getPosOnCircle(centerX, centerY, percentEnd - gapPercentOuter, radiusOuter, decimals, startingAngle);

  // switch arc output between long or short arc segment according to percentage
  let longArc = percent >= 50 ? 1 : 0;
  let rotation = 0;
  let clockwise = 1;
  let counterclockwise = 0;
  let d = '';

  // if donut chart
  if (!isPieChart) {
    //inner coordinates
    let [x3, y3] = getPosOnCircle(centerX, centerY, percentEnd - gapPercentInner, radiusInner, decimals, startingAngle);
    let [x4, y4] = getPosOnCircle(centerX, centerY, percentStart + gapPercentInner, radiusInner, decimals, startingAngle);
    d = [
      "M", x1, y1,
      "A", radiusOuter, radiusOuter, rotation, longArc, clockwise, x2, y2,
      "L", x3, y3,
      "A", radiusInner, radiusInner, rotation, longArc, counterclockwise, x4, y4,
      "z"
    ];
  }

  // if pie chart – stroke equals radius: drop inner radius arc
  else {
    // find opposite coordinates
    let [x1o, y1o] = getPosOnCircle(centerX, centerY, (percentStart - gapPercentOuter) - 50, radiusOuter, decimals, startingAngle);
    let [x2o, y2o] = getPosOnCircle(centerX, centerY, (percentEnd + gapPercentOuter) - 50, radiusOuter, decimals, startingAngle);

    let extrapolatedIntersection = getLinesIntersection(
      [x1, y1, x1o, y1o], [x2, y2, x2o, y2o],
      decimals);
    d = [
      "M", x1, y1,
      "A", radiusOuter, radiusOuter, rotation, longArc, clockwise, x2, y2,
      "L", extrapolatedIntersection.join(" "),
      "z"
    ];
  }

  return d.join(" ");
}

// helper: get x/y coordinates according to angle percentage
function getPosOnCircle(centerX, centerY, percent, radius, decimals = 3, angleOffset = -90) {
  let angle = 360 / (100 / percent) + angleOffset;
  let x = +(centerX + Math.cos((angle * Math.PI) / 180) * radius).toFixed(
    decimals
  );
  let y = +(centerY + Math.sin((angle * Math.PI) / 180) * radius).toFixed(
    decimals
  );
  return [x, y];
}

// helper: get intersection coordinates
function getLinesIntersection(l1, l2, decimals = 3) {
  let intersection = [];
  let c2x = l2[0] - l2[2];
  let c3x = l1[0] - l1[2];
  let c2y = l2[1] - l2[3];
  let c3y = l1[1] - l1[3];
  // down part of intersection point formula
  let d = c3x * c2y - c3y * c2x;
  if (d != 0) {
    // upper part of intersection point formula
    let u1 = l1[0] * l1[3] - l1[1] * l1[2];
    let u4 = l2[0] * l2[3] - l2[1] * l2[2];
    // intersection point formula
    let px = +((u1 * c2x - c3x * u4) / d).toFixed(decimals);
    let py = +((u1 * c2y - c3y * u4) / d).toFixed(decimals);
    intersection = [px, py];
  }
  return intersection;
}

function hslToHex(h, s, l) {
  l /= 100;
  const a = s * Math.min(l, 1 - l) / 100;
  const f = n => {
    const k = (n + h / 30) % 12;
    const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
    return Math.round(255 * color).toString(16).padStart(2, '0'); // convert to Hex and prefix "0" if needed
  };
  return `#${f(0)}${f(8)}${f(4)}`;
}
<div class="pie-generate" data-pie='{
        "width": 100,
        "height": 100,
        "radius": 50,
        "centerX": 50,
        "centerY": 50,
        "strokeWidth": 20,
        "gap": 0,
        "decimals": 3,
        "segments": [
            ["25", {"color":"#80e080", "id":"seg01", "class":"segCustom"}],
            ["45", {"color":"#4fc3f7", "id":"seg02", "class":"segCustom"}],
            ["10", {"color":"#9575cd", "id":"seg03", "class":"segCustom"}],
            ["20", {"color":"#f06292", "id":"seg04", "class":"segCustom"}]
            ]
        }'>
</div>

You can tweak different segment percentages by changing the JSOn data-attribute.

<div class="pie-generate" data-pie='{
        "width": 100,
        "height": 100,
        "radius": 50,
        "centerX": 50,
        "centerY": 50,
        "strokeWidth": 20,
        "gap": 0,
        "decimals": 3,
        "segments": [
            ["25", {"color":"#80e080", "id":"seg01", "class":"segCustom"}],
            ["45", {"color":"#4fc3f7", "id":"seg02", "class":"segCustom"}],
            ["10", {"color":"#9575cd", "id":"seg03", "class":"segCustom"}],
            ["20", {"color":"#f06292", "id":"seg04", "class":"segCustom"}]
            ]
        }'>
</div>

Segment output:

<path d="M 50 0 A 50 50 0 0 1 100 50 L 80 50 A 30 30 0 0 0 50 20 z" fill="#80e080" />
herrstrietzel
  • 11,541
  • 2
  • 12
  • 34