Skip to content

Instantly share code, notes, and snippets.

@flashingcursor
Last active November 3, 2025 03:11
Show Gist options
  • Select an option

  • Save flashingcursor/69d0c624a0c55a08a8236f170185c1e9 to your computer and use it in GitHub Desktop.

Select an option

Save flashingcursor/69d0c624a0c55a08a8236f170185c1e9 to your computer and use it in GitHub Desktop.
JointJS: Cable
<div id="paper"></div>
const { dia, shapes, linkTools, elementTools } = joint;
const cellNamespace = {
...shapes,
};
const graph = new dia.Graph({}, { cellNamespace });
const paper = new dia.Paper({
el: document.getElementById('paper'),
width: 800,
height: 600,
model: graph,
cellViewNamespace: cellNamespace,
overflow: true,
highlighting: {
connecting: {
name: 'mask'
}
},
snapLinks: { radius: 10 },
connectionStrategy: function(end, view, magnet, coords, link) {
const bbox = view.model.getBBox();
const { x, y } = bbox.pointNearestToPoint(coords).difference(bbox.topLeft());
end.anchor = { name: 'topLeft', args: { dx: x, dy: y }};
end.connectionPoint = { name: 'anchor' };
return end;
},
validateConnection: function(cellViewS, magnetS, cellViewT, magnetT, end, linkView) {
if (magnetS === magnetT) return false;
if (cellViewS === cellViewT) return false;
if (end === 'target' && !cellViewT.model.get('connect')) return false;
if (end === 'source' && !cellViewS.model.get('connect')) return false;
return true;
},
});
paper.scale(2, 2);
// Cable
const copperMarker = {
type: 'rect',
x: -6,
y: -2,
width: 8,
height: 4,
stroke: 'none',
fill: '#B87333',
};
const cableSource = new shapes.standard.Rectangle({
position: { x: 0, y: 0 },
size: { width: 20, height: 20 },
z: -1,
attrs: {
body: {
class: 'cable-end',
fill: '#222',
stroke: 'none',
},
},
});
const cableTarget = new shapes.standard.Rectangle({
position: { x: 100, y: 0 },
size: { width: 20, height: 20 },
z: -1,
attrs: {
body: {
class: 'cable-end',
fill: '#222',
stroke: 'none',
},
},
});
const cable = new shapes.standard.Link({
z: -2,
source: {
id: cableSource.id,
anchor: { name: 'right', args: { rotate: true }},
connectionPoint: { name: 'anchor' },
},
target: {
id: cableTarget.id,
anchor: { name: 'left', args: { rotate: true }},
connectionPoint: { name: 'anchor' },
},
connector: { name: 'curve', args: { rotate: true }},
attrs: {
line: {
stroke: '#222',
strokeWidth: 20,
targetMarker: null,
strokeLinecap: 'square',
},
},
});
const redWireSource = new shapes.standard.Link({
source: {
id: cableSource.id,
anchor: { name: 'left', args: { rotate: true }},
connectionPoint: { name: 'anchor' },
},
target: { x: -50, y: 20 },
connector: { name: 'curve', args: { rotate: true }},
attrs: {
line: {
stroke: 'red',
strokeWidth: 4,
targetMarker: copperMarker,
},
},
});
const blueWireSource = new shapes.standard.Link({
source: {
id: cableSource.id,
anchor: { name: 'left', args: { dy: 5, rotate: true }},
connectionPoint: { name: 'anchor' },
},
target: { x: -50, y: 50 },
connector: { name: 'curve', args: { rotate: true }},
attrs: {
line: {
stroke: 'blue',
strokeWidth: 4,
targetMarker: copperMarker,
},
},
});
const greenWireSource = new shapes.standard.Link({
source: {
id: cableSource.id,
anchor: { name: 'left', args: { dy: -5, rotate: true }},
connectionPoint: { name: 'anchor' },
},
target: { x: -50, y: -10 },
connector: { name: 'curve', args: { rotate: true }},
attrs: {
line: {
stroke: 'green',
strokeWidth: 4,
targetMarker: copperMarker,
},
},
});
const redWireTarget = new shapes.standard.Link({
source: { x: 150, y: 20 },
target: {
id: cableTarget.id,
anchor: { name: 'right', args: { rotate: true }},
connectionPoint: { name: 'anchor' },
},
connector: { name: 'curve', args: { rotate: true }},
attrs: {
line: {
stroke: 'red',
strokeWidth: 4,
targetMarker: null,
sourceMarker: copperMarker,
},
},
});
const blueWireTarget = new shapes.standard.Link({
source: { x: 150, y: 50 },
target: {
id: cableTarget.id,
anchor: { name: 'right', args: { dy: 5, rotate: true }},
connectionPoint: { name: 'anchor' },
},
connector: { name: 'curve', args: { rotate: true }},
attrs: {
line: {
stroke: 'blue',
strokeWidth: 4,
targetMarker: null,
sourceMarker: copperMarker,
},
},
});
const greenWireTarget = new shapes.standard.Link({
source: { x: 150, y: -10 },
target: {
id: cableTarget.id,
anchor: { name: 'right', args: { dy: -5, rotate: true }},
connectionPoint: { name: 'anchor' },
},
connector: { name: 'curve', args: { rotate: true }},
attrs: {
line: {
stroke: 'green',
strokeWidth: 4,
targetMarker: null,
sourceMarker: copperMarker,
},
},
});
graph.addCells([
cableSource, cableTarget,
cable,
redWireSource, blueWireSource, greenWireSource,
redWireTarget, blueWireTarget, greenWireTarget,
]);
cableSource.embed([redWireSource, blueWireSource, greenWireSource]);
cableTarget.embed([redWireTarget, blueWireTarget, greenWireTarget]);
cableSource.rotate(45);
cableTarget.rotate(45);
cableSource.position(150, 100, { deep: true });
cableTarget.position(300, 100, { deep: true })
// Other elements
const r1 = new shapes.standard.Rectangle({
connect: true,
position: { x: 0, y: 100 },
size: { width: 50, height: 50 },
attrs: {
body: {
fill: 'white',
stroke: '#333',
},
},
});
const r2 = new shapes.standard.Rectangle({
connect: true,
position: { x: 400, y: 100 },
size: { width: 50, height: 50 },
attrs: {
body: {
fill: 'white',
stroke: '#333',
},
},
});
graph.addCells([r1, r2]);
// Tools
[cable].forEach(link => {
link.findView(paper).addTools(new dia.ToolsView({
tools: [
new linkTools.Vertices(),
]
}));
});
[redWireSource, blueWireSource, greenWireSource].forEach(link => {
link.findView(paper).addTools(new dia.ToolsView({
tools: [
new linkTools.Vertices(),
new linkTools.TargetArrowhead({
tagName: 'rect', attributes: { width: 10, height: 10, x: -2, y: -5, fill: 'red', opacity: 0.1 }}),
]
}));
});
[redWireTarget, blueWireTarget, greenWireTarget].forEach(link => {
link.findView(paper).addTools(new dia.ToolsView({
tools: [
new linkTools.Vertices(),
new linkTools.SourceArrowhead({
tagName: 'rect', attributes: { width: 10, height: 10, x: -7, y: -5, fill: 'red', opacity: 0.1 }}),
]
}));
});
const RotateTool = elementTools.Control.extend({
children: [
{
tagName: 'image',
selector: 'handle',
attributes: {
cursor: 'pointer',
x: -10,
y: -10,
width: 20,
height: 20,
'xlink:href': 'https://assets.codepen.io/7589991/rotate.png'
}
},
{
tagName: 'rect',
selector: 'extras',
attributes: {
'pointer-events': 'none',
fill: 'none',
stroke: '#33334F',
'stroke-dasharray': '2,4',
rx: 5,
ry: 5
}
}
],
getPosition: function(view) {
const { model } = view;
const { width, height } = model.size();
return new g.Point(width + 10, height + 10);
},
setPosition: function(view, coordinates) {
const { model } = view;
const { width, height } = model.size();
const center = new g.Point(width / 2, height / 2);
const angle = center.angleBetween(coordinates, this.getPosition(view));
model.rotate(Math.round(angle));
}
});
[cableSource, cableTarget].forEach(cell => {
cell.findView(paper).addTools(new dia.ToolsView({
tools: [
new RotateTool({ selector: 'body' })
]
}));
});
<script src="https://cdn.jsdelivr.net/npm/@joint/[email protected]/dist/joint.min.js"></script>
#paper-container {
position: absolute;
right: 0;
top: 0;
left: 0;
bottom: 0;
}
#rankdir {
position: absolute;
top: 10px;
left: 10px;
}
#logo {
position: absolute;
bottom: 20px;
right: 0;
}
.cable-end:hover {
fill: #444;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment