水平显示D3网络图,不折叠

6ovsh4lw  于 2023-05-18  发布在  其他
关注(0)|答案(1)|浏览(106)

我正在创建一个网络图,它用路径连接节点。
我的要求很简单-网络图应该是垂直或水平的,没有折叠。
到目前为止,我创建了一个以水平格式显示图表的图形。
然而,如果节点集非常有限,则图形仅显示为单行(没有折叠)(我尝试了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>
83qze16e

83qze16e1#

正如评论中提到的。您的用例非常简单,可以使用比例和形状重新创建。我使用了一个线性标度,并使用节点的索引作为标度的域,因为节点之间的时间间隔在多个数量级上不同。
每个节点包含在一个组中,以简化圆、文本和线的相对定位。
由于边的数量具有节点的长度-1,所以我使用each函数单独迭代节点组,并且如果当前索引不是最后一个,则仅附加边。

// 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 width = 1600;
const height = 400;

const margin = 100;

const data = nodes.map((d) => {
d.id = d._time - nodes[0]._time;
return d;
});

console.log({data})

const svg = d3.select('svg');



const container = svg.append('g')
.style('transform', `translate(${margin}px, ${height / 2}px)`);
const innerWidth = width - (margin * 2);

const scale = d3.scaleLinear()
.range([0, innerWidth])
.domain(d3.extent(data, (d, i) => i));

const groups = container.selectAll('g')
.data(data)
.enter()
.append('g')
.style('transform', (d, i) => `translate(${scale(i)}px, 0`);

groups.each(function(d, i) {
    const e = d3.select(this);
  if (i < data.length - 1) {
    e.append('line')
      .attr('x1', 0)
      .attr('y1', 0)
      .attr('x2', scale(1))
      .attr('y2', 0);
      
    e.append('text')
        .attr('x', scale(1) / 2)
      .attr('y', -20)
      .attr('text-anchor', 'middle')
      .text(data[i+1].id)
  }
});

groups.append('circle')
.attr('r', 30);

groups.append('text')
.attr('x', 0)
.attr('y', 5)
.attr('text-anchor', 'middle')
.text((d) => d.id);

groups.append('text')
.attr('x', 0)
.attr('y', 50)
.attr('text-anchor', 'middle')
.attr('font-size', 10)
.text((d) => d.SOURCE);
circle {
  fill: white;
  stroke: red;
}

line {
  stroke:black;
}
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg width=1600 height=480></svg>

相关问题