我正在创建一个网络图,它用路径连接节点。
我的要求很简单-网络图应该是垂直或水平的,没有折叠。
到目前为止,我创建了一个以水平格式显示图表的图形。
然而,如果节点集非常有限,则图形仅显示为单行(没有折叠)(我尝试了forceManyBody().strength()
和forceLink(links).distance()
的多次试错以使其工作)
但对于更大的不。图会这样折叠
d3.forceManyBody().strength(-600)
的一些变体给我一个单行,但链接顺序相反,像这样--
在这里,5050圈应该是第一圈,但它在最后到来。
我的问题是
1.如何根据节点正确查找forceManyBody().strength()
和forceLink(links).distance()
,以便只有一行
1.为什么第一个圆圈最后才出现?
我不介意如果我必须滚动查看所有节点(可能是d3.zoom可以帮助?)
找人指点。请在下面找到代码和数据:
const width = 1413;
const height = 480;
// data
const nodes = [{
"_time": 1666891307118,
"CUSTOMER_NAME": " Customer_1",
"CUSTOMER": "CID_123",
"SOURCE": "QUEUE_PROCESSOR",
"SUPPLIER_ANID": "SUPP_ID",
"TRACKING_STATUS": "FAILED",
"CHECKPOINT": "5050",
"DOCUMENT_NUMBER": "DOC_NO_123",
"PAYLOAD_ID": "PID_123"
},
{
"_time": 1666891307241,
"CUSTOMER_NAME": " Customer_1",
"CUSTOMER": "CID_123",
"SOURCE": "PROPAGATION_DISPATCHER",
"SUPPLIER_ANID": "SUPP_ID",
"TRACKING_STATUS": "FAILED",
"CHECKPOINT": "1110",
"DOCUMENT_NUMBER": "DOC_NO_123",
"PAYLOAD_ID": "PID_123"
},
{
"_time": 1666891307580,
"CUSTOMER_NAME": " Customer_1",
"CUSTOMER": "CID_123",
"SOURCE": "PROPAGATION_PROCESSOR",
"SUPPLIER_ANID": "SUPP_ID",
"TRACKING_STATUS": "FAILED",
"CHECKPOINT": "1150",
"DOCUMENT_NUMBER": "DOC_NO_123",
"PAYLOAD_ID": "PID_123"
},
{
"_time": 1666891307937,
"CUSTOMER_NAME": " Customer_1",
"CUSTOMER": "CID_123",
"SOURCE": "QUEUE_PROCESSOR",
"SUPPLIER_ANID": "SUPP_ID",
"TRACKING_STATUS": "FAILED",
"CHECKPOINT": "5000",
"DOCUMENT_NUMBER": "DOC_NO_123",
"PAYLOAD_ID": "PID_123"
},
{
"_time": 1666891308121,
"CUSTOMER_NAME": " Customer_1",
"CUSTOMER": "CID_123",
"SOURCE": "QUEUE_PROCESSOR",
"SUPPLIER_ANID": "SUPP_ID",
"TRACKING_STATUS": "FAILED",
"CHECKPOINT": "5010",
"DOCUMENT_NUMBER": "DOC_NO_123",
"PAYLOAD_ID": "PID_123"
},
{
"_time": 1666891308278,
"CUSTOMER_NAME": " Customer_1",
"CUSTOMER": "CID_123",
"SOURCE": "CXML_OUT_DISPATCHER",
"SUPPLIER_ANID": "SUPP_ID",
"TRACKING_STATUS": "FAILED",
"CHECKPOINT": "1250",
"DOCUMENT_NUMBER": "DOC_NO_123",
"PAYLOAD_ID": "PID_123"
},
{
"_time": 1666891308605,
"CUSTOMER_NAME": " Customer_1",
"CUSTOMER": "CID_123",
"SOURCE": "PROPAGATION_PROCESSOR",
"SUPPLIER_ANID": "SUPP_ID",
"TRACKING_STATUS": "FAILED",
"CHECKPOINT": "1145",
"DOCUMENT_NUMBER": "DOC_NO_123",
"PAYLOAD_ID": "PID_123"
},
{
"_time": 1666891309471,
"CUSTOMER_NAME": " Customer_1",
"CUSTOMER": "CID_123",
"SOURCE": "CXML_OUT_DISPATCHER",
"SUPPLIER_ANID": "SUPP_ID",
"TRACKING_STATUS": "FAILED",
"CHECKPOINT": "1300",
"DOCUMENT_NUMBER": "DOC_NO_123",
"PAYLOAD_ID": "PID_123"
},
{
"_time": 1666891309485,
"CUSTOMER_NAME": " Customer_1",
"CUSTOMER": "CID_123",
"SOURCE": "CXML_OUT_DISPATCHER",
"SUPPLIER_ANID": "SUPP_ID",
"TRACKING_STATUS": "FAILED",
"CHECKPOINT": "1450",
"DOCUMENT_NUMBER": "DOC_NO_123",
"PAYLOAD_ID": "PID_123"
},
{
"_time": 1666891313018,
"CUSTOMER_NAME": " Customer_1",
"CUSTOMER": "CID_123",
"SOURCE": "QUEUE_PROCESSOR",
"SUPPLIER_ANID": "SUPP_ID",
"TRACKING_STATUS": "FAILED",
"CHECKPOINT": "5050",
"DOCUMENT_NUMBER": "DOC_NO_123",
"PAYLOAD_ID": "PID_123"
},
{
"_time": 1666902123954,
"CUSTOMER_NAME": " Customer_1",
"CUSTOMER": "CID_123",
"SOURCE": "EXTERNAL_GATEWAY",
"SUPPLIER_ANID": "SUPP_ID",
"TRACKING_STATUS": "FAILED",
"CHECKPOINT": "1440",
"DOCUMENT_NUMBER": "DOC_NO_123",
"PAYLOAD_ID": "PID_123"
}
];
const links = [{
"source": 0,
"target": 1,
"time": 123
},
{
"source": 1,
"target": 2,
"time": 339
},
{
"source": 2,
"target": 3,
"time": 357
},
{
"source": 3,
"target": 4,
"time": 184
},
{
"source": 4,
"target": 5,
"time": 157
},
{
"source": 5,
"target": 6,
"time": 327
},
{
"source": 6,
"target": 7,
"time": 866
},
{
"source": 7,
"target": 8,
"time": 14
},
{
"source": 8,
"target": 9,
"time": 3533
},
{
"source": 9,
"target": 10,
"time": 10810936
}
];
const circleRadius = 25;
const linkColor = '#999'; //#FFFF00
const dangerColor = '#FF5286';
const dangerTimeInSec = 2;
const WAITING_FOR_CONFIRMATION_COLOR = '#F8D06B';
const IN_PROCESS_COLOR = '#6E9FFF';
const COMPLETED_COLOR = '#6CCF8E';
const ERROR_COLOR = '#FF5286';
function getStatusColor(data) {
if (data.TRACKING_STATUS === 'WAITING_FOR_CONFIRMATION') {
return WAITING_FOR_CONFIRMATION_COLOR;
}
if (data.TRACKING_STATUS === 'IN_PROCESS') {
return IN_PROCESS_COLOR;
}
if (data.TRACKING_STATUS === 'COMPLETED') {
return COMPLETED_COLOR;
}
if (data.TRACKING_STATUS === 'FAILED') {
return ERROR_COLOR;
}
return 'gray';
}
function getTimeTextColor(data) {
if (data.time > (dangerTimeInSec * 1000)) {
return dangerColor;
}
return linkColor
}
function getTimeBetweenNodes(data) {
const timeInSecs = data.time / 1000;
return `${timeInSecs}s`
}
function createChart() {
const svgId = "svgId";
const node = document.getElementById(svgId);
// svg.append('g';)
while (node && node.firstChild) {
node && node.firstChild.remove();
}
const svg = d3.select(`#${CSS.escape(svgId)}`);
// const centerX = width /2;
const centerY = height / 2;
const simulation = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-600))
.force(
"collision",
d3
.forceCollide()
.radius(function(d) {
return d.radius * 2;
})
)
.force("link", d3.forceLink(links).distance(50))
.force("y", d3.forceY(0).strength(0.55))
.force("center", d3.forceCenter(width / 2, centerY))
.stop();
for (let i = 0; i < 300; ++i) {
simulation.tick();
}
const arrowId = `arrow-${svgId}`;
svg.append("svg:defs").append("svg:marker")
.attr("id", arrowId)
.attr("viewBox", "0 -5 10 10")
.attr('refX', 0)
.attr("markerWidth", 5)
.attr("markerHeight", 5)
.attr("orient", "auto")
.append("svg:path")
.style("stroke", linkColor)
.attr("fill", linkColor)
.attr("d", "M0,-5L10,0L0,5");
const lines = svg.selectAll("line")
.data(links)
.enter().append("path")
.attr("class", "link")
.style("stroke", linkColor)
.attr('marker-end', (d) => `url(#${arrowId})`)
.style("stroke-width", 1);
const circles = svg.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('fill', 'none')
.attr('stroke', (d) => {
return getStatusColor(d)
})
.style("pointer-events", "visible")
.attr('stroke-width', 2)
.attr('r', circleRadius)
// .call(drag)
// .call(zoom)
// .on('click', handleClick);
// svg.call(zoom);
const texts = svg.selectAll('text')
.data(nodes)
.enter()
.append('text')
.attr('text-anchor', 'middle')
.attr('text-baseline', 'middle')
.attr('font-size', '.8rem')
.attr('fill', '#FFF')
.style('pointer-events', 'none')
.text((node) => `${node.CHECKPOINT}`);
const timeTexts = svg
.selectAll("timeText")
.data(links)
.enter()
.append("text")
.attr("text-anchor", "middle")
.attr("text-baseline", "middle")
.attr("font-size", ".8rem")
.style("pointer-events", "none")
.attr('fill', (d) => getTimeTextColor(d))
.style('pointer-events', 'none')
.text((node) => getTimeBetweenNodes(node));
const sourceTexts = svg.selectAll('sourceTexts')
.data(nodes)
.enter()
.append('foreignObject')
.attr("width", 80)
.attr("height", 80);
sourceTexts.append("xhtml:div")
.append('p')
.attr('class', 'source-text')
.html((d) => {
return d.SOURCE.split("_").join(" ")
});
circles.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y);
texts.attr('x', (d) => d.x)
.attr('y', (d) => d.y + (circleRadius / 8));
sourceTexts.attr('x', (d) => {
return d.x - (circleRadius * 1.5);
})
.attr('y', (d) => d.y + (circleRadius));
timeTexts.attr("x", (d) => {
return d.source.x + (d.target.x - d.source.x) / 2;
}).attr("y", (d) => {
return d.source.y + (d.target.y - d.source.y) / 2 - 10;
});
lines
.attr("d", (d) => "M" + (d.source.x + circleRadius) + "," + (d.source.y) + ", " + (d.target.x - (circleRadius + 10)) + "," + (d.target.y))
}
setTimeout(() => {
createChart()
}, 1000);
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg id="svgId" width="1413px" height="100vh"></svg>
1条答案
按热度按时间83qze16e1#
正如评论中提到的。您的用例非常简单,可以使用比例和形状重新创建。我使用了一个线性标度,并使用节点的索引作为标度的域,因为节点之间的时间间隔在多个数量级上不同。
每个节点包含在一个组中,以简化圆、文本和线的相对定位。
由于边的数量具有节点的长度-1,所以我使用each函数单独迭代节点组,并且如果当前索引不是最后一个,则仅附加边。