Vets
Vets

Reputation: 76

SVG pie chart Text labels not showing using just css html and svg (no chart js or other)

I can see "Project" my text in the editor window but cannot get it to show on the graph or in the pie wedge area. Any help would be greatly appreciated. I need the label text aligned to the right or to show on top. I stacked other images on top of the graph so I tried a z-index to do it. Here is my code.

/* SHOW LABEL ON HOVER */
jQuery(".group_path").hover(
  function() {
    jQuery(this).find(".text_toggle").css("display", "block");
  },
  function() {
    jQuery(this).find(".text_toggle").css("display", "none");
  }
);
/* Trying to get text to show as labels - also Jquery code in script file */

.text_toggle {
  display: none;
  fill: transparent;
}

.group_path:hover .text_toggle {
  display: block;
  font-size: 1em;
  text-align: right;
  z-index: 5;
}
<!-- Jquery 3.6 -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>


<!-- Body -->
<svg viewBox='-1 -1 2 2' style='transform: scale(1.0);  rotate(-90deg)'>
  <g id="4" >
    <g id="4.01" class="group_path" fill='rgb(84,161,229)'  >
      <path stroke='white' stroke-width='.0125px' d='M 1.000000 0.000000 A 1 1 0 0 1 0.873262 0.487250 L 0 0 '></path>
      <text class="text_toggle" d='M 1.000000 0.000000 A 1 1 0 0 1 0.873262 0.487250 L 0 0 '  ><tspan >Project</tspan></text>
    </g>
    <g id="4.02" class="group_path" fill='rgb(242,162,84)'>
      <path  stroke='white' stroke-width='.0125px' d='M 0.873262 0.487250 A 1 1 0 0 1 -0.147119 0.989119 L 0 0 '></path>
    </g>
    <g id="4.03" class="group_path" fill='rgb(237,110,133)' >
      <path stroke='white' stroke-width='.0125px' d='M -0.147119 0.989119 A 1 1 0 0 1 -0.689114 0.724653 L 0 0 '></path>
    </g>
    <g id="4.04" class="group_path" fill='rgb(173,205,225)' >
      <path stroke='white' stroke-width='.0125px' d='M -0.689114 0.724653 A 1 1 0 0 1 -0.915241 0.402907 L 0 0 '></path>
    </g>
    <g id="4.05" class="group_path" fill='rgb(187,221,147)' >
      <path stroke='white' stroke-width='.0125px' d='M -0.915241 0.402907 A 1 1 0 0 1 -0.946085 0.323917 L 0 0 '></path>
    </g>
    <g id="4.06" class="group_path" fill='rgb(238,158,155)' >
      <path stroke='white' stroke-width='.0125px' d='M -0.946085 0.323917 A 1 1 0 0 1 -0.978581 -0.205863 L 0 0 '></path>
    </g>
    <g id="4.07" class="group_path" fill='rgb(84,161,229)' >
      <path stroke='white' stroke-width='.0125px' d='M -0.978581 -0.205863 A 1 1 0 0 1 -0.879316 -0.476238 L 0 0 '></path>
    </g>
    <g id="4.08" class="group_path" fill='rgb(108,190,191)'>
      <path stroke='white' stroke-width='.0125px' d='M -0.879316 -0.476238 A 1 1 0 0 1 -0.527846 -0.849340 L 0 0 '></path>
    </g>
    <g id="4.09" class="group_path" fill='rgb(242,162,84)' >
      <path stroke='white' stroke-width='.0125px' d='M -0.527846 -0.849340 A 1 1 0 0 1 0.056518 -0.998402 L 0 0 '></path>
    </g>
    <g id="4.10" class="group_path" fill='rgb(237,110,133)'>
      <path stroke='white' stroke-width='.0125px' d='M 0.056518 -0.998402 A 1 1 0 0 1 0.543760 -0.839241 L 0 0 '></path>
    </g>
    <g id="4.11" class="group_path" fill='rgb(173,205,225)'>
      <path stroke='white' stroke-width='.0125px' d='M 0.543760 -0.839241 A 1 1 0 0 1 0.711535 -0.702650 L 0 0 '></path>
    </g>
    <g id="4.12" class="group_path" fill='rgb(187,221,147)'>
      <path stroke='white' stroke-width='.0125px' d='M 0.711535 -0.702650 A 1 1 0 0 1 0.724653 -0.689114 L 0 0 '></path>
    </g>
    <g id="4.13" class="group_path" fill='rgb(42,228,229)'>
      <path stroke='white' stroke-width='.00625px' d='M 0.724653 -0.689114 A 1 1 0 0 1 1.000000 -0.000000 L 0 0 '></path>
    </g>                      
    <circle fill='#fff' cx='0' cy='0' r='0.80'/>
    </g>
</svg>

I don't want to use jquery if I can avoid it

Upvotes: 0

Views: 673

Answers (1)

herrstrietzel
herrstrietzel

Reputation: 17265

You face several problems:

  • text elements can't use any d attributes (preserved for path elements)
  • css property z-index won't have any effect on svg elements – you'll need to add labels on top of your pie chart segments
  • you need to get x/y coordinates for your label <text> elements – js to the rescue!
  • ids are not ideal (starting with numbers, containing periods) – those elements are not selectable in css or js (unless you escape them)
  • (better close your segments' paths via z command)

How to get the right text anchor coordinates

pie chart test anchors

For properly aligned text labels, we need to get the x/y coordinates of the semi-arc of each segment.
(illustrated by red circles)

The main concept is to check where pie chart wedges are intersecting a "centerline" circle: we need to add this auxiliary circle element with a radius between outer radius=1 and inner radius=0.8 – so our centerline circle needs to have a radius of 0.9.

Example 1: pre-processing (find x/y for text labels)

let pie = document.querySelector("svg");
let segments = pie.querySelectorAll(".group_path");
let labelGroupHtml = "";
let textanchors = "";

// auxiliary circle element to get label coordinates
let circleIntersect = document.querySelector(".circleIntersect");
let circleLength = circleIntersect.getTotalLength();
// define precision for intersection checking
let steps = circleLength / 180;
let circlePoints = [];
for (let i = 0; i < circleLength; i += steps) {
  let point = circleIntersect.getPointAtLength(i);
  circlePoints.push(point);
}

// find intersections beween each piechart slices and auxiliary circle
function getIntersect(path) {
  let intersects = [];
  let middlePoint = 0;
  for (let i = 0; i < circlePoints.length; i++) {
    let point = circlePoints[i];
    let isIntersect = path.isPointInFill(point);
    if (isIntersect) {
      intersects.push({
        x: +point.x.toFixed(2),
        y: +point.y.toFixed(2)
      });
    }
  }
  if (intersects.length) {
    // get segment's middle coordinates
    let midIndex = Math.ceil((intersects.length - 1) / 2);
    middlePoint = intersects[midIndex];
  }
  return middlePoint;
}

segments.forEach(function(el, i) {
  let segementId = "label_" + i;
  let path = el.querySelector("path");
  let labelText = path.getAttribute("data-label");
  // add generic labels if not defined
  labelText = labelText ? labelText : "Segment" + (i + 1);
  path.setAttribute("data-target", segementId);

  let intersect = getIntersect(path);
  if (intersect) {
    let midX = intersect["x"];
    let midY = intersect["y"];
    textanchors +=
      '<circle class="notSelectable" fill="red" cx="' +
      midX +
      '" ' +
      'cy="' +
      midY +
      '" r="0.02" />';

    let label =
      '<text dy="2%" id="' +
      segementId +
      '"  x="' +
      midX +
      '" y="' +
      midY +
      '" transform="rotate(90 ' +
      midX +
      " " +
      midY +
      ')"  class="text_label" ><tspan >' +
      labelText +
      "</tspan></text>";
    labelGroupHtml += label;
  }
});

pie.insertAdjacentHTML(
  "beforeend",
  '<g class="labels">' + labelGroupHtml + "</g>"
);

// just for illustrating the retrieved text anchors
pie
  .querySelector(".preprocessing")
  .insertAdjacentHTML(
    "beforeend",
    '<g class="textanchors">' + textanchors + "</g>"
  );

// event listeners
let pieSegemts = pie.querySelectorAll("path");
if (pieSegemts.length) {
  pieSegemts.forEach(function(segment, i) {
    segment.addEventListener("click", function(e) {
      /**
       * uncomment the closelabels call and
       * mouseleave event listener if you need only one segemnt to be active
       */
      /*
      closeLabels();
      */
      let labelSelector = e.currentTarget.getAttribute("data-target");
      let label = pie.querySelector("#" + labelSelector);
      label.classList.toggle("label_active");
      segment.classList.toggle("segment_active");
    });
    /*
    segment.addEventListener("mouseleave", function (e) {
      closeLabels();
    });
    */
  });
}

// hide other labels
function closeLabels() {
  let opened = pie.querySelectorAll(".label_active, .segment_active");
  opened.forEach(function(el, i) {
    el.classList.remove("label_active");
    el.classList.remove("segment_active");
  });
}

// ungroup elements – inherit properties
ungroup(".group_path");

function ungroup(selector) {
  let groups = document.querySelectorAll(selector);
  groups.forEach(function(group, i) {
    let attributes = [...group.attributes];
    let children = [...group.children];
    children.forEach(function(el, i) {
      attributes.forEach(function(att, i) {
        el.setAttribute(att["name"], att["nodeValue"]);
        el.classList.add("segment");
      });
      group.parentNode.insertBefore(el, group.nextElementSibling);
      group.remove();
    });
  });
}

// replace ids containing numbers
cleanNumIds();

function cleanNumIds() {
  let idEls = document.querySelectorAll("[id]");
  idEls.forEach(function(el, i) {
    let id = el.id;
    let idNum = (+id).toString();
    if (idNum === id) {
      el.setAttribute("data-id", id);
      el.id = "seg_" + id.replaceAll(".", "-");
    }
  });
}
body {
  font-family: "Sogoe UI", "Open Sans", Arial;
}

svg {
  display: inline-block;
  width: 20em;
  overflow: visible;
  border: 1px solid #ccc;
}

.segment {
  stroke: #fff;
  stroke-width: 0.0125;
}

.segment_active {
  opacity: 0.5;
}

.text_label {
  font-size: 0.1px;
  fill: #000;
  text-anchor: start;
  visibility: hidden;
}

.text_label,
.notSelectable {
  user-select: none;
  pointer-events: none;
}

.label_active {
  visibility: visible;
}
<!-- Body -->
<svg viewBox='-1 -1 2 2' style='transform:rotate(-90deg)'>
  <g id="4">
    <g id="4.01" class="group_path" fill='rgb(84,161,229)'>
      <path d='M 1.000000 0.000000 A 1 1 0 0 1 0.873262 0.487250 L 0 0z' data-label="Project"></path>
    </g>
    <g id="4.02" class="group_path" fill='rgb(242,162,84)'>
      <path d='M 0.873262 0.487250 A 1 1 0 0 1 -0.147119 0.989119 L 0 0z' data-label="Segment 2"></path>
    </g>
    <g id="4.03" class="group_path" fill='rgb(237,110,133)'>
      <path d='M -0.147119 0.989119 A 1 1 0 0 1 -0.689114 0.724653 L 0 0z'></path>
    </g>
    <g id="4.04" class="group_path" fill='rgb(173,205,225)'>
      <path d='M -0.689114 0.724653 A 1 1 0 0 1 -0.915241 0.402907 L 0 0z'></path>
    </g>
    <g id="4.05" class="group_path" fill='rgb(187,221,147)'>
      <path d='M -0.915241 0.402907 A 1 1 0 0 1 -0.946085 0.323917 L 0 0z'></path>
    </g>
    <g id="4.06" class="group_path" fill='rgb(238,158,155)'>
      <path d='M -0.946085 0.323917 A 1 1 0 0 1 -0.978581 -0.205863 L 0 0z'></path>
    </g>
    <g id="4.07" class="group_path" fill='rgb(84,161,229)'>
      <path d='M -0.978581 -0.205863 A 1 1 0 0 1 -0.879316 -0.476238 L 0 0z'></path>
    </g>
    <g id="4.08" class="group_path" fill='rgb(108,190,191)'>
      <path d='M -0.879316 -0.476238 A 1 1 0 0 1 -0.527846 -0.849340 L 0 0z'></path>
    </g>
    <g id="4.09" class="group_path" fill='rgb(242,162,84)'>
      <path d='M -0.527846 -0.849340 A 1 1 0 0 1 0.056518 -0.998402 L 0 0z'></path>
    </g>
    <g id="4.10" class="group_path" fill='rgb(237,110,133)'>
      <path d='M 0.056518 -0.998402 A 1 1 0 0 1 0.543760 -0.839241 L 0 0z'></path>
    </g>
    <g id="4.11" class="group_path" fill='rgb(173,205,225)'>
      <path d='M 0.543760 -0.839241 A 1 1 0 0 1 0.711535 -0.702650 L 0 0z'></path>
    </g>
    <g id="4.12" class="group_path" fill='rgb(187,221,147)'>
      <path d='M 0.711535 -0.702650 A 1 1 0 0 1 0.724653 -0.689114 L 0 0z'></path>
    </g>
    <g id="4.13" class="group_path" fill='rgb(42,228,229)'>
      <path d='M 0.724653 -0.689114 A 1 1 0 0 1 1.000000 -0.000000 L 0 0z'></path>
    </g>
    <circle fill='#fff' cx='0' cy='0' r='0.80' />
  </g>
  <!-- pseudo donut hole -->
  <circle fill="#fff" cx='0' cy='0' r='0.80' />
  <!-- circle for text x/y analyzing -->
  <g class="preprocessing">
    <circle class="circleIntersect notSelectable" stroke-width="0.01" stroke='red' fill="none" stroke-width="0.1" cx='0' cy='0' r='0.9' />
  </g>
</svg>

How it works:
We need to "travel" along the aforementioned centerline circle and check when segments intersects:
First we need to get this circles pathlength

  • by getTotalLength() (OK, we could have uses pathLength property as well ...)
  • then we split the circumference into segments resulting in an array of path length positions by getPointAtLength() . In this example 180 divisions/segments to provide enough precision when finding the ideal text x/y for a segment's label (with 100 divisions we might not get very thin pie wedges).
  • we can then check each path (pie segment) for points intersecting with the centerline circle by path.isPointInFill(point) and save them to an array of DOMPoints containing x/y coordinates (let intersects)
  • we get multiple intersecting points – the one we're interested in is the middle point (let midIndex = Math.ceil((intersects.length - 1) / 2))
  • now we can append <text> elements to the pie chart svg with right x/y coordinates (the label text is retrieved from a data-attribute)

... quite a lot of js?

Once the pie chart's svg is optimized and transformed you can save it as a static asset (e.g. by inspecting it in dev tools) and remove the pre-processing functions like so:

let pie = document.querySelector("svg");
let segments = pie.querySelectorAll(".group_path");

// event listeners
// event listeners
let pieSegemts = pie.querySelectorAll("path");
if (pieSegemts.length) {
  pieSegemts.forEach(function (segment, i) {
    segment.addEventListener("mouseenter", function (e) {
      /**
       * uncomment the closelabels call and
       * mouseleave event listener if you need only one segemnt to be active
       */
      closeLabels();
     
      let labelSelector = e.currentTarget.getAttribute("data-target");
      let label = pie.querySelector("#" + labelSelector);
      label.classList.toggle("label_active");
      segment.classList.toggle("segment_active");
    });
    
    segment.addEventListener("mouseleave", function (e) {
      closeLabels();
    });
    
  });
}

function closeLabels() {
  let opened = pie.querySelectorAll(".label_active, .segment_active");
  opened.forEach(function (el, i) {
    el.classList.remove("segment_active");
    el.classList.remove("label_active");
  });
}
body {
  font-family: "Sogoe UI", "Open Sans", Arial;
}
svg {
  display: inline-block;
  width: 20em;
  overflow: visible;
  border: 1px solid #ccc;
}

.segment {
  stroke: #fff;
  stroke-width: 0.0125;
}

.segment_active {
  opacity: 0.5;
}

.text_label {
  font-size: 0.1px;
  fill: #000;
  text-anchor: start;
  visibility: hidden;
}
.text_label,
.notSelectable {
  user-select: none;
  pointer-events: none;
}

.label_active {
  visibility: visible;
}
<svg viewBox="-1 -1 2 2" style="transform:rotate(-90deg)">
  <g id="seg_4" data-id="4">
    <path d="M 1.000000 0.000000 A 1 1 0 0 1 0.873262 0.487250 L 0 0z" data-label="Project" data-target="label_0" id="seg_4-01" class="group_path segment" fill="rgb(84,161,229)" data-id="4.01"></path>
    <path d="M 0.873262 0.487250 A 1 1 0 0 1 -0.147119 0.989119 L 0 0z" data-label="Segment 2" data-target="label_1" id="seg_4-02" class="group_path segment" fill="rgb(242,162,84)" data-id="4.02"></path>
    <path d="M -0.147119 0.989119 A 1 1 0 0 1 -0.689114 0.724653 L 0 0z" data-target="label_2" id="seg_4-03" class="group_path segment" fill="rgb(237,110,133)" data-id="4.03"></path>
    <path d="M -0.689114 0.724653 A 1 1 0 0 1 -0.915241 0.402907 L 0 0z" data-target="label_3" id="seg_4-04" class="group_path segment" fill="rgb(173,205,225)" data-id="4.04"></path>
    <path d="M -0.915241 0.402907 A 1 1 0 0 1 -0.946085 0.323917 L 0 0z" data-target="label_4" id="seg_4-05" class="group_path segment" fill="rgb(187,221,147)" data-id="4.05"></path>
    <path d="M -0.946085 0.323917 A 1 1 0 0 1 -0.978581 -0.205863 L 0 0z" data-target="label_5" id="seg_4-06" class="group_path segment" fill="rgb(238,158,155)" data-id="4.06"></path>
    <path d="M -0.978581 -0.205863 A 1 1 0 0 1 -0.879316 -0.476238 L 0 0z" data-target="label_6" id="seg_4-07" class="group_path segment" fill="rgb(84,161,229)" data-id="4.07"></path>
    <path d="M -0.879316 -0.476238 A 1 1 0 0 1 -0.527846 -0.849340 L 0 0z" data-target="label_7" id="seg_4-08" class="group_path segment" fill="rgb(108,190,191)" data-id="4.08"></path>
    <path d="M -0.527846 -0.849340 A 1 1 0 0 1 0.056518 -0.998402 L 0 0z" data-target="label_8" id="seg_4-09" class="group_path segment" fill="rgb(242,162,84)" data-id="4.09"></path>
    <path d="M 0.056518 -0.998402 A 1 1 0 0 1 0.543760 -0.839241 L 0 0z" data-target="label_9" id="4.10" class="group_path segment" fill="rgb(237,110,133)"></path>
    <path d="M 0.543760 -0.839241 A 1 1 0 0 1 0.711535 -0.702650 L 0 0z" data-target="label_10" id="seg_4-11" class="group_path segment" fill="rgb(173,205,225)" data-id="4.11"></path>
    <path d="M 0.711535 -0.702650 A 1 1 0 0 1 0.724653 -0.689114 L 0 0z" data-target="label_11" id="seg_4-12" class="group_path segment" fill="rgb(187,221,147)" data-id="4.12"></path>
    <path d="M 0.724653 -0.689114 A 1 1 0 0 1 1.000000 -0.000000 L 0 0z" data-target="label_12" id="seg_4-13" class="group_path segment" fill="rgb(42,228,229)" data-id="4.13"></path>
    <circle fill="#fff" cx="0" cy="0" r="0.80"></circle>
  </g>
  <!-- pseudo donut hole -->
  <circle fill="#fff" cx="0" cy="0" r="0.80"></circle>
  <g class="labels"><text dy="2%" id="label_0" x="0.87" y="0.23" transform="rotate(90 0.87 0.23)" class="text_label">
      <tspan>Project</tspan>
    </text><text dy="2%" id="label_1" x="0.38" y="0.81" transform="rotate(90 0.38 0.81)" class="text_label">
      <tspan>Segment 2</tspan>
    </text><text dy="2%" id="label_2" x="-0.38" y="0.81" transform="rotate(90 -0.38 0.81)" class="text_label">
      <tspan>Segment3</tspan>
    </text><text dy="2%" id="label_3" x="-0.73" y="0.52" transform="rotate(90 -0.73 0.52)" class="text_label">
      <tspan>Segment4</tspan>
    </text><text dy="2%" id="label_4" x="-0.84" y="0.32" transform="rotate(90 -0.84 0.32)" class="text_label">
      <tspan>Segment5</tspan>
    </text><text dy="2%" id="label_5" x="-0.9" y="0.03" transform="rotate(90 -0.9 0.03)" class="text_label">
      <tspan>Segment6</tspan>
    </text><text dy="2%" id="label_6" x="-0.84" y="-0.32" transform="rotate(90 -0.84 -0.32)" class="text_label">
      <tspan>Segment7</tspan>
    </text><text dy="2%" id="label_7" x="-0.65" y="-0.62" transform="rotate(90 -0.65 -0.62)" class="text_label">
      <tspan>Segment8</tspan>
    </text><text dy="2%" id="label_8" x="-0.2" y="-0.88" transform="rotate(90 -0.2 -0.88)" class="text_label">
      <tspan>Segment9</tspan>
    </text><text dy="2%" id="label_9" x="0.26" y="-0.86" transform="rotate(90 0.26 -0.86)" class="text_label">
      <tspan>Segment10</tspan>
    </text><text dy="2%" id="label_10" x="0.58" y="-0.69" transform="rotate(90 0.58 -0.69)" class="text_label">
      <tspan>Segment11</tspan>
    </text><text dy="2%" id="label_11" x="0.65" y="-0.62" transform="rotate(90 0.65 -0.62)" class="text_label">
      <tspan>Segment12</tspan>
    </text><text dy="2%" id="label_12" x="0.84" y="-0.32" transform="rotate(90 0.84 -0.32)" class="text_label">
      <tspan>Segment13</tspan>
    </text></g>
</svg>

The only js functions left are responsible for event binding (click, mouseover etc.) and toggling.

Edit: Finding label coordinates

you should also check out the way more elegant approach by Paul LeBeau (Pure svg pie chart, text align center)

Upvotes: 2

Related Questions