vnodes
Version:
Vue components to create svg interactive graphs, diagrams or node visual tools.
236 lines (218 loc) • 7.22 kB
JavaScript
import util from './util'
import { flextree } from 'd3-flextree'
export default class Graph {
constructor () {
this.nodes = []
this.edges = []
}
positionNode ({ node, parent, dir = 'right', spacing = 40, invertOffset = false } = {}) {
node = typeof node === 'string' ? this.nodes.find(n => n.id === node) : node
parent = typeof parent === 'string' ? this.nodes.find(n => n.id === parent) : parent
const pos = util.findPosition(node, parent, dir, this.nodes, spacing, invertOffset)
this.updateNode(node, { x: pos.x, y: pos.y })
}
graphNodes ({ nodes, edges, type = 'basic', dir = 'right', spacing = 40 } = {}) {
nodes = nodes || this.nodes
edges = edges || this.edges
const dag = util.createDAG(nodes, edges) // removes cycles if any
if (!dag.length) {
return
}
if (type === 'basic' || type === 'basic-invert') {
const visited = {}
const findPos = (node, parent) => {
if (visited[node.id]) {
return
}
const collisions = nodes.filter(n => !!visited[n.id])
const pos = util.findPosition(node, parent, dir, collisions, spacing, type === 'basic-invert')
node.x = pos.x
node.y = pos.y
this.updateNode(node.id, {
x: node.x,
y: node.y
})
visited[node.id] = true
node.children.forEach(n => findPos(n, node))
}
dag
.filter(node => !node.parentIds.length)
.forEach(node => findPos(node, null))
} else
if (type === 'tree') {
const layout = flextree()
const flipH = (dir === 'left' || dir === 'right')
const roots = dag.filter(n => !n.parentIds.length)
roots.forEach(root => {
const graph = []
const offsetX = root.x
const offsetY = root.y
util.dagToFlextree(root, graph, flipH, spacing)
const tree = layout.hierarchy(graph[0])
layout(tree)
// apply layout to nodes
const invertX = dir === 'left' ? -1 : 1
const invertY = dir === 'up' ? -1 : 1
const applyChanges = n => {
this.updateNode(n.data.id, {
x: (flipH ? n.y : n.x) * invertX + offsetX,
y: (flipH ? n.x : n.y) * invertY + offsetY
})
n.children && n.children.forEach(applyChanges)
}
applyChanges(tree)
})
} else {
throw new Error('unknown layout type ' + type)
}
}
reset () {
this.edges = []
this.nodes = []
}
createNode (fields = {}) {
if (typeof fields === 'string') {
fields = { id: fields } // support a single id string or an object as params
}
const node = Object.assign({
id: Math.random().toString(36).slice(2),
x: 0,
y: 0,
width: 50,
height: 50,
}, fields)
this.nodes.push(node)
return node
}
updateNode (node, fields = {}) {
if (typeof node === 'string') node = this.nodes.find(n => n.id === node)
if (!node) throw new Error(`node ${node} does not exist`)
return Object.assign(node, fields)
}
removeNode (node) {
const index = this.nodes.indexOf(node)
if (index > -1) {
this.nodes.splice(index, 1)
}
return index
}
createEdge (from, to, fields = {}) {
if (arguments.length === 1) {
// support calling with single argument
fields = arguments[0]
from = fields.from
to = fields.to
} else {
// support passing node objects instead of ids
if (typeof from === 'object') from = from.id
if (typeof to === 'object') to = to.id
}
if (!from) throw new Error('orig required')
if (!to) throw new Error('dest required')
const edge = Object.assign({
id: Math.random().toString(36).slice(2),
from,
to,
fromAnchor: { x: '50%', y: '50%' },
toAnchor: { x: '50%', y: '50%' },
type: 'linear',
pathd: '', // reactive path
}, fields)
this.edges.push(edge)
return edge
}
updateEdge (edge, fields) {
return Object.assign(edge, fields)
}
removeEdge (edge) {
const index = this.edges.indexOf(edge)
if (index > -1) {
this.edges.splice(index, 1)
}
return index
}
/**
* Force-directed layout by @emeric254
*/
reorderGraph() {
for(let i = 0; i < 200; i++){
let n = 0
for(let node of this.nodes){
for (let otherNode of this.nodes) {
if(otherNode.id === node.id){
continue
}
else{
// distance(offset) between two nodes
let dx = otherNode.x - node.x;
let dy = otherNode.y - node.y;
let offset = Math.sqrt(dx * dx + dy * dy);
// if nodes is linked
if(this.edges.find(edge => {
return (edge.to === node.id && edge.from === otherNode.id) ||
(edge.from === node.id && edge.to === otherNode.id)
})){
if(offset < 500){
if (offset < 400) {
if (offset < 200) {
// if linked nodes is so close, up distance between them
node.x -= dx;
node.y -= dy;
} else {
node.x -= dx / 3;
node.y -= dy / 3;
}
} else {
//if linked nodes have medium distance, make them little closer and up force count
n++
node.x += dx / 6;
node.y += dy / 6;
}
}
else{
//if linked nodes have long distance, make them closer
node.x += dx / 3;
node.y += dy / 3;
}
}
// if nodes isn't linked
else{
if(offset < 1000){
if(offset < 100){
// if unlinked nodes is very close, make them much further
node.x -= dx * 3;
node.y -= dy * 3;
}
else{
// if unlinked nodes is medium close, make them further
if(offset < 500){
node.x -= dx / 3;
node.y -= dy / 3;
}
else{
// if unlinked nodes is not so close, make them little further
node.x -= dx / 9;
node.y -= dy / 9;
}
}
}
else{
// if distance between nodes is so long, up force count
n++
}
}
}
}
// move all nodes to start of coordinates
let dx = 0 - node.x
let dy = 0 - node.y
node.x += dx / 15
node.y += dy / 15
}
//if force of the nodes is 70% of all graph, graph is reorder
if(n / (this.nodes.length * this.nodes.length) > 0.7){
break
}
}
}
}