thelonelyCoder
thelonelyCoder

Reputation: 322

How to make a three graph dynamic in HTML5, Css3 and Vanilla js?

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.

Upvotes: 0

Views: 389

Answers (1)

Ruben Helsloot
Ruben Helsloot

Reputation: 13129

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>

Upvotes: 2

Related Questions