I am new to d3 and I'm a trying to make a visualization with interactive nodes where each node can be clicked. When the node is clicked it should expand to show child nodes. I was able to get all the nodes to display interactively and I added an on click event, but I am not sure how I can get the child nodes to expand on click.
I am using the data from data.children in the onclick function and passing it to d3.hierarchy to set the data as the root. I am just not sure how to expand the data.
I am looking to make something like this where the circle node is in the center and the child nodes expand around it/outwards.
child child
\ /
node
|
child
Does anyone have any suggestions on how I could achieve this? I found d3.tree in the docs but that is more of a horizontal tree structure.
export default function ThirdTab(): React.MixedElement {
const ref = useRef();
const viewportDimension = getViewportDimension();
useEffect(() => {
const width = viewportDimension.width - 150;
const height = viewportDimension.height - 230;
const svg = d3
.select(ref.current)
.style('width', width)
.style('height', height);
const zoomG = svg.attr('width', width).attr('height', height).append('g');
const g = zoomG
.append('g')
.attr('transform', `translate(500,280) scale(0.31)`);
svg.call(
d3.zoom().on('zoom', () => {
zoomG.attr('transform', d3.event.transform);
}),
);
const nodes = g.selectAll('g').data(annotationData);
const group = nodes
.enter()
.append('g')
.attr('cx', width / 2)
.attr('cy', height / 2)
.attr('class', 'dotContainer')
.style('cursor', 'pointer')
.call(
d3
.drag()
.on('start', function dragStarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.03).restart();
d.fx = d.x;
d.fy = d.y;
})
.on('drag', function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
})
.on('end', function dragEnded(d) {
if (!d3.event.active) simulation.alphaTarget(0.03);
d.fx = null;
d.fy = null;
}),
);
const circle = group
.append('circle')
.attr('class', 'dot')
.attr('r', 20)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.style('fill', '#33adff')
.style('fill-opacity', 0.3)
.attr('stroke', '#b3a2c8')
.style('stroke-width', 4)
.attr('id', d => d.name)
.on('click', function click(data) {
const root = d3.hierarchy(data.children);
const links = root.links();
const nodes = root.descendants();
console.log(nodes);
});
const label = group
.append('text')
.attr('x', d => d.x)
.attr('y', d => d.y)
.text(d => d.name)
.style('text-anchor', 'middle')
.style('fill', '#555')
.style('font-family', 'Arial')
.style('font-size', 15);
const simulation = d3
.forceSimulation()
.force(
'center',
d3
.forceCenter()
.x(width / 2)
.y(height / 2),
)
.force('charge', d3.forceManyBody().strength(1))
.force(
'collide',
d3.forceCollide().strength(0.1).radius(170).iterations(1),
);
simulation.nodes(annotationData).on('tick', function () {
circle
.attr('cx', function (d) {
return d.x;
})
.attr('cy', function (d) {
return d.y;
});
label
.attr('x', function (d) {
return d.x;
})
.attr('y', function (d) {
return d.y 40;
});
});
}, [viewportDimension.width, viewportDimension.height]);
return (
<div className="third-tab-content">
<style>{`
.tooltip {
position: absolute;
z-index: 10;
visibility: hidden;
background-color: lightblue;
text-align: center;
padding: 4px;
border-radius: 4px;
font-weight: bold;
color: rgb(179, 162, 200);
}
`}</style>
<svg
ref={ref}
id="annotation-container"
role="img"
title="Goal Tree Container"></svg>
</div>
);
}
CodePudding user response:
`useEffect(() => { const width = viewportDimension.width - 150; const height = viewportDimension.height - 230;
const svg = d3
.select(ref.current)
.style('width', width)
.style('height', height);
const zoomG = svg.attr('width', width).attr('height', height).append('g');
const g = zoomG
.append('g')
.attr('transform', `translate(500,280) scale(0.31)`);
svg.call(
d3.zoom().on('zoom', () => {
zoomG.attr('transform', d3.event.transform);
}),
);
const nodes = g.selectAll('g').data(annotationData);
const simulation = d3
.forceSimulation(annotationData)
.force(
'center',
d3
.forceCenter()
.x(width / 2)
.y(height / 2),
)
.force('charge', d3.forceManyBody().strength(1));
const group = nodes
.enter()
.append('g')
.attr('x', d => d.x)
.attr('y', d => d.y)
.attr('id', d => 'container' d.index)
.attr('class', 'dotContainer')
.style('white-space', 'pre')
.style('cursor', 'pointer')
.call(
d3
.drag()
.on('start', function dragStarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.03).restart();
d.fx = d.x;
d.fy = d.y;
})
.on('drag', function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
})
.on('end', function dragEnded(d) {
if (!d3.event.active) simulation.alphaTarget(0.03);
d.fx = null;
d.fy = null;
}),
);
simulation.on('tick', function () {
group.attr('transform', function (d) {
return 'translate(' d.x ',' d.y ')';
});
children
.attr('x', function (d) {
return d.x;
})
.attr('y', function (d) {
return d.y;
});
});
simulation.force(
'collide',
d3.forceCollide().strength(0.1).radius(170).iterations(10),
);
let currentlyExpandedNode;
let currentNode;
const circle = group
.append('circle')
.attr('class', 'dot')
.attr('id', d => {
return 'circle' d.index;
})
.attr('r', 20)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.style('fill', '#33adff')
.style('fill-opacity', 0.3)
.attr('stroke', 'gray')
.style('stroke-width', 4)
.on('click', function click(data) {
currentNode = d3.select(`#container${data.index}`);
if (currentlyExpandedNode) {
d3.selectAll('.child').remove();
d3.selectAll('#child-text').remove();
}
currentlyExpandedNode = data;
const pie = d3
.pie()
.value(() => 1)
.sort(null);
const circlex1 = currentNode.select(`#circle${data.index}`).attr('cx');
const circley1 = currentNode.select(`#circle${data.index}`).attr('cy');
const children = currentNode
.selectAll('line.child')
.data(pie(data.children))
.enter()
.append('line')
.attr('stroke', 'gray')
.attr('stroke-width', 1)
.attr('stroke-dasharray', '5 2')
.attr('class', 'child')
.attr('x1', circlex1) // starting point
.attr('y1', circley1)
.attr('x2', circlex1) // transition starting point
.attr('y2', circley1)
.transition()
.duration(300)
.attr('x2', circlex1 62) // end point
.attr('y2', circley1 - 62);
const childrenCircles = currentNode
.selectAll('circle.child')
.data(data.children)
.enter()
.append('circle')
.attr('class', 'child')
.attr('cx', () => circlex1 70)
.attr('cy', () => circley1 - 70)
.attr('r', 10)
.style('fill', '#b3a2c8')
.style('fill-opacity', 0.8)
.attr('stroke', 'gray')
.style('stroke-width', 2);
children.each(childData => {
currentNode
.append('text')
.attr('x', () => circlex1 80)
.attr('y', () => circley1 - 100)
.text(childData.data.name)
.attr('id', 'child-text')
.style('text-anchor', 'middle')
.style('fill', '#555')
.style('font-family', 'Arial')
.style('font-size', 15);
});
});
group
.append('text')
.attr('x', d => d.x)
.attr('y', d => d.y 50)
.text(d => d.name)
.style('text-anchor', 'middle')
.style('fill', '#555')
.style('font-family', 'Arial')
.style('font-size', 15);
const children = group.selectAll('.child-element');
}, [viewportDimension.width, viewportDimension.height]);`