Please see a video clip of the behavior here: https://imgur.com/a/Hs4iuC5
Each block is pushed into it's location by an X and Y force, and a collide force is used to pushed them apart while being dragged.
The odd behavior is that the collision gets offset away from the drag start position. The farther you move from the drag start position the greater the offset.
var size = 40;
var items = [{
cx: 50,
cy: 200,
size: size,
collideR: size * 0.5
},
{
cx: 100,
cy: 200,
size: size,
collideR: size * 0.5
},
{
cx: 600,
cy: 200,
size: size,
collideR: size * 0.5
},
{
cx: 750,
cy: 200,
size: size,
collideR: size * 0.5
}
];
var alphaTarget = 0.03;
var sim;
var nodes =
d3.select(".grid-svg")
.selectAll("rect")
.data(items)
.enter()
.append("rect")
.attr("class", "grid-item-block")
.attr('x', (d) => d.cx)
.attr('y', (d) => d.cy)
.attr("width", (d) => d.size)
.attr("height", (d) => d.size)
.attr("transform", (d) => `translate(-${d.size / 2}, -${d.size / 2})`)
.call(
d3
.drag()
.on("start", (event, d) => {
if (!event.active) sim.alphaTarget(alphaTarget).restart();
d.fx = d.x;
d.fy = d.y;
sim.force(
"collide",
d3.forceCollide().radius((dc) => dc.collideR)
);
})
.on("drag", (event, d) => {
d.fx = event.x;
d.fy = event.y;
})
.on("end", (event, d) => {
if (!event.active) sim.alphaTarget(alphaTarget);
d.fx = null;
d.fy = null;
sim.force("collide", null);
})
);
sim = d3
.forceSimulation()
.alphaDecay(0.2)
.alphaMin(0.005)
.force(
"x",
d3
.forceX()
.strength(3.0)
.x((d) => d.cx)
)
.force(
"y",
d3
.forceY()
.strength(3.0)
.y((d) => d.cy)
);
sim.nodes(items).on("tick", () => {
nodes.attr("x", (d) => d.x).attr("y", (d) => d.y);
});
.grid-item-block {
fill: #009900;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg class="grid-svg" width="800" height="300"></svg>
In the example, if you drag the left block near the other 3 blocks you'll notice that the effect on block #2 is roughly symmetric, block #3 doesn't start until you start to overlap the block and is stronger on the right of block #3, and an even larger offset difference with block #4
I've found the underlying reason: the farther you get from the point d.cx, d.cy, the more it pulls on the block to get back. The enormous pull on the block makes d3 think that there is no reason to move the block it's colliding with. After all, after one tick, the block you're dragging will no longer overlap, because it's pulled back so hard.
I fixed this by re-initialising the centering forces, and only give them any strength if it was not the currently dragged block. Otherwise, the strength is 0 and the force is effectively not applied.
var size = 20;
var width = 400;
var height = 200;
var items = [{
cx: 2 * size,
cy: height / 2,
size: size,
collideR: size
},
{
cx: width - 2 * size,
cy: height / 2,
size: size,
collideR: size
}
];
var alphaTarget = 0.03;
var sim = d3
.forceSimulation()
.alphaDecay(0.2)
.alphaMin(0.005);
var nodes =
d3.select(".grid-svg")
.selectAll("rect")
.data(items)
.enter()
.append("rect")
.attr("class", "grid-item-block")
.attr('x', (d) => d.cx)
.attr('y', (d) => d.cy)
.attr("width", (d) => d.size)
.attr("height", (d) => d.size)
//.attr("transform", (d) => `translate(-${d.size / 2}, -${d.size / 2})`)
.call(
d3
.drag()
.on("start", function(event, d) {
if (!event.active) sim.alphaTarget(alphaTarget).restart();
d.isDragging = true;
d.fx = d.x;
d.fy = d.y;
sim.force(
"collide",
d3.forceCollide().radius((dc) => dc.collideR)
);
setForces();
})
.on("drag", (event, d) => {
d.fx = event.x;
d.fy = event.y;
})
.on("end", (event, d) => {
if (!event.active) sim.alphaTarget(alphaTarget);
d.isDragging = false;
d.fx = null;
d.fy = null;
sim.force("collide", null);
setForces();
})
);
function setForces() {
sim
.force(
"x",
d3
.forceX()
.strength((d) => d.isDragging ? 0 : 3.0)
.x((d) => d.cx)
)
.force(
"y",
d3
.forceY()
.strength((d) => d.isDragging ? 0 : 3.0)
.y((d) => d.cy)
);
}
setForces();
sim.nodes(items).on("tick", () => {
nodes.attr("x", (d) => d.x).attr("y", (d) => d.y);
});
.grid-item-block {
fill: #009900;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.js"></script>
<svg class="grid-svg" width="400" height="200"></svg>
Related
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]);`
When having a marker on a globe, the marker lays flat on the surface.
Although there might be trouble the moment the marker rotates out of sight; is there a way to give this marker height?
Instead of a dot on the surface of the globe, I'm trying to get a dot on a needle, sticking out a little bit above the surface of the globe.
Not this:
-----o-----
But this:
o
_____|_____
Mimicking one of those:
Currently the marker is drawn as follows:
const width = 220;
const height = 220;
const config = {
speed: 0.025,
verticalTilt: 10,
horizontalTilt: -10
}
let locations = [];
const svg = d3.select('svg')
.attr('width', width).attr('height', height);
const markerGroup = svg.append('g');
const projection = d3.geoOrthographic();
const initialScale = projection.scale(99.5).translate([100, 100]);
const path = d3.geoPath().projection(projection);
const center = [width / 2, height / 2];
drawGlobe();
drawGraticule();
enableRotation();
const locationData = [
{"latitude": -33.8688, "longitude": 151.2093}
];
function drawGlobe() {
d3.queue()
.defer(d3.json, 'https://raw.githubusercontent.com/cszang/dendrobox/master/data/world-110m2.json')
.await((error, worldData) => {
svg.selectAll(".segment")
.data(topojson.feature(worldData, worldData.objects.countries).features)
.enter().append("path")
.attr("class", "segment")
.attr("d", path)
.style("stroke", "silver")
.style("stroke-width", "1px")
.style("fill", (d, i) => 'silver')
.style("opacity", ".5");
locations = locationData;
drawMarkers();
});
}
function drawGraticule() {
const graticule = d3.geoGraticule()
.step([10, 10]);
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path)
.style("fill", "#fff")
.style("stroke", "#ececec");
}
function enableRotation() {
d3.timer(function(elapsed) {
projection.rotate([config.speed * elapsed - 120, config.verticalTilt, config.horizontalTilt]);
svg.selectAll("path").attr("d", path);
drawMarkers();
});
}
function drawMarkers() {
const markers = markerGroup.selectAll('circle')
.data(locations);
markers
.enter()
.append('circle')
.merge(markers)
.attr('cx', d => projection([d.longitude, d.latitude])[0])
.attr('cy', d => projection([d.longitude, d.latitude])[1])
.attr('fill', d => {
const coordinate = [d.longitude, d.latitude];
gdistance = d3.geoDistance(coordinate, projection.invert(center));
return gdistance > 1.55 ? 'none' : 'tomato';
})
// 1.57
.attr('r', 3);
markerGroup.each(function() {
this.parentNode.appendChild(this);
});
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<svg></svg>
Using this answer as inspiration, you can create a second projection, equivalent to the first one, but with a larger scale value. That will project a point directly above the actual point on the globe, as if it was hanging above it. This allows you to draw a line from the ground up, and look at it from all angles. It even works with your hide marker logic.
const width = 220;
const height = 220;
const config = {
speed: 0.025,
verticalTilt: 10,
horizontalTilt: -10
}
let locations = [];
const svg = d3.select('svg')
.attr('width', width).attr('height', height);
const markerGroup = svg.append('g');
const projection = d3.geoOrthographic()
.scale(99.5)
.translate([100, 100]);
const markerProjection = d3.geoOrthographic()
.scale(108)
.translate(projection.translate());
const path = d3.geoPath().projection(projection);
const center = [width / 2, height / 2];
drawGlobe();
drawGraticule();
enableRotation();
const locationData = [
{"latitude": -33.8688, "longitude": 151.2093}
];
function drawGlobe() {
d3.queue()
.defer(d3.json, 'https://raw.githubusercontent.com/cszang/dendrobox/master/data/world-110m2.json')
.await((error, worldData) => {
svg.selectAll(".segment")
.data(topojson.feature(worldData, worldData.objects.countries).features)
.enter().append("path")
.attr("class", "segment")
.attr("d", path)
.style("stroke", "silver")
.style("stroke-width", "1px")
.style("fill", (d, i) => 'silver')
.style("opacity", ".5");
locations = locationData;
drawMarkers();
});
}
function drawGraticule() {
const graticule = d3.geoGraticule()
.step([10, 10]);
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path)
.style("fill", "#fff")
.style("stroke", "#ececec");
}
function enableRotation() {
d3.timer(function(elapsed) {
projection.rotate([config.speed * elapsed - 120, config.verticalTilt, config.horizontalTilt]);
markerProjection.rotate(projection.rotate());
svg.selectAll("path").attr("d", path);
drawMarkers();
});
}
function drawMarkers() {
const markers = markerGroup.selectAll('.marker')
.data(locations);
const newMarkers = markers
.enter()
.append('g')
.attr('class', 'marker')
newMarkers.append("line");
newMarkers.append("circle")
.attr("r", 3);
newMarkers.merge(markers)
.selectAll("line")
.attr("x1", d => projection([d.longitude, d.latitude])[0])
.attr("y1", d => projection([d.longitude, d.latitude])[1])
.attr("x2", d => markerProjection([d.longitude, d.latitude])[0])
.attr("y2", d => markerProjection([d.longitude, d.latitude])[1])
.attr('stroke', d => {
const coordinate = [d.longitude, d.latitude];
gdistance = d3.geoDistance(coordinate, markerProjection.invert(center));
return gdistance > (Math.PI / 2) ? 'none' : 'black';
})
newMarkers
.merge(markers)
.selectAll("circle")
.attr('cx', d => markerProjection([d.longitude, d.latitude])[0])
.attr('cy', d => markerProjection([d.longitude, d.latitude])[1])
.attr('fill', d => {
const coordinate = [d.longitude, d.latitude];
gdistance = d3.geoDistance(coordinate, markerProjection.invert(center));
return gdistance > (Math.PI / 2) ? 'none' : 'tomato';
})
markerGroup.each(function() {
this.parentNode.appendChild(this);
});
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<svg></svg>
I have a force layout graph where user adds nodes dynamically. How can i center all my nodes with a bit distance between, and make them move independently and not around the center.
I have tried removing the d3.forceCenter(width / 2, height / 2) which makes nodes move independently but then it possitions all nodes at (0, 0).
simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody().strength(0))
.force('center', d3.forceCenter(width / 2, height / 2));
I want all the nodes to be centered and move independently.
EDIT:
I tried setting cx and cy values but that did not work either.
const nodeEnter = nodeElements
.enter()
.append('circle')
.attr('r', 20)
.attr('fill', 'orange')
.attr('cx', (d, i) => {
return (width / 2) + i * 10;
})
.attr('cy', (d, i) => {
return (height / 2) + i * 10;
})
.call(dragDrop(simulation))
.on('click', ({ id }) => handleClick(id));
Given what you said in your comment...
If i move 1 node then all other nodes move relatively in order to keep the center of mass at the same spot.
... you already know that forceCenter is the wrong tool for the task, since it will keep the centre of mass.
Therefore, just replace it for forceX and forceY:
const simulation = d3.forceSimulation()
.force('centerX', d3.forceX(width / 2))
.force('centerY', d3.forceY(height / 2));
Since you didn't provide enough code here is a general demo:
svg {
background-color: wheat;
}
<svg width="400" height="300"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>
const svg = d3.select('svg');
const width = svg.attr('width');
const height = svg.attr('height');
const data = d3.range(50).map(() => ({}));
const node = svg.selectAll()
.data(data)
.enter()
.append('circle')
.attr('r', 10)
.attr('fill', 'teal')
.attr('stroke', 'black')
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
const simulation = d3.forceSimulation()
.force('charge', d3.forceManyBody().strength(-15))
.force('centerX', d3.forceX(width / 2))
.force('centerY', d3.forceY(height / 2));
simulation
.nodes(data)
.on('tick', ticked);
function ticked() {
node.attr('cx', d => d.x)
.attr('cy', d => d.y);
}
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;
}
</script>
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.
In the example below, I'm trying to animate new items appearance.
As you can see, they animate from the bottom of the chart to their position.
However, existing items ("second" in this example) jump, instead of smoothly transitioning to their new position.
I thought it is because the new band suddenly appears, without a transition. So, I tried to add a transition:
const band = bandUpdate.enter()
.append('g')
.attr('class', 'band')
.merge(bandUpdate)
.transition(t)
.attr('transform', (_, i) => `translate(0, ${i * bandHeight})`);
But, I'm getting:
Uncaught TypeError: band.selectAll(...).data is not a function
Could you explain the error please, and suggest a way to avoid the undesired jump?
Bonus: How could I animate the y axis labels?
Playground
const width = 300;
const height = 200;
const margin = { top: 30, right: 30, bottom: 30, left: 50 };
let data = {};
const main = d3.select('.chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
const xScale = d3.scaleLinear().domain([0, 16]).range([0, width]);
const xAxis = d3.axisBottom(xScale);
main.append('g')
.attr('transform', `translate(0, ${height})`)
.call(xAxis);
const yScale = d3.scaleBand().domain([]).range([0, height]);
const yAxis = d3.axisLeft(yScale);
const yAxisG = main.append('g').call(yAxis);
const bandG = main.append('g');
function update() {
const t = d3.transition().duration(500);
const ids = Object.keys(data);
yScale.domain(ids);
yAxisG.call(yAxis);
const bandHeight = yScale.bandwidth();
const bandUpdate = bandG.selectAll('.band').data(ids, id => id);
const band = bandUpdate.enter()
.append('g')
.attr('class', 'band')
.merge(bandUpdate)
// .transition(t) // Throws: Uncaught TypeError: band.selectAll(...).data is not a function
.attr('transform', (_, i) => `translate(0, ${i * bandHeight})`);
bandUpdate.exit().remove();
const itemUpdate = band.selectAll('.item')
.data(id => data[id], item => item.value);
const itemG = itemUpdate.enter().append('g').attr('class', 'item');
const rectHeight = 4;
itemG
.append('rect')
.attr('class', (_, i) => `item-${i}`)
.attr('x', d => xScale(d.value))
.attr('width', d => width - xScale(d.value))
.attr('height', rectHeight)
.attr('y', height)
.transition(t)
.attr('y', bandHeight / 2 - rectHeight / 2);
itemG
.append('circle')
.attr('class', (_, i) => `item-${i}`)
.attr('cx', d => xScale(d.value))
.attr('r', 6)
.attr('cy', height)
.transition(t)
.attr('cy', bandHeight / 2);
itemUpdate
.select('rect')
.attr('x', d => xScale(d.value))
.attr('width', d => width - xScale(d.value))
.transition(t)
.attr('y', bandHeight / 2 - rectHeight / 2);
itemUpdate
.select('circle')
.attr('cx', d => xScale(d.value))
.transition(t)
.attr('cy', bandHeight / 2);
itemUpdate.exit().remove();
}
update();
setTimeout(() => {
data['first'] = [
{
value: 7
},
{
value: 10
}
];
update();
}, 1000);
setTimeout(() => {
data['second'] = [
{
value: 1
}
];
update();
}, 2000);
setTimeout(() => {
data['third'] = [
{
value: 13
}
];
update();
}, 3000);
svg {
margin: 0 30px 30px 30px;
}
.item-0 {
fill: red;
}
.item-1 {
fill: green;
}
<div class="chart"></div>
<script src="https://unpkg.com/d3#4.4.1/build/d3.js"></script>
Just break your band constant:
const band = bandUpdate.enter()
.append('g')
.attr('class', 'band')
.merge(bandUpdate);
band.transition(t)
.attr('transform', (_, i) => `translate(0, ${i * bandHeight})`);
Here is the updated CodePen: http://codepen.io/anon/pen/oBWJdp?editors=0010
Explanation:
According to the documentation, selection.transition([name]):
Returns a new transition on the given selection with the specified name.
So, when you later in the code do this:
const itemUpdate = band.selectAll('.item')
.data(id => data[id], item => item.value);
You're selecting a new transition, and that's giving you the error (you cannot bind data to a transition).
Breaking the band constant makes itemUpdate a selection based in the band selection, not in the following transition.