vue+gojs 绘制鱼骨图(因果图)
最近查找js相关的鱼骨图组件,找了半天都没有合适的,自己参考gojs官网demo简单的实现了下,具体实现如下。
·
最近查找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;
}
}
更多推荐
已为社区贡献1条内容
所有评论(0)