How can I expand child nodes when a node is clicked in d3? - d3.js

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>
);
}

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]);`

Related

Having trouble with D3v4 update pattern with 3 levels of nested data

I have data nested into 3 levels, which I need to dynamically update. The kicker is that the elements for the mid-level need to actually display on TOP of the elements for the low-level due to some hover behavior I need, so I'm having trouble with what the enter/update/exit/merge pattern should look like. (There don't need to be any elements displayed for the high-level).
The code I have right now updates the data successfully but is not rendering the rectangles at all, instead giving me an error, Uncaught TypeError: this.setAttribute is not a function.
How do I fix this problem, please?
Here's what it should look like before updating:
And here's what it should look like after updating:
Here's a CodePen with the code Below
```
let width = 0.9 * window.innerWidth,
height = 0.9 * window.innerHeight,
colors = ['darkviolet', 'steelblue', 'coral', 'Turquoise', 'firebrick', 'mediumslateblue', 'palevioletred', 'green', 'aqua'];
let data1 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"1","item":"2"},
{"group":"A","segment":"1","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"3","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"4","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"6","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"7","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"9","item":"8"},
{"group":"C","segment":"9","item":"9"}],
data2 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"8","item":"2"},
{"group":"A","segment":"9","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"5","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"7","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"5","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"3","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"6","item":"8"},
{"group":"C","segment":"1","item":"9"}];
let button = d3.select('body')
.append('button')
.attr('type', 'button')
.style('display', 'block')
.text('Update')
.on('click', function() { update(data2) });
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
let color = d3.scaleOrdinal().range(colors);
update(data1);
function getxy(data) {
let grouped = Array.from(d3.group(data, d=> d.group, d=> d.segment), ([key, value]) => ({key, value}));
grouped.forEach(function(s) {
s.value = Array.from(s.value, ([key, value]) => ({key, value}));
s.value.forEach(function(d) {
d.start = d3.min(d.value, function(t) { t.segment = +t.segment; t.item = +t.item; return +t.item });
d.end = d3.max(d.value, function(t) { return t.item });
d.key = +d.key;
d.group = s.key;
})
})
let x1 = d3.scaleBand()
.domain([1, 2, 3, 4, 5, 6, 7, 8, 9])
.range([width*0.05, width])
.padding(0.0);
let y1 = d3.scaleBand()
.domain(['A', 'B', 'C'])
.range([10, height])
.padding(0.1);
return [x1, y1, grouped];
}
function update(data) {
let xy = getxy(data);
let x = xy[0], y = xy[1], groupedData = xy[2];
let barsAll = svg
.selectAll('.bars')
.data(groupedData);
barsAll.exit().remove();
let barsEnter = barsAll
.enter()
.append('g')
.attr('class', 'bars');
barsEnter = barsEnter.merge(barsAll);
let segmentsAll = barsEnter
.selectAll('.segments')
.data(function(d) { return d.value });
segmentsAll.exit().remove();
let segmentsEnter = segmentsAll.enter();
let bitsAll = segmentsEnter
.selectAll('.bits')
.data(function(d) { return d.value });
bitsAll.exit().remove();
let bitsEnter = bitsAll
.enter()
.append('circle')
.attr('class', 'bits')
.attr('r', width*0.05)
.attr('stroke', 'none');
bitsEnter = bitsEnter.merge(bitsAll);
bitsEnter
.attr('cx', function(d) { return x(d.item) })
.attr('cy', function(d) { return y(d.group) + y.bandwidth()/2 })
.attr('fill', function(d) { return color(d.segment) });
segmentsEnter.append('rect')
.attr('stroke', 'black')
.attr('class', 'segments')
.style('fill-opacity', 0.2);
segmentsEnter = segmentsEnter.merge(segmentsAll);
segmentsEnter
.attr('fill', function(d) { return color(d.key) })
.attr('height', y.bandwidth()*0.75)
.attr('x', function(d) { return x(d.start) - width*0.05 })
.attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
.attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
}
```
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-array.v2.min.js"></script>
Well, by going around the “merge” step on the mid-level segments and low-level bits (but not on the top-level bars), I was able to hack a fix, finally. Working pen
Still open to help from others because I feel like I should really get the hang of the whole flow - update, enter, exit, merge - at some point.
let width = 0.9 * window.innerWidth,
height = 0.9 * window.innerHeight,
colors = ['darkviolet', 'steelblue', 'coral', 'Turquoise', 'firebrick', 'mediumslateblue', 'palevioletred', 'green', 'aqua'];
let data1 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"1","item":"2"},
{"group":"A","segment":"1","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"3","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"4","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"6","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"7","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"9","item":"8"},
{"group":"C","segment":"9","item":"9"}],
data2 =
[{"group":"A","segment":"1","item":"1"},
{"group":"A","segment":"8","item":"2"},
{"group":"A","segment":"9","item":"3"},
{"group":"A","segment":"2","item":"4"},
{"group":"A","segment":"2","item":"5"},
{"group":"A","segment":"2","item":"6"},
{"group":"A","segment":"5","item":"7"},
{"group":"A","segment":"3","item":"8"},
{"group":"A","segment":"3","item":"9"},
{"group":"B","segment":"4","item":"1"},
{"group":"B","segment":"4","item":"2"},
{"group":"B","segment":"7","item":"3"},
{"group":"B","segment":"5","item":"4"},
{"group":"B","segment":"5","item":"5"},
{"group":"B","segment":"5","item":"6"},
{"group":"B","segment":"5","item":"7"},
{"group":"B","segment":"6","item":"8"},
{"group":"B","segment":"6","item":"9"},
{"group":"C","segment":"7","item":"1"},
{"group":"C","segment":"7","item":"2"},
{"group":"C","segment":"3","item":"3"},
{"group":"C","segment":"8","item":"4"},
{"group":"C","segment":"8","item":"5"},
{"group":"C","segment":"8","item":"6"},
{"group":"C","segment":"9","item":"7"},
{"group":"C","segment":"6","item":"8"},
{"group":"C","segment":"1","item":"9"}];
let button = d3.select('body')
.append('button')
.attr('type', 'button')
.style('display', 'block')
.text('Update')
.on('click', function() { update(data2) });
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
let color = d3.scaleOrdinal().range(colors);
function getxy(data) {
let grouped = Array.from(d3.group(data, d=> d.group, d=> d.segment), ([key, value]) => ({key, value}));
grouped.forEach(function(s) {
s.value = Array.from(s.value, ([key, value]) => ({key, value}));
s.value.forEach(function(d) {
d.start = d3.min(d.value, function(t) { t.segment = +t.segment; t.item = +t.item; return +t.item });
d.end = d3.max(d.value, function(t) { return t.item });
d.key = +d.key;
d.group = s.key;
})
})
let x1 = d3.scaleBand()
.domain([1, 2, 3, 4, 5, 6, 7, 8, 9])
.range([width*0.05, width])
.padding(0.0);
let y1 = d3.scaleBand()
.domain(['A', 'B', 'C'])
.range([10, height])
.padding(0.1);
return [x1, y1, grouped];
}
function update(data) {
let xy = getxy(data);
let x = xy[0], y = xy[1], groupedData = xy[2];
// update
let barsAll = svg
.selectAll('.bars')
.data(groupedData);
// exit
barsAll.exit().remove();
// enter
let barsEnter = barsAll
.enter();
barsEnter = barsEnter.merge(barsAll).append('g');
barsEnter.selectAll('.segments').remove();
d3.selectAll('.segments').remove();
let segmentsAll = barsEnter
.selectAll('.segments')
.data(function(d) { return d.value });
segmentsAll.exit().remove();
let segmentsEnter = segmentsAll
.enter();
let bitsAll = segmentsEnter
.selectAll('.bits')
.data(function(d) { return d.value });
bitsAll.exit().remove();
bitsAll
.enter()
.append('circle')
.attr('class', 'bits')
.attr('r', width*0.05)
.attr('stroke', 'none')
.attr('cx', function(d) { return x(d.item) })
.attr('cy', function(d) { return y(d.group) + y.bandwidth()/2 })
.attr('fill', function(d) { return color(d.segment) });
// bitsEnter = bitsEnter.merge(bitsAll);
bitsAll
.attr('cx', function(d) { return x(d.item) })
.attr('cy', function(d) { return y(d.group) + y.bandwidth()/2 })
.attr('fill', function(d) { return color(d.segment) });
segmentsEnter
.append('rect')
.attr('class', 'segments')
.attr('stroke', 'black')
.style('fill-opacity', 0.2)
.attr('fill', function(d) { return color(d.key) })
.attr('height', function() { return y.bandwidth()*0.75 })
.attr('x', function(d) { return x(d.start) - width*0.05 })
.attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
.attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
segmentsAll
.attr('fill', function(d) { return color(d.key) })
.attr('height', function() { return y.bandwidth()*0.75 })
.attr('x', function(d) { return x(d.start) - width*0.05 })
.attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
.attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
//segmentsAll = segmentsEnter.merge(segmentsAll);
// segmentsEnter
// .attr('fill', function(d) { return color(d.key) })
// .attr('height', function() { return y.bandwidth()*0.75 })
// .attr('x', function(d) { return x(d.start) - width*0.05 })
// .attr('y', function(d) { return y(d.group) + y.bandwidth()*0.125 })
// .attr('width', function(d) { return x(d.end) - x(d.start) + width*0.1 });
}
update(data1);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.js"></script>
<script src="https://d3js.org/d3-array.v2.min.js"></script>

On drag, force is applied to node before mouse click is released

I've been implementing a force layout in d3js and I can zoom and drag the layout / nodes.
When I'm dragging a node, the behavior is fine, except that when I stopped moving, the node instantly starts moving back to it's original position, even though I haven't released the mouse click and so the event should still be considered in progress.
I tried to fix the behavior by setting d.fx and d.fy to current values, which doesn't solve anything.
Here's the code:
const WIDTH = 1600;
const HEIGHT = 900;
const V_MARGIN = 20;
const H_MARGIN = 50;
const ALPHA_DECAY = 0.03;
const VELOCITY_DECAY = 0.6;
const LINK_DISTANCE = Math.min(WIDTH, HEIGHT) / 10;
const CHARGE_FORCE = -Math.min(WIDTH, HEIGHT) / 3;
const ITERATIONS = 16;
const CIRCLE_WIDTH = 3;
const ON_HOVER_OPACITY = 0.1;
const c10 = d3.scaleOrdinal(d3.schemeCategory10);
const SVG = d3.select('body')
.append('svg')
.attr('width', WIDTH)
.attr('height', HEIGHT)
;
const g = SVG.append('g')
.attr('class', 'everything')
;
d3.json('got_social_graph.json')
.then(data => {
const nodes = data.nodes;
const links = data.links;
//Create force layout
const force = d3.forceSimulation()
.nodes(nodes)
.alphaDecay(ALPHA_DECAY)
.velocityDecay(VELOCITY_DECAY)
.force('links', d3.forceLink(links).distance(LINK_DISTANCE))
.force("collide",d3.forceCollide(d => d.influence > 15 ? d.influence : 15).iterations(ITERATIONS))
.force('charge_force', d3.forceManyBody().strength(CHARGE_FORCE))
.force('center_force', d3.forceCenter((WIDTH - H_MARGIN) / 2, (HEIGHT - V_MARGIN) / 2))
.force("y", d3.forceY(0))
.force("x", d3.forceX(0))
;
//Create links
const link = g.append('g')
.attr('class', 'link')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke-width', d => d.weight / 10)
;
//Create nodes elements
const node = g.append('g')
.attr('class', 'node')
.selectAll('circle')
.data(nodes)
.enter()
.append('g')
;
//Append circles to nodes
const circle = node.append('circle')
.attr('r', d => d.influence > 10 ? d.influence : 10)
.attr('stroke-width', CIRCLE_WIDTH)
.attr('stroke', d => c10(d.zone * 10))
.attr('fill', 'black')
;
/*const label = node.append('text')
.attr('x', 12)
.attr('y', '0.25em')
.attr('font-size', d => d.influence * 1.5 > 9 ? d.influence * 1.5 : 9)
.text(d => d.character)
;*/
//Refresh force layout data
force.on('tick', () => {
node.attr('cx', d => d.x = Math.max(H_MARGIN, Math.min(WIDTH - H_MARGIN, d.x - H_MARGIN)))
.attr('cy', d => d.y = Math.max(V_MARGIN, Math.min(HEIGHT - V_MARGIN, d.y - V_MARGIN)))
.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
link.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y)
;
});
//Handle mouse events
circle
.on('click', (d, k, n) => {
d3.select(n[k])
})
.on('mouseover', (d, k, n) => {
})
.on('mouseout', (d, k, n) => {
circle
.attr('opacity', 1)
})
;
//Handle translation events
node.call(d3.drag()
.on('start', (d, n, k) => {
if (!d3.event.active) force.alphaTarget(0.3).restart();
})
.on('drag', (d, n, k) => {
d3.select(n[k])
.attr('cx', d.x = d3.event.x)
.attr('cy', d.y = d3.event.y)
})
.on('end', (d, n, k) => {
if (!d3.event.active) force.alphaTarget(0);
}))
;
//Handle zoom events
g.call(d3.zoom()
.scaleExtent([0.8, 2.5])
.on('start', () => {
if (!d3.event.active) force.alphaTarget(0.3).restart();
d3.event.sourceEvent.stopPropagation();
})
.on('zoom', () => {
if (!d3.event.active) force.alphaTarget(0.3).restart();
g.attr('transform', d3.event.transform)
})
.on('end', () => {
if (!d3.event.active) force.alphaTarget(0);
})
);
});
I want the node to stay over my mouse cursor until the click is released. The node doesn't need to be sticky.

Bar chart unable to execute transition after button clicked

I am drawing a simple bar chart with two buttons to toggle the chart based on two datasets.
However, the chart does not do the transition when I click on the buttons.
The data is loaded from a csv file.
party,ge14,latest
PKR,47,50
DAP,42,42
Umno,54,38
GPS,0,19
PAS,18,18
Bersatu,13,16
Independent,3,12
Amanah,11,11
Warisan,8,9
GBS,0,3
Other BN parties,25,2
Upko,0,1
Source code
d3.csv('data/seatcount.csv')
.then(data => {
const width = 900,
height = 700,
margin = 25;
const svg = d3.select('#bar')
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('viewBox', '0 0 ' + Math.min(width, height) + ' ' + Math.min(width, height))
.attr('preserveAspectRatio', 'xMinYMin')
.append('g');
// define scale
const xScale = d3.scaleBand()
.domain(data.map(d => d.party))
.range([margin, (width - margin)])
.padding(.2);
const yScale = d3.scaleLinear()
.domain([0, 60])
.range([(height - margin), margin]);
// define axes
const xAxis = d3.axisBottom(xScale)
.ticks(12)
const yAxis = d3.axisLeft(yScale)
.ticks(6);
svg.append('g')
.attr('transform', `translate(0, ${height - margin})`)
.call(xAxis)
.style('font-size', '.7em');
svg.append('g')
.attr('transform', `translate(${margin}, 0)`)
.call(yAxis)
.style('font-size', '.7em');
// plot columns
let cols = svg.selectAll('.col')
.data(data)
.enter();
cols.append('rect')
.attr('x', (d) => xScale(d.party))
.attr('y', (height - margin))
.attr('width', xScale.bandwidth())
.attr('height', 0)
.style('fill', '#dddddd')
.transition()
.delay((d, i) => 100 * i)
.attr('y', (d) => {
return yScale(d.latest)
})
.attr('height', (d) => (height - margin - yScale(d.latest)));
// label the bars
cols.append('text')
.attr('x', d => {
return (xScale(d.party) + xScale.bandwidth() / 2);
})
.attr('y', d => {
return (yScale(d.latest) + 13);
})
.style('fill', '#333333')
.attr('text-anchor', 'middle')
.text(d => d.latest)
.style('font-size', '.8em')
.style('visibility', 'hidden')
.transition()
.delay((d, i) => 100 * i)
.style('visibility', 'visible');
function moveCols(data, period) {
cols.data(data)
.transition()
.attr('x', function(d) {
return xScale(d.party);
})
.attr('y', function(d) {
console.log(yScale(d[period]));
return yScale(d[period]);
})
.attr('width', xScale.bandwidth())
.attr('height', function(d) {
return (height - margin - yScale(d[period]));
});
cols.selectAll('text')
.remove();
cols.append('text')
.attr('x', d => {
return (xScale(d.party) + xScale.bandwidth() / 2);
})
.attr('y', d => {
return (yScale(d[period]) + 13);
})
.style('fill', '#333333')
.attr('text-anchor', 'middle')
.text(d => d[period])
.style('font-size', '.8em')
.style('visibility', (d, i) => {
// console.log(d[period]);
if (d[period] <= 0) {
return 'hidden';
} else {
return 'visible';
}
});
}
d3.select('#latest').on('click', () => {
moveCols(data, 'latest')
});
d3.select('#ge14').on('click', () => {
moveCols(data, 'ge14')
})
});
In the end, once I have clicked on #latest or #ge14, only the label of the bars changed, but not the bars themselves.
And there are errors showed in the console.
Uncaught TypeError: this.getAttribute is not a function
at ot.<anonymous> (d3.v5.min.js:2)
at ot.e (d3.v5.min.js:2)
at o (d3.v5.min.js:2)
at d3.v5.min.js:2
at fr (d3.v5.min.js:2)
at cr (d3.v5.min.js:2)
I have reformatted my code and it works! I got no complaint.
const margin = { top: 20, right: 20, bottom: 50, left: 40 },
width = 900
height = 700
const svg = d3.select('#bar')
.append('svg')
.attr('width', width)
.attr('height', height)
.attr('viewBox', '0 0 ' + Math.min(width, height) + ' ' + Math.min(width, height))
.attr('preserveAspectRatio', 'xMinYMin');
const x = d3.scaleBand()
.rangeRound([0, (width - margin.left - margin.right)])
.padding(0.1);
const y = d3.scaleLinear()
.rangeRound([(height - margin.bottom), 0]);
const g = svg.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
d3.csv('data/seatcount.csv')
.then(data => {
data.forEach(d => {
d.party = d.party;
d.ge14 = +d.ge14;
d.latest = +d.latest;
});
x.domain(data.map(d => d.party));
y.domain([0, d3.max(data, d => d.latest)]);
let duration = 1000;
// define axes
g.append('g')
.attr('class', 'axis, x-axis')
.attr('transform', `translate(0, ${height - margin.bottom})`)
.call(d3.axisBottom(x));
g.append('g')
.attr('class', 'axis, y-axis')
.call(d3.axisLeft(y));
// plot columns
g.selectAll('.col')
.data(data)
.enter()
.append('rect')
.attr('x', d => x(d.party))
.attr('y', height - margin.bottom)
.attr('width', x.bandwidth())
.attr('height', 0)
.attr('class', 'col')
.transition()
.duration(duration)
.attr('y', d => y(d.latest))
.attr('height', d => (height - margin.bottom - y(d.latest)))
.ease(d3.easeBounce);
g.selectAll('.label')
.data(data)
.enter()
.append('text')
.attr('class', 'label')
.attr('x', d => {
return (x(d.party) + x.bandwidth() / 2);
})
.attr('y', (d, i) => {
if (d.latest <= 5) {
return (y(d.latest) - 5);
} else {
return (y(d.latest) + 13);
}
})
.text(d => d.latest)
.style('font-size', '.8em')
.style('visibility', 'hidden')
.transition()
.delay(duration)
.style('visibility', 'visible');
function moveCols(data, period) {
y.domain([0, d3.max(data, d => d[period])]);
g.select('.y-axis')
.transition()
.call(d3.axisLeft(y));
g.selectAll('.label')
.remove();
g.selectAll('.col')
.data(data)
.transition()
.attr('x', d => x(d.party))
.attr('y', d => y(d[period]))
.attr('width', x.bandwidth())
.attr('height', d => (height - margin.bottom - y(d[period])))
.ease(d3.easeBounce);
g.selectAll('.label')
.data(data)
.enter()
.append('text')
.attr('class', 'label')
.attr('x', d => {
return (x(d.party) + x.bandwidth() / 2);
})
.attr('y', (d, i) => {
if (d[period] <= 5) {
return (y(d[period]) - 5);
} else {
return (y(d[period]) + 13);
}
})
.style('fill', '#333333')
.attr('text-anchor', 'middle')
.text(d => d[period])
.style('font-size', '.8em');
}
d3.select('#latest').on('click', () => {
moveCols(data, 'latest');
});
d3.select('#ge14').on('click', () => {
moveCols(data, 'ge14');
})
});

Stop the movement of nodes in force layout d3.js

I want to display the nodes and relationship between them from json file.
I am using force layout d3.js but there is alot of movement of nodes when the page is opened.
I want to stop these movements and also want to stop the other node movements when one node is dragged.
Code : http://jsfiddle.net/bhgq017u/13/
var colors = d3.scaleOrdinal(d3.schemeCategory10);
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
node,
link;
svg.append('defs').append('marker')
.attrs({'id':'arrowhead',
'viewBox':'-0 -5 10 10',
'refX':13,
'refY':0,
'orient':'auto',
'markerWidth':8,
'markerHeight':8,
'xoverflow':'visible'})
.append('svg:path')
.attr('d', 'M 0,-5 L 10 ,0 L 0,5')
.attr('fill', "gray");
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function (d) {return d.id;}).distance(500).strength(1))
.force("charge", d3.forceManyBody())
.force("center", d3.forceCenter(width / 2, height / 2));
d3.json('https://api.myjson.com/bins/beqb0', function (error, graph) {
if (error) throw error;
update(graph.links, graph.nodes);
})
function update(links, nodes) {
link = svg.selectAll(".link")
.data(links)
.enter()
.append("line")
.attr("class", "link")
.attr('marker-end','url(#arrowhead)')
link.append("title")
.text(function (d) {return d.type;});
edgepaths = svg.selectAll(".edgepath")
.data(links)
.enter()
.append('path')
.attrs({
'class': 'edgepath',
'fill-opacity': 0,
'stroke-opacity': 0,
'id': function (d, i) {return 'edgepath' + i}
})
.style("pointer-events", "none");
edgelabels = svg.selectAll(".edgelabel")
.data(links)
.enter()
.append('text')
.style("pointer-events", "none")
.attrs({
'class': 'edgelabel',
'id': function (d, i) {return 'edgelabel' + i},
'font-size': 10,
'fill': 'black'
});
edgelabels.append('textPath')
.attr('xlink:href', function (d, i) {return '#edgepath' + i})
.style("text-anchor", "middle")
.style("pointer-events", "none")
.attr("startOffset", "50%")
.text(function (d) {return d.type});
node = svg.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", "node")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
// .on("end", dragended)
);
node.append("circle")
.attr("r", 8)
.style("fill", function (d, i) {return colors(i);})
node.append("text")
.attr("dy", -10)
.text(function (d) {return d.name});
simulation
.nodes(nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
}
function ticked() {
link
.attr("x1", function (d) {return d.source.x;})
.attr("y1", function (d) {return d.source.y;})
.attr("x2", function (d) {return d.target.x;})
.attr("y2", function (d) {return d.target.y;});
node
.attr("transform", function (d) {return "translate(" + d.x + ", " + d.y + ")";});
edgepaths.attr('d', function (d) {
return 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
});
edgelabels.attr('transform', function (d) {
if (d.target.x < d.source.x) {
var bbox = this.getBBox();
rx = bbox.x + bbox.width / 2;
ry = bbox.y + bbox.height / 2;
return 'rotate(180 ' + rx + ' ' + ry + ')';
}
else {
return 'rotate(0)';
}
});
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart()
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
In the dragend you must set the alphaTraget to 0.
Remove the forces on dragstart
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
simulation.force("link", null).force("charge", null).force("center", null);
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
}
Edit
If you also want to restrict at startup use
function update(links, nodes) {
// ...
simulation
.nodes(nodes)
.on("tick", ticked)
.stop();
simulation.force("link")
.links(links);
d3.range(100).forEach( () => simulation.tick() );
ticked();
simulation.force("link", null).force("charge", null).force("center", null);
simulation.start();
}

How to append images as nodes in D3 force layout graph?

So I'm working on freecodecamp's D3 force layout challenge : https://www.freecodecamp.com/challenges/show-national-contiguity-with-a-force-directed-graph
And as part of the challenge, I'm trying to append images of flags as the nodes in a force layout.
I've managed to append the flags and they are showing. When you click and drag on them, the links also move too. The problem is that they are stuck in the same position.
This is what I mean:
javascript (it's made within React):
createForceGraph() {
const { nodes, links } = this.state;
console.log(nodes);
console.log(links);
const w = 800;
const h = 500;
const margin = {
top: 30,
right: 30,
bottom: 80,
left: 80
};
const svg = d3.select('.chart')
.append('svg')
.attr('width', w)
.attr('height', h);
const simulation = d3.forceSimulation()
.force('link', d3.forceLink().id(function(d, i) { return i }).distance(1))
.force('charge', d3.forceManyBody().strength(1))
.force('center', d3.forceCenter(w / 2, h / 2))
.force('collision', d3.forceCollide(12));
const link = svg.append('g')
.attr('class', 'links')
.selectAll('line')
.data(links)
.enter()
.append('line')
.attr('stroke', 'black');
const node = d3.select('.nodes')
.selectAll('img')
.data(nodes)
.enter()
.append('img')
.attr('class', d => {
return `flag flag-${d.code}`;
})
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
simulation.nodes(nodes)
.on('tick', ticked);
simulation.force('link')
.links(links);
function ticked() {
link
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node
.style("left", function(d) { return d.x + 'px'; })
.style("top", function(d) { return d.y + 'px'; });
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
HTML:
<div>D3 Force-Directed Layout
<div className='chart'>
<div className='nodes'></div>
</div>
</div>
Maybe this can help you solve your problem?
https://bl.ocks.org/mbostock/950642

Resources