Reputation: 59
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:
Upvotes: 0
Views: 287
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