D3: .Transition() Not Working with Events

D3: .transition() not working with events

The problem here is a confusion between two different methods that use the same name:

  • selection.on(typenames[, listener])
  • transition.on(typenames[, listener])

Since you have a transition selection, when you write...

.on(...

The method expects to see three things (or typenames):

  • "start"
  • "end"
  • "interrupt"

However, "mouseover", "mouseout" or "click" are none of them. And then you get your error...

> Uncaught Error: unknown type: mouseover

Solution:

Bind the event listeners to a regular selection. Then, after that, create your transition selection.

Therefore, in your case, bind all the on listeners to the "enter" selection (which, by the way, makes more sense), removing them from the update selection.

Have a look at this little demo. First, I create a regular, enter selection, to which I add the event listener:

var bars = svg.selectAll(null)
.data(data)
.enter()
.append("rect")
//some attr here
.on("mouseover", function(d) {
console.log(d)
});

Then, I add the transition:

bars.transition()
.duration(1000)
etc...

Hover over the bars to see the "mouseover" working:

var svg = d3.select("svg");var data = [30, 280, 40, 140, 210, 110];var bars = svg.selectAll(null)  .data(data)  .enter()  .append("rect")  .attr("x", 0)  .attr("width", 0)  .attr("y", function(d, i) {    return i * 20  })  .attr("height", 18)  .attr("fill", "teal")  .on("mouseover", function(d) {    console.log(d)  });
bars.transition() .duration(1000) .attr("width", function(d) { return d })
.as-console-wrapper { max-height: 25% !important;}
<script src="https://d3js.org/d3.v4.min.js"></script><svg></svg>

d3.event not working when combining modules

import {event as d3event} from'd3-selection'
d3event === null // true; `event`'s value changes later with user input.
let d3 = Object.assign({}, d3selection, d3transition, d3event)
// won't have an `event` property since Object.assign({}, null) -> {}

Adding d3.event = event // or d3 = {event} will still return the null event observed at assignment time; see Mike Bostock's method of getting the current event for a solution.

The d3.event note now contains a link to an explanation of what a live binding is: a reference to a variable within d3-selection.

An additional caveat which cost me some time: if d3-drag is pointing to a different version of d3-select than your other tools, d3.select(/**/).call(d3.drag(/**/)) won't work, since events never reach the event variable that d3.drag is using. Somewhere around d3@^5, the main package no longer pins the d3-selection version, allowing this sort of mismatch.

D3 transition end events not firing correctly?

In the version in your jsfiddle, you only need to clear the timeout properly to make it work. The function setTimeout returns a reference that you need to pass to window.clearTimeout, i.e.

var t = null;
...
window.clearTimeout(t);
...
t = setTimeout(...);

Working fiddle here.

D3 transitions not working

You just have to correct one line of code:

instead of

 var players = canvas
.selectAll(".player")
.data(data, function(d){
// always create a unique key for your data-binding
return d.name + d.saves + d.total;
});

you write:

 var players = canvas
.selectAll(".player")
.data(data);

Now it works.
The problem was the following: The DOM elements you create in the enter selection are not valid SVG elements since at least one attribute was missing (E.g. the width is missing for the rect). As a result the transition created the object (from an underspecified SVG object).

So now you have transitions for all but the initial display because of the reason described above.

Here's the working JSFiddle:
https://jsfiddle.net/ee2todev/xjf0k2oy/

Single on event for multiple transitions

D3-transitions can be a bit tricky because the end event triggers at the end of each element's transition. There is no transition.on("endAll", ..., so since you don't want to trigger the repeat function twice at the end of the transition you are using an empty transition to delay calling repeat.

You could use a counter to see when the last transition is done, but this too is less clean. Ultimately, you will get less elegant code when managing multiple events with transitions that act on elements individually.

Instead, since each element's timer and transition events for a d3-transition are specific to that element (even if you set them as a group, each element has it's own timer and events), create a repeat function that is specific to each element:

function scroll(d) {
d.push(Math.random());

// Transition
d3.select(this).attr("transform",null)
.attr("d",line)
.transition()
.attr("transform", "translate(" + x(0) + ")")
.duration(750)
.ease(d3.easeLinear)
.on("end",scroll); // repeat

d.shift();
}

And call with selection.each(scroll); Now we are managing each transition (which handles each item in a selection separately) with a function that handles one item in the selection at a time.

This allows you to:

  • drop the empty transition,
  • access d directly if you call this function with .each() (instead of using paths.each(... twice in the function),

Other considerations:

  • you can apply this function to any group of paths you have a selection for without modifying the function itself.

  • the element being transitioned is this

Here it is at work:

let n = 40;let random = d3.randomUniform(-1, 1);
let data = [d3.range(n).map(random), d3.range(n).map(random)];
let margin = {top: 6, right: 0, bottom: 6, left: 40};let width = 960 - margin.right;let height = 120 - margin.top - margin.bottom;
let x = d3.scaleLinear() .domain([1, n - 2]) .range([0, width]);
let y = d3.scaleLinear() .domain([-1, 1]) .range([height, 0]);
let line = d3.line() .x(function(d, i) { return x(i); }) .y(function(d, i) { return y(d); }) .curve(d3.curveBasis);
let svg = d3.select("body").append("p").append("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .style("margin-left", -margin.left + "px") .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("defs").append("clipPath") .attr("id", "clip") .append("rect") .attr("width", width) .attr("height", height);
svg.append("g") .attr("class", "y axis") .call(d3.axisLeft(y).ticks(5));
let paths = svg.append('g') .attr('id', 'lines') .attr('clip-path', 'url(#clip)') .selectAll('path').data(data).enter() .append('path') .attr('class', 'line') .attr('stroke', (d, i) => d3.schemeCategory10[i]) .attr('d', line) .each(scroll); function scroll(d) { d.push(random()); d3.select(this).attr("transform",null) .attr("d",line) .transition() .attr("transform", "translate(" + x(0) + ")") .duration(750) .ease(d3.easeLinear) .on("end",scroll);
d.shift();}
#lines {    fill: none;    stroke: black;    stroke-width: 1.5px;}.y.axis path {    stroke: black;}p {    padding: 40px;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

D3 listening to end event of a transition

Ah, .each("end", function() {...}) (version 3) seems to have been replaced by .on("end", ...) in version 4.

Responding to tick events during d3 transitions

There's no tick event for transitions, so you can't do exactly what you want. There should be no need for this however -- you can simply add a transition to the line end in the same way as to the rectangle. The code would look something like this.

rect.transition().attr("x", newX).attr("y", newY);
line.transition().attr("x2", newX).attr("y2", newY);

D3 Stop interaction during ongoing transition

There are several ways to do this, like using a flag. However, the most idiomatic D3 is just check if the element has an active transition, which is can be as simple as:

if(d3.active(this)) return;

Here is your code with that change:

<!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<title>D3v6 Transition Example</title>
<!-- call external d3.js framework -->
<script src="https://d3js.org/d3.v6.js"></script>
</head>

<style>
body {
background-color: rgb(220, 220, 220);
overflow: hidden;
margin: 0px;
}

.node {
stroke: white;
stroke-width: 2px;
cursor: pointer;
}

.node:hover {
stroke: red
}

.link {
fill: none;
cursor: default;
stroke: rgb(0, 0, 0);
stroke-width: 3px;
}
</style>

<body>

<svg id="svg"> </svg>

<script>
var graph = {
"nodes": [{
"id": 0,
},
{
"id": 1,
},
{
"id": 2,
}
],
"links": [{
"source": 0,
"target": 1,
},
{
"source": 1,
"target": 2,
},
{
"source": 2,
"target": 0,
}
]
}

var width = window.innerWidth
var height = window.innerHeight

var svg = d3.select("svg")
.attr("class", "canvas")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function(event) {
svg.attr("transform", event.transform)
}))
.append("g")

// remove zoom on dblclick listener
d3.select("svg").on("dblclick.zoom", null)

var linkContainer = svg.append("g").attr("class", "linkContainer")
var nodeContainer = svg.append("g").attr("class", "nodeContainer")

var isRed = false;

var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) {
return d.id;
}).distance(100))
.force("charge", d3.forceManyBody().strength(-500))
.force("center", d3.forceCenter(width / 2, height / 2))
.force("collision", d3.forceCollide().radius(50))

initialize()

function initialize() {

link = linkContainer.selectAll(".link")
.data(graph.links)
.join("line")
.attr("class", "link")

node = nodeContainer.selectAll(".node")
.data(graph.nodes, d => d.id)
.join("g")
.attr("class", "node")

node.selectAll("circle")
.data(d => [d])
.join("circle")
.attr("r", 30)
.style("fill", "whitesmoke")
.on("mouseenter", mouseEnter)

node.selectAll("text")
.data(d => [d])
.join("text")
.style("class", "icon")
.attr("font-family", "FontAwesome")
.attr("dominant-baseline", "central")
.attr("text-anchor", "middle")
.attr("font-size", 30)
.attr("fill", "black")
.attr("stroke-width", "0px")
.attr("pointer-events", "none")
.text((d) => {
return d.id
})

simulation
.nodes(graph.nodes)
.on("tick", ticked);

simulation
.force("link")
.links(graph.links)

}

function mouseEnter(d) {
if (d3.active(this)) return;
if (!isRed) {
d3.select(this)
.transition()
.duration(1500)
.style("fill", "red")

isRed = true
} else {
d3.select(this)
.transition()
.duration(1500)
.style("fill", "whitesmoke")

isRed = false
}
}

function ticked() {
// update link positions
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;
});

// update node positions
node
.attr("transform", function(d) {
return "translate(" + d.x + ", " + d.y + ")";
});
}
</script>
</body>

</html>

Getting error on applying transition in d3

Add the .on(...) call before the .transition(), then it should be fine.



Related Topics



Leave a reply



Submit