Отображение сетевого графика D3 по горизонтали без сворачивания

Я создаю сетевой граф, который соединяет узлы путями.

Мои требования просты - график сети должен быть либо вертикальным, либо горизонтальным. без складывания.

Пока что я создал график, отображающий диаграмму в горизонтальном формате.

Однако график отображается только в одну строку (без сворачивания), если набор узлов очень ограничен (я пробовал несколько пробных ошибок forceManyBody().strength() и forceLink(links).distance(), чтобы как-то заставить его работать)

А вот для большего нет. узлов, граф складывается следующим образом:

Некоторые варианты d3.forceManyBody().strength(-600) дают мне одну строку, но с обратным порядком ссылок, например:

Здесь круг 5050 должен быть первым кругом, но он идет в конце.

Итак, мои вопросы --

  1. Как правильно найти forceManyBody().strength() и forceLink(links).distance() на основе узлов, чтобы у меня была одна строка
  2. Почему первый круг приходит наконец?

Я не возражаю, если мне придется прокручивать, чтобы просмотреть все узлы (может быть, 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>

Возможно, вы только что выбрали этот пример, чтобы продемонстрировать свою проблему (и если это так, не обращайте внимания на остальную часть этого комментария): масштаб (временные связи между узлами, кажется, имеют длину, пропорциональную их значению) и размещать свои узлы/ссылки в соответствии с этим?

mgraham 11.11.2022 11:35

@mgraham - я просто хочу, чтобы круги указывали друг на друга, показывая направление движения и время, которое потребовалось для этого движения. Можете ли вы помочь мне с существующим примером масштабного графика?

Rahul Bhardwaj 11.11.2022 12:14

То, что я предлагаю, не будет сетью. Взгляните на это --> d3indepth.com/scales - вы определяете масштаб и позиции, кумулятивно складывая информацию о времени, размещаете узлы в позициях и рисуете связи между ними. ( observablehq.com/@d3/d3-scalelinear содержит больше информации, но, вероятно, слишком подробно описывает то, что вы, кажется, хотите сделать)

mgraham 11.11.2022 12:52
[JS за 1 час] - 9. Асинхронный
[JS за 1 час] - 9. Асинхронный
JavaScript является однопоточным, то есть он может обрабатывать только одну задачу за раз. Для обработки длительных задач, таких как сетевые запросы,...
Подъем в javascript
Подъем в javascript
Hoisting - это поведение в JavaScript, при котором переменные и объявления функций автоматически "перемещаются" в верхнюю часть соответствующих...
Как использовать API парсинга квитанций с помощью JavaScript за 5 минут?
Как использовать API парсинга квитанций с помощью JavaScript за 5 минут?
В этом руководстве вы узнаете, как использовать API парсинга квитанций за 5 минут с помощью JavaScript. Eden AI предоставляет простой и удобный для...
Хук useOnClickOutside в ReactJS
Хук useOnClickOutside в ReactJS
Как разработчик ReactJS, вы, возможно, сталкивались с ситуацией, когда вам нужно закрыть модальное или выпадающее меню, когда кто-то щелкает за его...
Хуки (часть-2) - useEffect
Хуки (часть-2) - useEffect
Хук useEffect - один из самых мощных и универсальных инструментов в арсенале разработчика React. Он позволяет вам управлять побочными эффектами в...
Простое руководство по тестированию взаимодействия с пользователем с помощью библиотеки тестирования React
Простое руководство по тестированию взаимодействия с пользователем с помощью библиотеки тестирования React
В предыдущем посте я показал вам на примерах, как писать базовые тесты в React. Важнейшей частью пользовательского интерфейса приложений является...
0
3
80
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Как упоминалось в комментариях. Ваш вариант использования очень прост и может быть воссоздан с использованием масштабов и форм. Я использовал линейную шкалу и использовал индексы узлов в качестве домена для шкалы, так как временные интервалы между узлами различаются на несколько порядков.

Каждый узел содержится в группе для упрощения относительного позиционирования круга, текста и линии.

Поскольку количество ребер имеет длину узлов - 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.info({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>

@Rahul Bhardwaj, пожалуйста, рассмотрите мой ответ как возможное решение, поскольку он соответствует указанным требованиям в вашем вопросе.

wasserholz 28.11.2022 08:26

Как я могу дать тебе награду? Извините, я отсутствовал и не мог проверить ответ вовремя.

Rahul Bhardwaj 03.12.2022 16:56

Спасибо, что проголосовали и приняли мой ответ. К сожалению, срок действия бонуса уже истек.

wasserholz 03.12.2022 20:15

Другие вопросы по теме