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.
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