0

I have constructed a tree graph using html5, css3 but the nodes are static. Now I wanna make that graph dynamic. Here dynamic means suppose the node count increases and there are multiple children, now the graph will be generated with the new nodes and children.

1 Answer 1

2

This is just an example using vanilla JavaScript. I've taken a lot of inspiration from d3, including how to store the data as a self-referencing tree of nodes and how to traverse the tree breadth-first.

I've tried to comment the code as well as possible, I hope this gives you some inspiration. I'd try to improve the positioning of the labels a bit, and/or increase the margins so they're better visible.

const data = [{
    id: 0,
    label: "Command Sequence Starting",
  },
  {
    id: 1,
    parents: [0],
    label: "W_SCMadl_refresh",
    status: "done",
  },
  {
    id: 2,
    parents: [1],
    label: "W_adl_photo",
    status: "done",
  },
  {
    id: 3,
    parents: [2],
    label: "W_adl_collect",
    status: "done",
  },
  {
    id: 4,
    parents: [3],
    label: "W_adl_collect_cr",
    status: "done",
  },
  {
    id: 5,
    parents: [4],
    label: "W_adl_sync",
    status: "aborted",
  },
  {
    id: 6,
    parents: [5],
    label: "W_adl_attach",
    status: "waiting",
  },
  {
    id: 7,
    parents: [6],
    label: "W_adl_attach",
    status: "waiting",
  },
  {
    id: 8,
    parents: [7],
    label: "W_adl_publish",
    status: "waiting",
  },
  {
    id: 9,
    parents: [8],
    label: "W_adl_ds_ws",
    status: "waiting",
  },
  {
    id: 10,
    parents: [9],
    label: "W64_Shared_Preq_mkdir",
    status: "waiting",
  },
  {
    id: 11,
    parents: [10, 12],
    label: "W64_mkCopyPreq",
    status: "waiting",
  },
  {
    id: 12,
    parents: [0],
    label: "WIN64_MCCMon",
    status: "done",
  },
];

// Make the data array a self-referencing tree, where each node has pointers to their
// parents and their children nodes
data
  .filter(d => d.parents !== undefined)
  .forEach(d => {
    d.parents = data.filter(p => d.parents.includes(p.id));

    d.parents.forEach(p => {
      if (p.children === undefined) {
        p.children = [];
      }
      p.children.push(d);
    });
  });

const root = data.find(d => d.parents === undefined);

// Breadth first traversal of the tree, excuting `fn` for every node
const forEach = (root, fn) => {
  const stack = [root];
  while (stack.length) {
    const current = stack.shift();
    if (current.children) {
      stack.push(...current.children);
    }

    fn(current);
  }
};

const svg = document.querySelector(".mv-sequence svg");

const margin = {
  top: 20,
  bottom: 20,
  right: 20,
  left: 20,
};
const width = +svg.getAttribute("width") - margin.left - margin.right;
const stepHeight = 40;
const namespace = "http://www.w3.org/2000/svg";

const gContainer = document.createElementNS(namespace, "g");
gContainer.setAttribute("transform", `translate(${margin.left},${margin.top})`);
svg.appendChild(gContainer);

const linksContainer = document.createElementNS(namespace, "g");
gContainer.appendChild(linksContainer);

const nodesContainer = document.createElementNS(namespace, "g");
gContainer.appendChild(nodesContainer);


// Give node a level. First complete this loop, then start drawing, because we want to
// be robust against not all parents having a level yet
forEach(
  root,
  d => {
    if (d === root) {
      d.level = 0;
      return;
    }
    d.level = Math.max(...d.parents.map(p => p.level)) + 1;
  }
);

forEach(
  root,
  d => {
    // Position the node based on the number of siblings.
    const siblings = data.filter(n => n.level === d.level);

    // If the node is an only child. The root should be in the centre,
    // any other node should be in the average of it's parents
    if (siblings.length === 1) {
      if (d.parents === undefined) {
        d.x = width / 2;
      } else {
        d.x = d.parents.map(p => p.x).reduce((s, v) => s + v, 0) / d.parents.length;
      }
      return;
    }

    // Otherwise, divide the space evenly for all sibling nodes
    const siblingIndex = siblings.indexOf(d);
    const stepWidth = width / (siblings.length - 1);
    if (siblings.length % 2 === 0) {
      // Even number of siblings
      d.x = stepWidth * siblingIndex;
    } else {
      // Odd number of siblings, the center one must be in the middle
      d.x = width / 2 + stepWidth * (siblingIndex - (siblings.length - 1) / 2);
    }
  }
);

forEach(
  root,
  d => {
    // Append a circle and `text` for all new nodes
    d.y = d.level * stepHeight;
    const nodeContainer = document.createElementNS(namespace, "g");
    nodeContainer.setAttribute("transform", `translate(${d.x}, ${d.y})`);
    nodeContainer.classList.add("mv-command", d.status);
    nodesContainer.appendChild(nodeContainer);

    const circle = document.createElementNS(namespace, "circle");
    circle.setAttribute("r", stepHeight / 4);
    nodeContainer.appendChild(circle);

    const label = document.createElementNS(namespace, "text");
    label.setAttribute("dx", stepHeight / 4 + 5);
    label.textContent = d.label;
    nodeContainer.appendChild(label);

    // Append a link from every parent to this node
    (d.parents || []).forEach(p => {
      const link = document.createElementNS(namespace, "path");

      let path = `M${p.x} ${p.y}`;
      let dx = d.x - p.x;
      let dy = d.y - p.y;

      if (dy > stepHeight) {
        // Move down to the level of the child node
        path += `v${dy - stepHeight}`;
        dy = stepHeight;
      }

      path += `s0 ${dy / 2}, ${dx / 2} ${dy / 2}`;
      path += `s${dx / 2} 0, ${dx / 2} ${dy / 2}`;

      link.setAttribute("d", path)
      linksContainer.appendChild(link);
    })
  }
);

// Finally, set the height to fit perfectly
svg.setAttribute("height", Math.max(...data.map(d => d.level)) * stepHeight + margin.top + margin.bottom);
.mv-command.done {
  fill: #477738;
}

.mv-command.aborted {
  fill: #844138;
}

.mv-command.waiting {
  fill: #808080;
}

.mv-command.disabled {
  fill: #80808080;
}

.mv-command.running {
  fill: #005686;
  animation: mymove 2s infinite;
}

.mv-command>text {
  dominant-baseline: middle;
  font-size: 12px;
}

path {
  fill: none;
  stroke: darkgreen;
  stroke-width: 3px;
}
<div class="mv-sequence">
  <svg width="200"></svg>
</div>

Sign up to request clarification or add additional context in comments.

1 Comment

Really liked the way you demonstrate it 🙌🙌...amazing @Reben_Helsloot

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.