const data = [
{
"shapeName": "rect1",
"shapeId": "1.1",
"coordinateData": {
"height": 125,
"width": 100,
"xCoordinate": 300,
"yCoordinate": 100
},
"ticks": [
{
"tickId": "1.1.1",
"sourceTarget": [
{
"sourceTick": "1.1.1",
"targetTick": "1.4.11"
}
]
}
]
},
{
"shapeName": "rect2",
"shapeId": "1.2",
"coordinateData": {
"height": 125,
"width": 100,
"xCoordinate": 850,
"yCoordinate": 100
},
"ticks": [
{
"tickId": "1.2.1",
"sourceTarget": [
{
"sourceTick": "1.2.1",
"targetTick": "1.4.12"
}
]
}
]
},
{
"shapeName": "rect3",
"shapeId": "1.3",
"coordinateData": {
"height": 125,
"width": 100,
"xCoordinate": 1400,
"yCoordinate": 100
},
"ticks": [
{
"tickId": "1.3.1",
"sourceTarget": [
{
"sourceTick": "1.3.1",
"targetTick": "1.4.13"
}
]
}
]
},
{
"shapeName": "rect4",
"shapeId": "1.4",
"coordinateData": {
"height": 375,
"width": 100,
"xCoordinate": 1750,
"yCoordinate": 100
},
"ticks": [
{
"tickId": "1.4.11",
"sourceTarget": null
},
{
"tickId": "1.4.12",
"sourceTarget": null
},
{
"tickId": "1.4.13",
"sourceTarget": null
}
]
}
];
const svg = d3.select('svg');
svg.selectAll('rect.shape')
.data(data, d => d.shapeId)
.enter()
.append('rect')
.classed('shape', true)
.attr('width', d => d.coordinateData.width)
.attr('height', d => d.coordinateData.height)
.attr('x', d => d.coordinateData.xCoordinate)
.attr('y', d => d.coordinateData.yCoordinate);
const ticks = data.reduce((all, item) => {
item.ticks.forEach((tick, index) => {
const x = item.coordinateData.xCoordinate + (tick.sourceTarget ? item.coordinateData.width : 0);
const y = item.coordinateData.yCoordinate + item.coordinateData.height / 2 + (index - (item.ticks.length - 1) / 2) * 30;
all.push({id: tick.tickId, target: tick.sourceTarget ? tick.sourceTarget[0].targetTick : null, x, y});
});
return all;
}, []);
svg.selectAll('circle.tick')
.data(ticks, d => d.id)
.enter()
.append('circle')
.classed('tick', true)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('r', 10)
const links = ticks.filter(d => d.target).map((tick, index) => {
const target = ticks.find(t => t.id === tick.target);
return {id: index, fromX: tick.x, fromY: tick.y, toX: target.x, toY: target.y};
});
const getPath = link => `M ${link.fromX},${link.fromY} H ${link.fromX + 50} V ${link.toY} H ${link.toX}`;
svg.selectAll('path.link')
.data(links, d => d.id)
.enter()
.append('path')
.classed('link', true)
.attr('d', getPath)
svg {
border: 1px solid gray;
}
path, .shape {
stroke-width: 1;
stroke: black;
fill: none;
}
.tick {
fill: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="600" height="150" viewBox="0 0 2000 500"/>