D3 Stop interaction during ongoing transition

I noticed the Control Flow section in the D3 API and tried to apply those on my code. My final goal is to avoid any “mouseenter”, “mouseleave”, “click”.. etc. interaction as long as the transition() is still ongoing.

The snippet displays 3 nodes which are swapping the color after an “mouseenter” event. The problem is, if I interrupt a ongoing transition with in “mouseenter” event the color changes back to white. How can I avoid such behaviour?

After my applying attempts I receive await is only valid in async functions and async generators as an error.

<!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 (!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>

Answer

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>