Elad Notti
Elad Notti

Reputation: 59

D3+javascript+react : Cant add a label to edge (link)

I have a perfectly working 3d force layout graph. and I'm trying to add a label to a link (edge).

My problem is the positioning of the label on top of the link.

Here's the data:

const { edges, nodes } = {
  nodes: [
    {
    id: "projects",
    class: "RELATIONAL",
    weight: 1,
  },
    {
    id: "schemas",
    class: "RELATIONAL",
    weight: 1,
  },
    {
    id: "users",
    class: "collection",
    weight: 1,
  },
    {
      id: "cats",
      class: "collection",
      weight: 1,
    }
  ],
  edges: [
    {
    source: 'projects',
    target: 'schemas',
    weight: 2,
    overlap: 1,
      connectionType: 'O2O',
    type: 'parent' // should be the type of connection O2M , O2O erc..
  },
    {
    source: 'users',
    target: 'projects',
    weight: 2,
    overlap: 1,
      connectionType: 'O2O',
    type: 'parent' // should be the type of connection O2M , O2O erc..
  },
    {
    source: 'schemas',
    target: 'projects',
    weight: 2,
    overlap: 1,
      connectionType: 'O2O',
    type: 'child' // should be the type of connection O2M , O2O erc..
  },
    {
    source: 'cats',
    target: 'users',
    weight: 2,
      connectionType: 'O2O',
    overlap: 1,
    type: 'child' // should be the type of connection O2M , O2O erc..
  },
    {
    source: 'users',
    target: 'cats',
    weight: 2,
    overlap: 1,
      connectionType: 'O2O',
    type: 'child' // should be the type of connection O2M , O2O erc..
  }
  ]
}

here's my code:

import React, { useEffect, useRef } from "react";
import { forceSimulation, forceManyBody, forceLink, forceCenter } from 'd3-force';
import * as d3 from "d3";
import { line, curveCardinal } from "d3-shape"
import { Box } from "@mui/system";

type Node = {
  id: string;
  class: string;
  component?: string; // for future use, color the nodes for dividing to components.
  x?: string;
  y?: string;
}


function Rd4Component (): JSX.Element {
  const d3Container = useRef(null);
  function arcPath(leftHand: any, d: any) {
    const start = leftHand ? d.source : d.target,
      end = leftHand ? d.target : d.source,
      dx = end.x - start.x,
      dy = end.y - start.y,
      dr = Math.sqrt(dx * dx + dy * dy),
      sweep = leftHand ? 0 : 1;
    return "M" + start.x + "," + start.y + "A" + dr + "," + dr + " 0 0," + sweep + " " + end.x + "," + end.y;
  }

  useEffect(() => {
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    const simulation = forceSimulation(nodes);

    simulation
      .force('charge', forceManyBody().strength(-300))
      .force('link',
        forceLink(edges)
          .id((d) => (d as Node).id)
          .distance(75)
      )
      .force("center", forceCenter(400, 100));

    const svg = d3.select('#Target');

    const edge = svg
      .selectAll('path.link')
      .data(edges)
      .enter()
      .append("path")
      .attr("marker-mid", (d) => {
        switch (d.type) {
          // todo - descide about this one (like dashed line )
          case 'parent': {
            return 'url(#markerArrow)';
          }
          default: return 'url(#markerArrow)';
        }
      })
      .attr("stroke", "rgb(148,158,213)")
      .attr("stroke-width", (d) => d.weight)
      .style("fill", "none");

    const linkPath = edge.append("svg:path")
      .attr("class", function(d: any) { return "link " + d.connectionType; })
      .attr("marker-end", function(d) { return "url(#" + d.connectionType + ")"; });

    const textPath = edge.append("svg:path")
      .attr("id", function(d: any) { return d.source.index + "_" + d.target.index; })
      .text( function(d: any) {return d.connectionType })
      .attr("class", "textpath");

    const node = svg
      .selectAll("circle")
      .data(nodes)
      .enter()
      .append("circle")
      .attr("r", 15)
      .attr("stroke", "rgb(148,158,213)")
      .attr("stroke-width", 0.5)
      .style("fill", "rgb(148,158,213)");

    const textContainer = svg
      .selectAll("g.label")
      .data(nodes)
      .enter()
      .append("g");

    textContainer
      .append("text")
      .text(d => d.id)
      .attr("font-size", 12)
      .attr("transform", () => {
        return `translate(${14}, ${16})`
      });

    const card = svg
      .append("g")
      .attr("pointer-events", "none")
      .attr("display", "none");

    const cardBackground = card.append("rect")
      .attr("width", 150)
      .attr("height", 45)
      .attr("fill", "#eee")
      .attr("stroke", "#333")
      .attr("rx", 5);

    const cardTextName = card
      .append("text")
      .attr("transform", "translate(8, 20)");

    const cardTextPosition = card
      .append("text")
      .attr("font-size", "10")
      .attr("transform", "translate(10, 35)");

    let currentTarget;

    node.on("mouseout", () => {
      card.attr("display", "none");
      currentTarget = null;
    });

    node.on("mouseover", (datum) => {
      currentTarget = d3.event.target;
      card.attr("display", "block");

      cardTextName.text(datum.class);
      cardTextPosition.text(datum.id);

   
      const nameWidth = cardTextName.node().getBBox().width;
      const positionWidth = cardTextPosition.node().getBBox().width;
      const cardWidth = Math.max(nameWidth, positionWidth);

      cardBackground.attr("width", cardWidth + 16);

      simulation.alphaTarget(0).restart();

    });

    const lineGenerator = line().curve(curveCardinal);

    function ticked() {
      textContainer
        .attr("transform", function(d: any) {
          return `translate(${d.x + 2}, ${d.y + 4})`;
        });

      node
        .attr("cx", function (d: any) {
          return d.x;
        })
        .attr("cy", function (d: any) {
          return d.y;
        });

      edge
        .attr("d", function (d: any) {
          const mid = [
            (d.source.x + d.target.x) / 2,
            (d.source.y + d.target.y) / 2
          ];

          const index = d.overlap;
          const distance = Math.sqrt(
            Math.pow(d.target.x - d.source.x, 2) +
            Math.pow(d.target.y - d.source.y, 2)
          );
          const slopeX = (d.target.x - d.source.x) / distance;
          const slopeY = (d.target.y - d.source.y) / distance;
          const curveSharpness = 3.5 * index;
          mid[0] += curveSharpness * slopeY;
          mid[1] -= curveSharpness * slopeX;

          return lineGenerator([
            [d.source.x, d.source.y],
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-ignore
            mid,
            [d.target.x, d.target.y]
          ]);
        });

      linkPath.attr("d", function(d: any) {
        return arcPath(false, d);
      });

      textPath.attr("d", function(d: any) {
        return arcPath(d.source.x < d.target.x, d);
      });

      if (currentTarget) {
        const dist = currentTarget.r.baseVal.value + 3;
        const xPos = currentTarget.cx.baseVal.value + dist + 5;
        const yPos = currentTarget.cy.baseVal.value - dist + 35;

        card.attr("transform", `translate(${xPos}, ${yPos})`);
      }
    }

    simulation.nodes(nodes)
      .on("tick", ticked)
  }, [d3Container.current]);

  return <Box sx={{overflowY: "scroll"}}>
    <svg height={500} width={600} id="Target">
      <defs>
        <marker id="markerArrow" markerWidth="7" markerHeight="7" refX="2" refY="2" orient="auto" markerUnits="strokeWidth">
          <path d="M0,0 L0,4 L4,2 z" fill="rgb(148,158,213)" />
        </marker>
      </defs>
    </svg>
  </Box>
}

export default Rd4Component;

here what I get:

result

Upvotes: 0

Views: 287

Answers (1)

Elad Notti
Elad Notti

Reputation: 59

So I figured it out.

Nodes:

  const node = svg
      .selectAll("circle")
      .data(nodes)
      .enter()
      .append("circle")
      .attr("r", 35) // radius field, so width and height 80X80
      .attr("stroke", "lightgray")
      .attr("stroke-width", 0.5)
      .style("fill", "lightgray")

Text:

    const textContainer = svg
      .selectAll("g.label")
      .data(nodes)
      .enter()
      .append("g");

    textContainer
      .append("text")
      .text((d) => d.id)
      .attr("font-size", 12)
      .attr("z-index", 9999999)
      .attr("transform", () => {
        return `translate(${-35}, ${0})`;
      });

Upvotes: 1

Related Questions