最近查找js相关的鱼骨图组件,找了半天都没有合适的,自己参考gojs官网demo简单的实现了下,效果如下。

废话少说,直接上代码。

引入gojs

npm install gojs --save

整合到vue

fishbone.vue

<template>
  <div>
    <div id="gos" class="lean"></div>
    <el-button @click="layoutFishbone">layoutFishbone</el-button>
    <el-button @click="layoutBranching">layoutBranching</el-button>
    <el-button @click="layoutNormal">layoutNormal</el-button>
  </div>
</template>

<script>
import go from 'gojs'
import { FishboneLayout, FishboneLink } from '../assets/FishboneLayout.js';
export default {
  data () {
    return {
      diagram: '',
      json: {
        'text': 'Incorrect Deliveries', 'size': 18, 'weight': 'Bold', 'causes': [
          {
            'text': 'Skills', 'size': 14, 'weight': 'Bold', 'causes': [
              {
                'text': 'knowledge', 'weight': 'Bold', 'causes': [
                  {
                    'text': 'procedures', 'causes': [
                      { 'text': 'documentation' }
                    ]
                  },
                  { 'text': 'products' }
                ]
              },
              { 'text': 'literacy', 'weight': 'Bold' }
            ]
          },
          {
            'text': 'Procedures', 'size': 14, 'weight': 'Bold', 'causes': [
              {
                'text': 'manual', 'weight': 'Bold', 'causes': [
                  { 'text': 'consistency' }
                ]
              },
              {
                'text': 'automated', 'weight': 'Bold', 'causes': [
                  { 'text': 'correctness' },
                  { 'text': 'reliability' }
                ]
              }
            ]
          },
          {
            'text': 'Communication', 'size': 14, 'weight': 'Bold', 'causes': [
              { 'text': 'ambiguity', 'weight': 'Bold' },
              {
                'text': 'sales staff', 'weight': 'Bold', 'causes': [
                  {
                    'text': 'order details', 'causes': [
                      { 'text': 'lack of knowledge' }
                    ]
                  }
                ]
              },
              {
                'text': 'telephone orders', 'weight': 'Bold', 'causes': [
                  { 'text': 'lack of information' }
                ]
              },
              {
                'text': 'picking slips', 'weight': 'Bold', 'causes': [
                  { 'text': 'details' },
                  { 'text': 'legibility' }
                ]
              }
            ]
          },
          {
            'text': 'Transport', 'size': 14, 'weight': 'Bold', 'causes': [
              {
                'text': 'information', 'weight': 'Bold', 'causes': [
                  { 'text': 'incorrect person' },
                  {
                    'text': 'incorrect addresses', 'causes': [
                      {
                        'text': 'customer data base', 'causes': [
                          { 'text': 'not up-to-date' },
                          { 'text': 'incorrect program' }
                        ]
                      }
                    ]
                  },
                  { 'text': 'incorrect dept' }
                ]
              },
              {
                'text': 'carriers', 'weight': 'Bold', 'causes': [
                  { 'text': 'efficiency' },
                  { 'text': 'methods' }
                ]
              }
            ]
          }
        ]
      }
    }
  },
  mounted () {
    const $ = go.GraphObject.make;
    let _this = this;
    this.diagram = $(go.Diagram, 'gos', { isReadOnly: false })
    this.diagram.nodeTemplate =
      $(go.Node,
        $(go.TextBlock,
          new go.Binding('text'),
          new go.Binding('font', '', _this.convertFont))
      );

    this.diagram.linkTemplateMap.add('normal',
      $(go.Link,
        { routing: go.Link.Orthogonal, corner: 4 },
        $(go.Shape)
      ));

    this.diagram.linkTemplateMap.add('fishbone',
      $(FishboneLink, // defined above
        $(go.Shape)
      ));

    const nodeDataArray = [];
    _this.walkJson(_this.json, nodeDataArray);
    this.diagram.model = new go.TreeModel(nodeDataArray);
    this.layoutFishbone();
  },
  methods: {
    convertFont (data) {
      let size = data.size;
      if (size === undefined)
        size = 13;
      let weight = data.weight;
      if (weight === undefined)
        weight = '';
      return weight + ' ' + size + 'px sans-serif';
    },
    walkJson (obj, arr) {
      const key = arr.length;
      obj.key = key;
      arr.push(obj);
      const children = obj.causes;
      if (children) {
        for (let i = 0; i < children.length; i++) {
          const o = children[i];
          o.parent = key;
          this.walkJson(o, arr);
        }
      }
    },
    layoutFishbone () {
      this.diagram.startTransaction('fishbone layout');
      this.diagram.linkTemplate = this.diagram.linkTemplateMap.getValue('fishbone');
      this.diagram.layout = go.GraphObject.make(FishboneLayout, {
        angle: 180,
        layerSpacing: 10,
        nodeSpacing: 20,
        rowSpacing: 10
      });
      this.diagram.commitTransaction('fishbone layout');
    },
    layoutBranching () {
      this.diagram.startTransaction('branching layout');
      this.diagram.linkTemplate = this.diagram.linkTemplateMap.getValue('normal');
      this.diagram.layout = go.GraphObject.make(go.TreeLayout, {
        angle: 180,
        layerSpacing: 20,
        alignment: go.TreeLayout.AlignmentBusBranching
      });
      this.diagram.commitTransaction('branching layout');
    },
    layoutNormal () {
      this.diagram.startTransaction('normal layout');
      this.diagram.linkTemplate = this.diagram.linkTemplateMap.getValue('normal');
      this.diagram.layout = go.GraphObject.make(go.TreeLayout, {
        angle: 180,
        breadthLimit: 1000,
        alignment: go.TreeLayout.AlignmentStart
      });
      this.diagram.commitTransaction('normal layout');
    }
  }
}
</script>

<style scoped>
.lean {
  height: 500px;
  width: 90%;
  border: 1px solid black;
  background-color: #dae4e4;
}
</style>

FishboneLayout.js


import go from "gojs";

/**
 * FishboneLayout is a custom {@link Layout} derived from {@link TreeLayout} for creating "fishbone" diagrams.
 * A fishbone diagram also requires a {@link Link} class that implements custom routing, {@link FishboneLink}.
 *
 * This only works for angle === 0 or angle === 180.
 *
 * This layout assumes Links are automatically routed in the way needed by fishbone diagrams,
 * by using the FishboneLink class instead of go.Link.
 *
 * If you want to experiment with this extension, try the <a href="../../extensionsTS/Fishbone.html">Fishbone Layout</a> sample.
 * @category Layout Extension
 */
export class FishboneLayout extends go.TreeLayout {
  /**
   * Constructs a FishboneLayout and sets the following properties:
   *   - {@link #alignment} = {@link TreeLayout.AlignmentBusBranching}
   *   - {@link #setsPortSpot} = false
   *   - {@link #setsChildPortSpot} = false
   */
  constructor() {
    super();
    this.alignment = go.TreeLayout.AlignmentBusBranching;
    this.setsPortSpot = false;
    this.setsChildPortSpot = false;
  }
  /**
   * Create and initialize a {@link LayoutNetwork} with the given nodes and links.
   * This override creates dummy vertexes, when necessary, to allow for proper positioning within the fishbone.
   * @param {Diagram|Group|Iterable.<Part>} coll A {@link Diagram} or a {@link Group} or a collection of {@link Part}s.
   * @return {LayoutNetwork}
   */
  makeNetwork(coll) {
    // assert(this.angle === 0 || this.angle === 180);
    // assert(this.alignment === go.TreeLayout.AlignmentBusBranching);
    // assert(this.path !== go.TreeLayout.PathSource);
    // call base method for standard behavior
    const net = super.makeNetwork(coll);
    // make a copy of the collection of TreeVertexes
    // because we will be modifying the TreeNetwork.vertexes collection in the loop
    const verts = new go.List().addAll(net.vertexes.iterator);
    verts.each(function(v) {
      // ignore leaves of tree
      if (v.destinationEdges.count === 0) return;
      if (v.destinationEdges.count % 2 === 1) {
        // if there's an odd number of real children, add two dummies
        const dummy = net.createVertex();
        dummy.bounds = new go.Rect();
        dummy.focus = new go.Point();
        net.addVertex(dummy);
        net.linkVertexes(v, dummy, null);
      }
      // make sure there's an odd number of children, including at least one dummy;
      // commitNodes will move the parent node to where this dummy child node is placed
      const dummy2 = net.createVertex();
      dummy2.bounds = v.bounds;
      dummy2.focus = v.focus;
      net.addVertex(dummy2);
      net.linkVertexes(v, dummy2, null);
    });
    return net;
  }
  /**
   * Add a direction property to each vertex and modify {@link TreeVertex#layerSpacing}.
   */
  assignTreeVertexValues(v) {
    super.assignTreeVertexValues(v);
    v["_direction"] = 0; // add this property to each TreeVertex
    if (v.parent !== null) {
      // The parent node will be moved to where the last dummy will be;
      // reduce the space to account for the future hole.
      if (v.angle === 0 || v.angle === 180) {
        v.layerSpacing -= v.bounds.width;
      } else {
        v.layerSpacing -= v.bounds.height;
      }
    }
  }
  /**
   * Assigns {@link Link#fromSpot}s and {@link Link#toSpot}s based on branching and angle
   * and moves vertexes based on dummy locations.
   */
  commitNodes() {
    if (this.network === null) return;
    // vertex Angle is set by BusBranching "inheritance";
    // assign spots assuming overall Angle === 0 or 180
    // and links are always connecting horizontal with vertical
    this.network.edges.each(function(e) {
      const link = e.link;
      if (link === null) return;
      link.fromSpot = go.Spot.None;
      link.toSpot = go.Spot.None;
      const v = e.fromVertex;
      const w = e.toVertex;
      if (v.angle === 0) {
        link.fromSpot = go.Spot.Left;
      } else if (v.angle === 180) {
        link.fromSpot = go.Spot.Right;
      }
      if (w.angle === 0) {
        link.toSpot = go.Spot.Left;
      } else if (w.angle === 180) {
        link.toSpot = go.Spot.Right;
      }
    });
    // move the parent node to the location of the last dummy
    let vit = this.network.vertexes.iterator;
    while (vit.next()) {
      const v = vit.value;
      const len = v.children.length;
      if (len === 0) continue; // ignore leaf nodes
      if (v.parent === null) continue; // don't move root node
      const dummy2 = v.children[len - 1];
      v.centerX = dummy2.centerX;
      v.centerY = dummy2.centerY;
    }
    const layout = this;
    vit = this.network.vertexes.iterator;
    while (vit.next()) {
      const v = vit.value;
      if (v.parent === null) {
        layout.shift(v);
      }
    }
    // now actually change the Node.location of all nodes
    super.commitNodes();
  }
  /**
   * This override stops links from being committed since the work is done by the {@link FishboneLink} class.
   */
  commitLinks() {}
  /**
   * Shifts subtrees within the fishbone based on angle and node spacing.
   */
  shift(v) {
    const p = v.parent;
    if (p !== null && (v.angle === 90 || v.angle === 270)) {
      const g = p.parent;
      if (g !== null) {
        const shift = v.nodeSpacing;
        if (g["_direction"] > 0) {
          if (g.angle === 90) {
            if (p.angle === 0) {
              v["_direction"] = 1;
              if (v.angle === 270) this.shiftAll(2, -shift, p, v);
            } else if (p.angle === 180) {
              v["_direction"] = -1;
              if (v.angle === 90) this.shiftAll(-2, shift, p, v);
            }
          } else if (g.angle === 270) {
            if (p.angle === 0) {
              v["_direction"] = 1;
              if (v.angle === 90) this.shiftAll(2, -shift, p, v);
            } else if (p.angle === 180) {
              v["_direction"] = -1;
              if (v.angle === 270) this.shiftAll(-2, shift, p, v);
            }
          }
        } else if (g["_direction"] < 0) {
          if (g.angle === 90) {
            if (p.angle === 0) {
              v["_direction"] = 1;
              if (v.angle === 90) this.shiftAll(2, -shift, p, v);
            } else if (p.angle === 180) {
              v["_direction"] = -1;
              if (v.angle === 270) this.shiftAll(-2, shift, p, v);
            }
          } else if (g.angle === 270) {
            if (p.angle === 0) {
              v["_direction"] = 1;
              if (v.angle === 270) this.shiftAll(2, -shift, p, v);
            } else if (p.angle === 180) {
              v["_direction"] = -1;
              if (v.angle === 90) this.shiftAll(-2, shift, p, v);
            }
          }
        }
      } else {
        // g === null: V is a child of the tree ROOT
        const dir = p.angle === 0 ? 1 : -1;
        v["_direction"] = dir;
        this.shiftAll(dir, 0, p, v);
      }
    }
    for (let i = 0; i < v.children.length; i++) {
      const c = v.children[i];
      this.shift(c);
    }
  }
  /**
   * Shifts a subtree.
   */
  shiftAll(direction, absolute, root, v) {
    // assert(root.angle === 0 || root.angle === 180);
    let locx = v.centerX;
    locx += (direction * Math.abs(root.centerY - v.centerY)) / 2;
    locx += absolute;
    v.centerX = locx;
    for (let i = 0; i < v.children.length; i++) {
      const c = v.children[i];
      this.shiftAll(direction, absolute, root, c);
    }
  }
}
/**
 * Custom {@link Link} class for {@link FishboneLayout}.
 * @category Part Extension
 */
export class FishboneLink extends go.Link {
  computeAdjusting() {
    return this.adjusting;
  }
  /**
   * Determines the points for this link based on spots and maintains horizontal lines.
   */
  computePoints() {
    const result = super.computePoints();
    if (result) {
      // insert middle point to maintain horizontal lines
      if (
        this.fromSpot.equals(go.Spot.Right) ||
        this.fromSpot.equals(go.Spot.Left)
      ) {
        let p1;
        // deal with root node being on the "wrong" side
        const fromnode = this.fromNode;
        const fromport = this.fromPort;
        if (
          fromnode !== null &&
          fromport !== null &&
          fromnode.findLinksInto().count === 0
        ) {
          // pretend the link is coming from the opposite direction than the declared FromSpot
          const fromctr = fromport.getDocumentPoint(go.Spot.Center);
          const fromfar = fromctr.copy();
          fromfar.x += this.fromSpot.equals(go.Spot.Left) ? 99999 : -99999;
          p1 = this.getLinkPointFromPoint(
            fromnode,
            fromport,
            fromctr,
            fromfar,
            true
          ).copy();
          // update the route points
          this.setPoint(0, p1);
          let endseg = this.fromEndSegmentLength;
          if (isNaN(endseg)) endseg = fromport.fromEndSegmentLength;
          p1.x += this.fromSpot.equals(go.Spot.Left) ? endseg : -endseg;
          this.setPoint(1, p1);
        } else {
          p1 = this.getPoint(1); // points 0 & 1 should be OK already
        }
        const tonode = this.toNode;
        const toport = this.toPort;
        if (tonode !== null && toport !== null) {
          const toctr = toport.getDocumentPoint(go.Spot.Center);
          const far = toctr.copy();
          far.x += this.fromSpot.equals(go.Spot.Left) ? -99999 / 2 : 99999 / 2;
          far.y += toctr.y < p1.y ? 99999 : -99999;
          const p2 = this.getLinkPointFromPoint(
            tonode,
            toport,
            toctr,
            far,
            false
          );
          this.setPoint(2, p2);
          let dx = Math.abs(p2.y - p1.y) / 2;
          if (this.fromSpot.equals(go.Spot.Left)) dx = -dx;
          this.insertPoint(2, new go.Point(p2.x + dx, p1.y));
        }
      } else if (
        this.toSpot.equals(go.Spot.Right) ||
        this.toSpot.equals(go.Spot.Left)
      ) {
        const p1 = this.getPoint(1); // points 1 & 2 should be OK already
        const fromnode = this.fromNode;
        const fromport = this.fromPort;
        if (fromnode !== null && fromport !== null) {
          const parentlink = fromnode.findLinksInto().first();
          const fromctr = fromport.getDocumentPoint(go.Spot.Center);
          const far = fromctr.copy();
          far.x +=
            parentlink !== null && parentlink.fromSpot.equals(go.Spot.Left)
              ? -99999 / 2
              : 99999 / 2;
          far.y += fromctr.y < p1.y ? 99999 : -99999;
          const p0 = this.getLinkPointFromPoint(
            fromnode,
            fromport,
            fromctr,
            far,
            true
          );
          this.setPoint(0, p0);
          let dx = Math.abs(p1.y - p0.y) / 2;
          if (parentlink !== null && parentlink.fromSpot.equals(go.Spot.Left))
            dx = -dx;
          this.insertPoint(1, new go.Point(p0.x + dx, p1.y));
        }
      }
    }
    return result;
  }
}

 

Logo

前往低代码交流专区

更多推荐