0

I'm currently working on a rather large scaled vue projects that uses d3 (and quasar to be exact) and I'm having a bit of a hard time finding a good structure for it. I already wrote some code, but since I was neither familiar with vue nor with d3 in the beginning, I didn't consider some aspects that are now becoming problematic. I'd like to rework the code, to make it cleaner with a solid architecture in mind. Though because most of the "action" happens on the svg, I feel a bit lost with how to divide my components. If I'd just be using vue or basic JavaScript it would be easier, for example I would make the nodes and links as independent components that can be used in my map component. However in my case the nodes are svg elements, that I'm dynamically appending to the svg which raises several questions/ problems. To shortly explain how it works:

I have an update() function that performs a data join on my hierarchies descendants and appends new nodes to the svg for entering descendants. When a node is clicked it's children array is set to null and the nodes saved in their are instead appended to an array named _children. since the descendants() function recursively checks for children in every node and appends them to the descendants array, a clicked node's children that are now stored in _children are not joined to any DOM element.

It's based on some examples for expandable Trees, all of them working pretty much the same. I assume most of them are based on the same "original" Tree. You can find one of those examples here:

D3.js Zooming and panning a collapsible tree diagram

The logic of my code is very similar. In general I'm not perfectly happy with my approach because as far as I've understood in vue you use data binding to append elements, its not done manually (e.g document.getElementById("bla")). But all examples I've found, where svg is combined with vue in a way that is similar to my project, this approach was used instead of a reactive one, but that's just a general concern.

One other thing that is problematic is that I have different type of nodes, each with it's own way of appending, which results in the code becoming very long. I'd like to outsource some of it into other files to make script for the map not as bloated as it is at the moment.

Here is a small section of the code where I'm appending the nodes (you don't need to read through all of it, its just to show the extent:

  var nodeEnter = node
    .enter()
    .append("g")
    .classed("node", true)
    .classed("Tree", true)
    //.attr("class", "node")
    .attr("transform", (d) => {
      //console.log(
      //d.data.name + ": translate(" + source.x0 + "," + source.y0 + ")"
      //);
      //console.log("translate(" + source.x0 + "," + source.y0 + ")")
      return "translate(" + source.x0 + "," + source.y0 + ")";
    })
    .attr("id", function (d) {
      return "node" + d.id;
    });

   nodeEnter
    .append((d) => {
      return d.data.type === "loop"
        ? this.appendCircNode(d)
        : this.appendRectNode(d);
    })
    .attr("width", this.rectW)
    .attr("height", this.rectH)
    .attr("rx", 5)
    .attr("ry", 5)
    .style("cursor", "pointer")
    .call(this.drag)

    .attr("stroke", function (d) {
      var color = "";
      switch (d.data.type) {
        case "normal":
          color = "#00A289";
          break;
        case "machineLvl":
          color = "#1F82C0";
          break;
        case "loop":
          color = "#FF9C4B";
          break;
      }
      return color;
    })
    .attr("stroke-width", 2)
    .style("fill", function (d) {
      var color = "";
      switch (d.data.type) {
        case "normal":
          if (d._children) {
            color = "#00A289";
          } else {
            color = "#66C7B8";
          }
          break;
        case "machineLvl":
          color = "#1F82C0";
          break;
        case "loop":
          if (d._children) {
            color = "#FF9C4B";
          } else {
            color = "#FFB981";
          }
          break;
      }
      return color;
    })
    .on("click", this.click);

  var addButton = nodeEnter
    .append("g")
    .attr("class", "addButton")
    .attr("transform", function (d) {
      return "translate(" + rectW / 2 + "," + rectH + ")";
    })
    .attr("opacity", 0)
    .on("mouseover", function () {
      d3.select(this).attr("opacity", 1);
    })
    .on("mouseout", function () {
      d3.select(this).attr("opacity", 0);
    })
    .on("click", this.addNode);

  addButton
    .append("circle")
    .attr("cx", 0)
    .attr("cy", 0)
    .attr("r", 8)
    .attr("fill", "orange");

  addButton
    .append("path")
    .attr("d", function () {
      var path = d3.path();
      path.moveTo(0, 4);
      path.lineTo(0, -4);
      path.moveTo(-4, 0);
      path.lineTo(4, 0);

      return path;
    })
    .attr("stroke", "black");

  nodeEnter
    .append("text")
    .attr("dy", 20)
    .attr("dx", 30)
    .attr("text-anchor", "middle")
    .style("fill", "white")
    .text(function (d) {
      return d.data.name;
    });

  //append input field to svg as foreign Object that is rendered as HTML
  //start input field

  nodeEnter.append((d) => {
    let foreigner = document.createElementNS(
      "http://www.w3.org/2000/svg",
      "foreignObject"
    );
    foreigner.setAttributeNS(null, "x", 70);
    foreigner.setAttributeNS(null, "y", 15);
    foreigner.setAttributeNS(null, "width", "50px");
    foreigner.setAttributeNS(null, "height", "20px");

    let txt = document.createElement("input");
    txt.id = d.id;
    txt.classList.add("inputDuration");
    txt.style.border = 0;
    txt.style.outline = 0;
    txt.style.backgroundColor = "transparent";
    txt.style.height = "10px";
    txt.style.fontSize = "10px";
    txt.value = d.data.Duration;
    txt.onblur = (event) => {
      let selectedNode = this.nodes.find((n) => n.id == event.target.id);
      this.updateTime(
        selectedNode,
        selectedNode.data.startTime,
        parseInt(event.target.value)
      );
      this.updateInputFields(this.nodes);
    };
    foreigner.appendChild(txt);
    return foreigner;
  });

  nodeEnter.append((d) => {
    let foreigner = document.createElementNS(
      "http://www.w3.org/2000/svg",
      "foreignObject"
    );
    foreigner.setAttributeNS(null, "x", 70);
    foreigner.setAttributeNS(null, "y", -5);
    foreigner.setAttributeNS(null, "width", "50px");
    foreigner.setAttributeNS(null, "height", "20px");

    let txt = document.createElement("input");
    txt.id = d.id;
    txt.classList.add("inputStart");
    txt.style.border = 0;
    txt.style.outline = 0;
    txt.style.backgroundColor = "transparent";
    txt.style.height = "10px";
    txt.style.fontSize = "10px";
    txt.value = d.data.startTime;
    txt.onblur = (event) => {
      let selectedNode = this.nodes.find((n) => n.id == event.target.id);
      this.updateTime(selectedNode, parseInt(event.target.value));
      this.updateInputFields(this.nodes);
    };

    foreigner.appendChild(txt);
    return foreigner;
  });

As you can see the code gets very long and I would like to divide some of it into other files. I don't really know if it makes sense to have an svg element be there own component, besides that so far I couldn't even get it working. My test Node looked something like this:

<template>
 <svg:rect width="width" height="height">
  ....(other stuff like svg:text to show the name)
 <svg:rect/>
<template/>

export default defineCOmponent{
 name: testNode
 props: {some props}

and then I used it in my chart component:

<template>
 <svg>
  <testNode/>
 <svg>
<template/>

maybe I could put the rectangle in my custom component in its own svg, but that would make a lot of very nested svg's and seems not really preferable... also its not even close to what I want, which is then dynamically adding the component to the svg in my update method. I looked through some posts that tackled doing that, but it seemed rather complicated. Then I thought about not defining my nodes in a component, but rather outsourcing them into a method:

export default function defaultNode({height, width, start, duration...}) {
 let detached = d3
        .create("svg:rect")
        .attr("width", this.rectW)
        .attr("height", this.rectH)
        .attr("rx", 5)
        .attr("ry", 5);
 //appending other stuff

 return detached.node();

I know that is a lot of input, I certainly don't want you to give me a perfect, finished, already completely written out code solution. I'm just lacking people who have experience with vue and could give me some best practice advice. If you could give me any I'd be very grateful. Also please excuse my poor English and my way too frequently occurring spelling mistakes...

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129

0 Answers0