4

See my code so far for Drilldown graph. This will show main categories and after clicking on one it will show sub categories. Everything works as expected apart from overlapping labels for smaller values.enter image description here

import * as d3 from 'd3';
import { useEffect, useRef, useState } from 'react';
import { deepFilter, deepSearch } from '../../utilz';
const graphData = (data) => {
  return data.map((item, index) => {
    return {
      ...item,
      color: d3.schemePastel1[index],
      children: item.children.map((item, index) => {
        return {
          ...item,
          color: d3.schemePastel1[index],
        };
      }),
    };
  });
};

const Drilldown = ({ data, x, y }) => {
  const [level, setLevel] = useState({ level: 0, index: null });
  const graph = useRef(null);

  useEffect(() => {
    let isCancelled = false;
    const drawChart = (graph, width, height) => {
      const legendRectSize = 18;
      const legendSpacing = 4;
      const radius = 100;
      const donutWidth = 40;
      const textOffset = 10;

      const svg = d3
        .select(graph.current)
        .append('svg')
        .attr('width', width)
        .attr('height', height)
        .append('g')
        .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');

      const pie = d3
        .pie()
        .value(function (d) {
          return d.value;
        })
        .sort(null);

      const arc = d3
        .arc()
        .innerRadius(radius - donutWidth)
        .outerRadius(radius);

      const arcs = svg
        .selectAll('g.slice')
        .data(pie(deepFilter(graphData(data), 'level', level.level, 'index', level.index)))
        .enter()
        .append('g')
        .attr('class', 'slice');

      const arcOver = d3
        .arc()
        .outerRadius(radius * 1.1)
        .innerRadius(radius - donutWidth);

      arcs
        .append('path')
        .attr('fill', function (d, i) {
          return d.data.color;
        })
        .attr('d', function (d) {
          return arc(d);
        })
        .on('mouseover', (e, d) => {
          d3.select(e.originalTarget)
            .transition()
            .duration(100)
            .attr('d', arcOver)
            .attr('fill', () => {
              return d3.hsl(d.data.color).darker(0.5);
            })
            .style('cursor', 'pointer');
        })
        .on('mouseout', (d) => {
          d3.select(d.originalTarget)
            .transition()
            .duration(500)
            .ease(d3.easeBounce)
            .attr('fill', (d, i) => {
              return d.data.color;
            })
            .attr('d', arc);
        })
        .on('click', (e, d) => {
          if (d.data.level === 0) {
            if (d.data.children.length === 0) {
            } else {
              setLevel({ level: d.data.level + 1, index: d.data.index });
            }
          }
        });

      const labels = svg
        .selectAll('g.slice')
        .append('text')
        .attr('transform', function (d) {
          var pos = arcOver.centroid(d);
          pos[0] = radius * (midAngle(d) < Math.PI ? 1.5 : -1) + (midAngle(d) < Math.PI ? -20 : -20);
          return 'translate(' + pos + ')';
        })
        .attr('text-anchor', 'middle')
        .text(function (d, i) {
          return d.data.value + ' MB';
        })
        .style('fill', 'gray')
        .style('font-size', '18px')
        .style('font-weight', '600');

      let prev;
      labels.each(function (d, i) {
        console.log(i, d);
        if (i > 0) {
          var thisbb = this.getBoundingClientRect(),
            prevbb = prev.getBoundingClientRect();
          // move if they overlap
          if (!(thisbb.right < prevbb.left || thisbb.left > prevbb.right || thisbb.bottom < prevbb.top || thisbb.top > prevbb.bottom)) {
            const matrix = prev.transform.baseVal.consolidate().matrix;
            d3.select(this).attr('transform', `translate(${matrix.e}, ${matrix.f + prevbb.bottom - prevbb.top})`);
          }
        }

        prev = this;
      });

      const legend = svg
        .selectAll('.legend')
        .data(deepFilter(graphData(data), 'level', level.level, 'index', level.index))
        .enter()
        .append('g')
        .attr('class', 'legend')
        .attr('transform', function (d, i) {
          const legendHeight = legendRectSize + legendSpacing;
          if (i % 2 === 0) {
            const vert = height / 2 + legendHeight * (i > 0 && i - 1);
            return 'translate(' + (width / 2) * -1 + ',' + vert + ')';
          } else {
            const vert = height / 2 + legendHeight * (i > 1 && i - 2);
            return 'translate(' + 0 + ',' + vert + ')';
          }
        });
      legend
        .append('rect')
        .attr('width', legendRectSize)
        .attr('height', legendRectSize)
        .style('fill', (d, i) => {
          return d.color;
        })
        .style('stroke', (d, i) => {
          return d.color;
        });

      legend
        .append('text')
        .attr('x', legendRectSize + legendSpacing)
        .attr('y', legendRectSize - legendSpacing)
        .text(function (d) {
          return d.name.substring(0, 30);
        });

      function midAngle(d) {
        return d.startAngle + (d.endAngle - d.startAngle) / 2;
      }

      // const polylines = svg
      //   .selectAll('g.slice')
      //   .append('polyline')
      //   .attr('points', function (d) {
      //     const pos = arcOver.centroid(d);
      //     pos[0] = radius * 1 * (midAngle(d) < Math.PI ? 1 : -1) + (midAngle(d) < Math.PI ? 10 : -20);
      //     return [arc.centroid(d), arcOver.centroid(d), pos];
      //   })
      //   .style('fill', 'none')
      //   .style('stroke', 'gray')
      //   .style('stroke-width', '1px');

      d3.select('svg').attr('height', height + d3.selectAll('.legend').size() * legendRectSize);
    };

    if (!isCancelled) {
      d3.selectAll('svg').remove();
      drawChart(graph, 500, 250);
    }
    return () => {
      isCancelled = true;
    };
  }, [data, level]);

  return <div ref={graph}> {level.level > 0 && <button onClick={() => setLevel({ level: 0, index: null })}>Back</button>}</div>;
};

export default Drilldown;

and this is data for the graph

[
  {
    "level": 0,
    "index": 0,
    "name": "Business Broadband",
    "value": 50,
    "children": [],
    "color": "#fbb4ae"
  },
  {
    "level": 0,
    "index": 1,
    "name": "Dedicated",
    "value": 650,
    "children": [
      {
        "_id": "60e6c6177258f94970bd2d07",

        "index": 1,
        "level": 1,
        "value": 600,
        "color": "#fbb4ae"
      },
      {
        "_id": "60e6c6e363d1e60cb03f3a57",

        "index": 1,
        "level": 1,
        "value": 10,
        "color": "#b3cde3"
      },
      {
        "_id": "60e6cd20d29baa52f801318a",

        "index": 1,
        "level": 1,
        "value": 10,
        "color": "#ccebc5"
      },
      {
        "_id": "60e6c6883cd7b54e4889580c",

        "index": 1,
        "level": 1,
        "value": 10,
        "color": "#decbe4"
      },
      {
        "_id": "60e6c3785b41f64e2c4f3940",

        "index": 1,
        "level": 1,
        "value": 10,
        "color": "#fed9a6"
      },
      {
        "_id": "60c2332a827d5d09e071f6cc",

        "index": 1,
        "level": 1,
        "value": 10,
        "color": "#ffffcc"
      }
    ],
    "color": "#b3cde3"
  },
  {
    "level": 0,
    "index": 2,
    "name": "Core Ports",
    "value": 40,
    "children": [
      {
        "index": 2,
        "level": 1,
        "value": 40,
        "color": "#fbb4ae"
      }
    ],
    "color": "#ccebc5"
  },
  {
    "level": 0,
    "index": 3,
    "name": "Remaining",
    "value": 650,
    "children": [],
    "color": "#decbe4"
  }
]

I know my code is bit messy and for sure it could be shortened but iam still learning D3 library :-)

I have looked at this example for overlapping (link) and many others but nothing worked properly. I also would like to have polylines to each label which i have commented out atm as i need to first offset the labels.

Any help would be appreciated

EDIT:

adding this codesandbox demo

Jakub Koudela
  • 160
  • 1
  • 18
  • 2
    This is one of those 'fun' data visualization problems where you would want to research some label placement algorithms. There are a few layout methods you could probably do that will work in some scenarios. All that said, simplest solution would be to use mouseover tooltips/labels if the slice is too small. – apachuilo Sep 22 '21 at 18:36
  • Showing the label only on hover is probably the easiest solution. Another more creative solution could be to use d3.js to build a force network from the overlapping labels and let the forces balance out to have labels, that do not overlap. https://www.d3indepth.com/force-layout/ – wasserholz Sep 29 '21 at 07:34

0 Answers0