Рендеринг родословной с помощью d3 или cytoscape - PullRequest
6 голосов
/ 29 апреля 2020

У меня проблемы с генерацией красивого генеалогического дерева с Javascript.

Требования:

  • Каждый ребенок должен быть связан с двумя родителями в дереве, а не с одним, как на некоторых графиках
  • Мне нравится, когда супруги рядом друг друга в дереве (одинаковая вертикальная позиция)
  • Я бы хотел организовать узлы по поколениям по вертикали, чтобы вы могли сразу увидеть людей, родившихся в одно и то же десятилетие.
  • One человек может иметь несколько супругов с течением времени, и дети с каждым из них
  • Родители и дети могут свободно добавляться в дереве, поэтому не просто "проследите родословную от одного человека вверх"

То, что я пробовал, было ближе всего к этому:

  1. Cytoscape JS с Dagre в качестве механизма компоновки и стиль кривой: такси включены ребра. Family tree

    (Диаграмма со случайными данными. Solid строки - отношения родитель-ребенок, пунктирные линии - супруги)

    Проблема в том, что супруги не выровнены друг с другом. Dagre исторически поддерживал «ранг» в качестве параметра для узлов, что означает, что вы можете заставить некоторые узлы иметь указанную высоту c (если хотите, подумайте об этом как о «поколении»). К сожалению, больше не работает , и ответственный разработчик больше не работает над проектом . Это хорошо решило бы мою проблему.

Другие вещи, которые я пробовал, но не смогли:

  1. Понижение версии Dagre до более старой версии с поддержкой ранг?

    Не получил ранг для работы с ЛЮБОЙ версией dagre.

  2. D3 с dagre-d3

    Та же проблема, что и выше, поскольку dagre-d3 является модифицированной версией dagre Это означает, что он не поддерживает ранжирование в поколениях.

  3. Семейное древо yFiles Демонстрация выглядит великолепно, но коммерчески. Стоимость для моих целей (хотел бы, чтобы кто-то создал свое семейное древо) составляет 26 000 долларов США (!?!) За одну лицензию разработчика. Очевидно, не приемлемо.

    yFiles family tree

Мой вопрос

Возможно ли получить выровнять узлы в моем графе cytoscape / dagre по вертикали, как я описал выше?

Если нет, я готов попробовать другие библиотеки и другие алгоритмы компоновки.

Я ищу рабочий пример, похожий на решение yFiles, но с использованием инструментов с открытым исходным кодом.

1 Ответ

5 голосов
/ 02 мая 2020

Прежде чем вы углубитесь в мой ответ :), возможно, вы захотите проверить WebCola , с которым я столкнулся при исследовании ориентированных графов с принудительной силой:

JavaScript макет на основе ограничений для высококачественной визуализации и исследования графиков с использованием D3. js и других веб-библиотек графики.

Позволяет указывать x и ограничения на размерность y , как я сделал для измерения y в моем примере ниже. Я сам этим не пользовался, но, похоже, очень хорошо подходит под ваши требования. И он работает с CytoScape, так что вы можете опираться на то, что вы уже сделали ...

Применение размерных ограничений к силовому графу:

Поскольку вы не имеете дело со строгой иерархией (например, вы не начинаете с одного потомка и не идете вверх), одним из подходов будет использование D3 Force Directed Graph с узлом для представления каждого член семьи Это обеспечит дополнительную гибкость по сравнению с линейной иерархией.

Требуемая компоновка поколений может быть достигнута путем ограничения узлов фиксированными точками на оси y.

Вот подтверждение концепции :

  • Три поколения членов семьи
  • Несколько супругов представлены Алисой и Бобом / Бобом и Кэрол
  • Дэвидом является потомком Алисы и Боба
  • Джеймс является потомком Боба и Кэрол
  • Генерация узла (или координата y), рассчитанная по assignGeneration на основе связанных дочерних, партнерских и родительских узлов
  • Координата узла X обрабатывается d3, что, я думаю, будет более надежным, чем попытка вручную назначить каждому узлу позицию по оси x
  • Основа c Стиль:
    • Партнерские ссылки - коралловые
    • Дочерние ссылки - светло-голубые
    • Родственные ссылки - светло-зеленые

Надеюсь, здесь достаточно, чтобы вы решили будь то виаб Ле подход. Настройка презентационных вертикальных / горизонтальных связей между родителями и детьми должна быть довольно простой, но может потребовать немного экспериментов.

Корректировки (в зависимости от объема данных и связей узлов и т. Д. c.), Вероятно, потребуются применить к simulation - опять же, понадобятся некоторые эксперименты для генерации оптимального макета. Более подробная информация о различных силах доступна здесь .

<!DOCTYPE html>
<html>

<head>
  <style>
svg {
  border: 1px solid gray;
}

.partner_link {
  stroke: lightcoral;
}

.child_link {
  stroke: lightskyblue;
}

.sibling_link {
  stroke: lightseagreen;
}
  </style>
</head>

<body>
  <script src="https://d3js.org/d3.v5.min.js"></script>
  <script type="text/javascript">

var nodeData = [{
  id: 1,
  name: 'Alice',
  partners: [2],
  children: [4]
}, {
  id: 2,
  name: 'Bob',
  partners: [1, 3],
  children: [4,10]
}, {
  id: 3,
  name: 'Carol',
  partners: [2],
  children: [10]
}, {
  id: 4,
  name: 'David',
  partners: [7],
  children: [8]
}, {
  id: 5,
  name: 'Emily',
  partners: [6],
  children: [7, 9]
}, {
  id: 6,
  name: 'Fred',
  partners: [5],
  children: [7, 9]
}, {
  id: 7,
  name: 'Grace',
  partners: [4],
  children: [8]
}, {
  id: 8,
  name: 'Harry',
  partners: null,
  children: null
}, {
  id: 9,
  name: 'Imogen',
  partners: null,
  children: null
}, {
  id: 10,
  name: 'James',
  partners: null,
  children: null
}];

var linkData = [];

nodeData.forEach((node, index) => {
  if (node.partners) {
    node.partners.forEach(partnerID => {
      linkData.push({ source: node, target: nodeData.find(partnerNode => partnerNode.id === partnerID), relationship: 'Partner' });
    })
  }
  if (node.children) {
    node.children.forEach(childID => {
      const childNode = nodeData.find(childNode => childNode.id === childID);
      if (node.children.length > 1) {
        childNode.siblings = node.children.slice(0, node.children.indexOf(childNode.id)).concat(node.children.slice(node.children.indexOf(childNode.id) + 1, node.children.length));
        childNode.siblings.forEach(siblingID => {
          linkData.push({ source: childNode, target: nodeData.find(siblingNode => siblingNode.id === siblingID), relationship: 'Sibling' });
        })
      }
      linkData.push({ source: node, target: childNode, relationship: 'Child' });
    })
  }
});

linkData.map(d => Object.create(d));

assignGeneration(nodeData, nodeData, 0);

var w = 500,
  h = 500;

var svg = d3.select("body")
  .append("svg")
  .attr("width", w)
  .attr("height", h);

var color = d3.scaleOrdinal(d3.schemeCategory10);

var rowScale = d3.scalePoint()
  .domain(dataRange(nodeData, 'generation'))
  .range([0, h - 50])
  .padding(0.5);

var simulation = d3.forceSimulation(nodeData)
  .force('link', d3.forceLink().links(linkData).distance(50).strength(1))
  .force("y", d3.forceY(function (d) {
    return rowScale(d.generation)
  }))
  .force("charge", d3.forceManyBody().strength(-300).distanceMin(60).distanceMax(120))
  .force("center", d3.forceCenter(w / 2, h / 2));

var links = svg.append("g")
  .attr("stroke", "#999")
  .attr("stroke-opacity", 0.8)
  .selectAll("line")
  .data(linkData)
  .join("line")
  .attr("stroke-width", 1)
  .attr("class", d => {
    return d.relationship.toLowerCase() + '_link';
  });;

var nodes = svg.append("g")
  .attr("class", "nodes")
  .selectAll("g")
  .data(nodeData)
  .enter().append("g")

var circles = nodes.append("circle")
  .attr("r", 5)
  .attr("fill", function (d) {
    return color(d.generation)
  });

var nodeLabels = nodes.append("text")
  .text(function (d) {
    return d.name;
  }).attr('x', 12)
  .attr('y', 20);

var linkLabels = links.append("text")
  .text(function (d) {
    return d.relationship;
  }).attr('x', 12)
  .attr('y', 20);

/*
// Y Axis - useful for testing:
var yAxis = d3.axisLeft(rowScale)(svg.append("g").attr("transform", "translate(30,0)"));
*/

simulation.on("tick", function () {
  links
    .attr("x1", d => {
      return d.source.x;
    })
    .attr("y1", d => {
      return rowScale(d.source.generation);
    })
    .attr("x2", d => {
      return d.target.x;
    })
    .attr("y2", d => {
      return rowScale(d.target.generation);
    });
  nodes.attr("transform", function (d) {
    return "translate(" + d.x + "," + rowScale(d.generation) + ")";
  })
});

function dataRange(records, field) {
  var min = d3.min(records.map(record => parseInt(record[field], 10)));
  var max = d3.max(records.map(record => parseInt(record[field], 10)));
  return d3.range(min, max + 1);
};

function assignGeneration(nodes, generationNodes, generationCount) {
  const childNodes = [];
  generationNodes.forEach(function (node) {
    if (node.children) {
      // Node has children
      node.generation = generationCount + 1;
      node.children.forEach(childID => {
        if (!childNodes.find(childNode => childNode.id === childID)) {
          childNodes.push(generationNodes.find(childNode => childNode.id === childID));
        }
      })
    } else {
      if (node.partners) {
        node.partners.forEach(partnerID => {
          if (generationNodes.find(partnerNode => partnerNode.id === partnerID && partnerNode.children)) {
            // Node has partner with children
            node.generation = generationCount + 1;
          }
        })
      } else {
        // Use generation of parent + 1
        const parent = nodes.find(parentNode => parentNode.children && parentNode.children.indexOf(node.id) !== -1);
        node.generation = parent.generation + 1;
      }
    }
  });
  if (childNodes.length > 0) {
    return assignGeneration(nodes, childNodes, generationCount += 1);
  } else {
    nodes.filter(node => !node.generation).forEach(function (node) {
      node.generation = generationCount + 1;
    });
    return nodes;
  }
}

  </script>
</body>

</html>
...