К примеру, между .span1 и .span2
<span class="span1">1</span>
<div style="height:100px;"></div>
<span class="span2">2</span>
Вот собрал что-то похожее из svg+js, вообще я обычно такое делаю на d3.js, тут ради исключения сделал на голом js, на d3 листинг был бы не сильно длиннее, но все же лаконичнее...
Чтобы нарисовать прямоугольник, используйте перетаскивание левой кнопкой мыши на свободном месте.
Переместить прямоугольник можно так же перетаскиванием левой кнопкой.
Соединить прямоугольники можно при помощи перетаскивания правой кнопкой начиная на прямоугольнике
Двойное нажатие на прямоугольник позволяет добавить текст.
let x, y, shape, move, svg = document.querySelector('svg');
let attrs = (s, o) => Object.keys(o).forEach(p => s.setAttribute(p, o[p]))
let clampToCenter = e => [+e.getAttribute('x') + e.getAttribute('width')/2,
+e.getAttribute('y') + e.getAttribute('height')/2]
attrs(svg, {width: window.innerWidth, height: window.innerHeight});
function startDrawLink(e) {
shape = document.createElementNS('http://www.w3.org/2000/svg', 'line');
shape.source = e.target;
let p = clampToCenter(e.target);
attrs(shape,{x1: p[0], y1: p[1], x2: e.x, y2: e.y,
stroke: '#000', 'pointer-events': 'none'})
}
function startTranslateRect(e){
move = e.target;
move.px = +e.target.getAttribute('x');
move.py = +e.target.getAttribute('y');
move.ex = e.x;
move.ey = e.y;
}
function translateRect(e) {
attrs(move, {x: move.px + e.x - move.ex, y: move.py + e.y - move.ey})
let p = clampToCenter(move);
document.querySelectorAll('line').forEach(l => {
let isSrc = l.source === move
if (isSrc || l.target === move) {
l.setAttribute(isSrc ? 'x1' : 'x2', p[0])
l.setAttribute(isSrc ? 'y1' : 'y2', p[1])
}
})
e.target.text && attrs(e.target.text, {
x: move.px + e.x - move.ex + e.target.getAttribute('width')/2,
y: move.py + e.y - move.ey + e.target.getAttribute('height')/2
})
}
function doDrawRect(e){
attrs(shape, {
x: Math.min(e.x, shape.cx), y: Math.min(e.y, shape.cy),
width: Math.abs(e.x - shape.cx), height: Math.abs(e.y - shape.cy)
})
}
function doDrawLine(e){
if (e.target.nodeName === "rect" && e.target != shape.source) {
let p = clampToCenter(e.target)
attrs(shape, {x2: p[0], y2: p[1]})
shape.removeAttribute('stroke-dasharray')
} else {
attrs(shape, {x2: e.x, y2: e.y, 'stroke-dasharray': '10 10'})
}
}
addEventListener('mousedown', e => {
if (e.target.nodeName === "rect") {
if (e.button === 2)
startDrawLink(e)
if (e.button === 0)
startTranslateRect(e);
} else {
shape = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
shape.cx = e.x;
shape.cy = e.y;
attrs(shape, {fill: 'white', stroke: 'black'})
}
shape && svg.appendChild(shape);
})
addEventListener('mousemove', e => {
if (move)
translateRect(e);
if (shape)
if (shape.nodeName === "rect")
doDrawRect(e);
else
doDrawLine(e);
})
addEventListener('mouseup', e => {
if (shape && shape.nodeName === "line") {
shape.remove();
if (!shape.getAttribute('stroke-dasharray')) {
svg.insertBefore(shape, svg.firstChild)
shape.target = e.target;
}
}
shape = null;
move = null;
})
addEventListener('contextmenu', e => e.preventDefault())
addEventListener('click', e => {
if (e.detail == 2 && e.target.nodeName === "rect") {
if (!e.target.text) {
e.target.text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
let p = clampToCenter(e.target)
attrs(e.target.text, {x: p[0], y: p[1], 'text-anchor': 'middle',
'dominant-baseline': 'middle', 'pointer-events': 'none'})
svg.appendChild(e.target.text);
}
e.target.text.innerHTML = prompt('enter text', e.target.text.innerHTML || '')
}
})
<body style="margin:0;overflow:hidden;user-select:none"><svg style="background-color: wheat"></svg>
——
UPD: похожий ответ с использованием d3.js
Продвижение своими сайтами как стратегия роста и независимости