1

D3.js mouse dragging event is not working

First question here, so I would be happy if you guys could let me know if I asked the correct way or not.

I am trying to replicate Obsidian's (a note taking app) graph view , where you can drag the nodes and also nodes grow bigger when you hover over the mouse button. I am doing this for for another usage case. However, although my first propotype is working completely fine, improved versions stopped working.

Here is the first script's mouse events, which is working completely fine:

import * as d3 from 'd3';
import * as lil from 'lil-gui';

const parameters = {
  repelForce: -100,
  linkForce: 0.5,
  centerForce: 0.1,
  linkDistance: 30,
  dragConnectedRooms: false,
  display: {
    nodes: true,
    links: true,
    labels: true,
  },
};

const boundaryDimensions = {
  width: 400,
  height: 100,
};

let simulation = null;
const width = 1280;
const height = 720;

let svg = null;

function updateSimulation() {
  if (simulation) {
    simulation
      .force('link', d3.forceLink().id(d => d.id).distance(parameters.linkDistance))
      .force('charge', d3.forceManyBody().strength(parameters.repelForce))
      .force('center', d3.forceCenter(width / 2, height / 2).strength(parameters.centerForce))
      .alpha(1)
      .restart();
  }
}

function drag(simulation) {
  function dragStarted(event) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    event.subject.fx = event.subject.x;
    event.subject.fy = event.subject.y;
  }

  function dragged(event) {
    event.subject.fx = event.x;
    event.subject.fy = event.y;
  }

  function dragEnded(event) {
    if (!event.active) simulation.alphaTarget(0);
    event.subject.fx = null;
    event.subject.fy = null;
  }

  return d3.drag()
    .on('start', dragStarted)
    .on('drag', dragged)
    .on('end', dragEnded);
}

function updateBoundary() {
  svg.select('.boundary')
    .attr('width', boundaryDimensions.width)
    .attr('height', boundaryDimensions.height)
    .attr('x', (width - boundaryDimensions.width) / 2)
    .attr('y', (height - boundaryDimensions.height) / 2);
}

d3.json('room-data.json').then(data => {
  simulation = d3.forceSimulation(data.nodes)
    .force('link', d3.forceLink(data.links).id(d => d.id).distance(parameters.linkDistance))
    .force('charge', d3.forceManyBody().strength(parameters.repelForce))
    .force('center', d3.forceCenter(width / 2, height / 2).strength(parameters.centerForce));

  svg = d3.create('svg')
    .attr('width', width)
    .attr('height', height)
    .call(d3.zoom().scaleExtent([0.1, 10]).on('zoom', function (event) {
      svg.attr('transform', event.transform);
    }));

  const linkGroup = svg.append('g').attr('class', 'links');
  const nodeGroup = svg.append('g').attr('class', 'nodes');
  const labelGroup = svg.append('g').attr('class', 'labels');

  const boundary = svg.append('rect')
    .attr('class', 'boundary')
    .attr('width', boundaryDimensions.width)
    .attr('height', boundaryDimensions.height)
    .attr('x', (width - boundaryDimensions.width) / 2)
    .attr('y', (height - boundaryDimensions.height) / 2)
    .style('fill', 'none')
    .style('stroke', 'white')
    .style('stroke-width', 1);

  const link = linkGroup
    .selectAll('line')
    .data(data.links)
    .join('line')
    .style('stroke', 'white');

  const node = nodeGroup
    .selectAll('circle')
    .data(data.nodes)
    .join('circle')
    .attr('r', d => Math.sqrt(d.area))
    .style('fill', 'white')
    .call(drag(simulation))
    .on('mouseover', function (event, d) {
      d3.select(this).transition()
        .duration(500)
        .attr('r', Math.sqrt(d.area) * 1.5)
        .style('fill', '#ffd92c');
      label.filter(l => l.id === d.id)
        .transition()
        .duration(500)
        .style('font-weight', 'bold')
        .attr('dy', '2em');
    })
    .on('mouseout', function (event, d) {
      d3.select(this).transition()
        .duration(500)
        .attr('r', Math.sqrt(d.area))
        .style('fill', 'white');
      label.filter(l => l.id === d.id)
        .transition()
        .duration(500)
        .style('font-weight', 'normal')
        .attr('dy', '1.3em');
    });

  const label = labelGroup
    .selectAll('text')
    .data(data.nodes)
    .enter()
    .append('text')
    .text(d => d.name)
    .style('font-family', 'Lato')
    .style('fill', 'white')
    .attr('text-anchor', 'middle')
    .attr('dy', '1.3em');

  simulation.on('tick', () => {
    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);

    node
      .attr('cx', d => d.x)
      .attr('cy', d => d.y);

    label
      .attr('x', d => d.x)
      .attr('y', d => d.y);
  });

  document.body.appendChild(svg.node());

  const gui = new lil.GUI({ width: 300 });
  gui.add(parameters, 'repelForce', -500, 500).onChange(updateSimulation).name('Repel Force');
  gui.add(parameters, 'linkForce', 0, 2).onChange(updateSimulation).name('Link Force');
  gui.add(parameters, 'centerForce', 0, 1).onChange(updateSimulation).name('Center Force');
  gui.add(parameters, 'linkDistance', 0, 100).onChange(updateSimulation).name('Link Distance');
  gui.add(parameters, 'dragConnectedRooms');
  gui.add(boundaryDimensions, 'width', 100, 400).step(12.5).onChange(updateBoundary).name('Boundary Width (m)');
  gui.add(boundaryDimensions, 'height', 100, 400).step(12.5).onChange(updateBoundary).name('Boundary Height (m)');


  const displayFolder = gui.addFolder('Display Options');
  displayFolder.add(parameters.display, 'nodes').name('Show Nodes').onChange(value => {
    nodeGroup.style('display', value ? 'block' : 'none');
  });
  displayFolder.add(parameters.display, 'links').name('Show Links').onChange(value => {
    linkGroup.style('display', value ? 'block' : 'none');
  });
  displayFolder.add(parameters.display, 'labels').name('Show Labels').onChange(value => {
    labelGroup.style('display', value ? 'block' : 'none');
  });
});

However, as my code became more developed, this function stopped working as I also tried to limit the dragging inside the boundary. Here is the parts related in the developed version:

function drag(simulation) {
  function dragStarted(event) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    event.subject.fx = event.subject.x;
    event.subject.fy = event.subject.y;
  }

  function dragged(event) {
    const boundaryWidth = parameters.initialInputs.boundaryDimensions.width;
    const boundaryHeight = parameters.initialInputs.boundaryDimensions.height;
    const nodeRadius = Math.sqrt(event.subject.area);

    // Calculate the allowed range within the boundary
    const minX = boundaryX + nodeRadius;
    const minY = boundaryY + nodeRadius;
    const maxX = boundaryX + boundaryWidth - nodeRadius;
    const maxY = boundaryY + boundaryHeight - nodeRadius;

    // Restrict the node's position within the allowed range
    event.subject.fx = Math.max(minX, Math.min(maxX, event.x));
    event.subject.fy = Math.max(minY, Math.min(maxY, event.y));
  }


  function dragEnded(event) {
    if (!event.active) simulation.alphaTarget(0);
    event.subject.fx = null;
    event.subject.fy = null;
  }

  return d3.drag()
    .on('start', dragStarted)
    .on('drag', dragged)
    .on('end', dragEnded);
}
...
  const node = nodeGroup
    .selectAll('circle')
    .data(data.nodes)
    .join('circle')
    .attr('r', d => Math.sqrt(d.area))
    .style('fill', d => colorScale(d.name))
    .call(drag(simulation))
    .on('mouseover', function (event, d) {
      d3.select(this).transition()
        .duration(500)
        .attr('r', Math.sqrt(d.area) * 1.5)
        .style('fill', '#fff');
      label.filter(l => l.id === d.id)
        .transition()
        .duration(500)
        .style('font-weight', 'bold')
        .attr('dy', '2em');
    })
    .on('mouseout', function (event, d) {
      d3.select(this).transition()
        .duration(500)
        .attr('r', Math.sqrt(d.area))
        .style('fill', colorScale(d.name));
      label.filter(l => l.id === d.id)
        .transition()
        .duration(500)
        .style('font-weight', 'normal')
        .attr('dy', '0.3em');
    });

I asked ChatGPT-4 a few times, but it couldn't identify the issue. I thought it could be related to the forces so I made them 0 but still did not work. Any ideas? Thanks beforehand!

ydenktas
  • 11
  • 1

0 Answers0