UNPKG

visjs-network

Version:

A dynamic, browser-based network visualization library.

688 lines (628 loc) 19.3 kB
let util = require('../../../../../util') let EndPoints = require('./EndPoints').default /** * The Base Class for all edges. * */ class EdgeBase { /** * @param {Object} options * @param {Object} body * @param {Label} labelModule */ constructor(options, body, labelModule) { this.body = body this.labelModule = labelModule this.options = {} this.setOptions(options) this.colorDirty = true this.color = {} this.selectionWidth = 2 this.hoverWidth = 1.5 this.fromPoint = this.from this.toPoint = this.to } /** * Connects a node to itself */ connect() { this.from = this.body.nodes[this.options.from] this.to = this.body.nodes[this.options.to] } /** * * @returns {boolean} always false */ cleanup() { return false } /** * * @param {Object} options */ setOptions(options) { this.options = options this.from = this.body.nodes[this.options.from] this.to = this.body.nodes[this.options.to] this.id = this.options.id } /** * Redraw a edge as a line * Draw this edge in the given canvas * The 2d context of a HTML canvas can be retrieved by canvas.getContext("2d"); * * @param {CanvasRenderingContext2D} ctx * @param {Array} values * @param {boolean} selected * @param {boolean} hover * @param {Node} viaNode * @private */ drawLine(ctx, values, selected, hover, viaNode) { // set style ctx.strokeStyle = this.getColor(ctx, values, selected, hover) ctx.lineWidth = values.width if (values.dashes !== false) { this._drawDashedLine(ctx, values, viaNode) } else { this._drawLine(ctx, values, viaNode) } } /** * * @param {CanvasRenderingContext2D} ctx * @param {Array} values * @param {Node} viaNode * @param {{x: number, y: number}} [fromPoint] * @param {{x: number, y: number}} [toPoint] * @private */ _drawLine(ctx, values, viaNode, fromPoint, toPoint) { if (this.from != this.to) { // draw line this._line(ctx, values, viaNode, fromPoint, toPoint) } else { let [x, y, radius] = this._getCircleData(ctx) this._circle(ctx, values, x, y, radius) } } // prettier-ignore /** * * @param {CanvasRenderingContext2D} ctx * @param {Array} values * @param {Node} viaNode * @param {{x: number, y: number}} [fromPoint] TODO: Remove in next major release * @param {{x: number, y: number}} [toPoint] TODO: Remove in next major release * @private */ _drawDashedLine(ctx, values, viaNode, fromPoint, toPoint) { // eslint-disable-line no-unused-vars ctx.lineCap = 'round' let pattern = [5, 5] if (Array.isArray(values.dashes) === true) { pattern = values.dashes } // only firefox and chrome support this method, else we use the legacy one. if (ctx.setLineDash !== undefined) { ctx.save() // set dash settings for chrome or firefox ctx.setLineDash(pattern) ctx.lineDashOffset = 0 // draw the line if (this.from != this.to) { // draw line this._line(ctx, values, viaNode) } else { let [x, y, radius] = this._getCircleData(ctx) this._circle(ctx, values, x, y, radius) } // restore the dash settings. ctx.setLineDash([0]) ctx.lineDashOffset = 0 ctx.restore() } else { // unsupporting smooth lines if (this.from != this.to) { // draw line ctx.dashedLine(this.from.x, this.from.y, this.to.x, this.to.y, pattern) } else { let [x, y, radius] = this._getCircleData(ctx) this._circle(ctx, values, x, y, radius) } // draw shadow if enabled this.enableShadow(ctx, values) ctx.stroke() // disable shadows for other elements. this.disableShadow(ctx, values) } } /** * * @param {Node} nearNode * @param {CanvasRenderingContext2D} ctx * @param {Object} options * @returns {{x: number, y: number}} */ findBorderPosition(nearNode, ctx, options) { if (this.from != this.to) { return this._findBorderPosition(nearNode, ctx, options) } else { return this._findBorderPositionCircle(nearNode, ctx, options) } } /** * * @param {CanvasRenderingContext2D} ctx * @returns {{from: ({x: number, y: number, t: number}|*), to: ({x: number, y: number, t: number}|*)}} */ findBorderPositions(ctx) { let from = {} let to = {} if (this.from != this.to) { from = this._findBorderPosition(this.from, ctx) to = this._findBorderPosition(this.to, ctx) } else { let [x, y] = this._getCircleData(ctx).slice(0, 2) from = this._findBorderPositionCircle(this.from, ctx, { x, y, low: 0.25, high: 0.6, direction: -1 }) to = this._findBorderPositionCircle(this.from, ctx, { x, y, low: 0.6, high: 0.8, direction: 1 }) } return { from, to } } /** * * @param {CanvasRenderingContext2D} ctx * @returns {Array.<number>} x, y, radius * @private */ _getCircleData(ctx) { let x, y let node = this.from let radius = this.options.selfReferenceSize if (ctx !== undefined) { if (node.shape.width === undefined) { node.shape.resize(ctx) } } // get circle coordinates if (node.shape.width > node.shape.height) { x = node.x + node.shape.width * 0.5 y = node.y - radius } else { x = node.x + radius y = node.y - node.shape.height * 0.5 } return [x, y, radius] } /** * Get a point on a circle * @param {number} x * @param {number} y * @param {number} radius * @param {number} percentage - Value between 0 (line start) and 1 (line end) * @return {Object} point * @private */ _pointOnCircle(x, y, radius, percentage) { let angle = percentage * 2 * Math.PI return { x: x + radius * Math.cos(angle), y: y - radius * Math.sin(angle) } } /** * This function uses binary search to look for the point where the circle crosses the border of the node. * @param {Node} node * @param {CanvasRenderingContext2D} ctx * @param {Object} options * @returns {*} * @private */ _findBorderPositionCircle(node, ctx, options) { let x = options.x let y = options.y let low = options.low let high = options.high let direction = options.direction let maxIterations = 10 let iteration = 0 let radius = this.options.selfReferenceSize let pos, angle, distanceToBorder, distanceToPoint, difference let threshold = 0.05 let middle = (low + high) * 0.5 while (low <= high && iteration < maxIterations) { middle = (low + high) * 0.5 pos = this._pointOnCircle(x, y, radius, middle) angle = Math.atan2(node.y - pos.y, node.x - pos.x) distanceToBorder = node.distanceToBorder(ctx, angle) distanceToPoint = Math.sqrt( Math.pow(pos.x - node.x, 2) + Math.pow(pos.y - node.y, 2) ) difference = distanceToBorder - distanceToPoint if (Math.abs(difference) < threshold) { break // found } else if (difference > 0) { // distance to nodes is larger than distance to border --> t needs to be bigger if we're looking at the to node. if (direction > 0) { low = middle } else { high = middle } } else { if (direction > 0) { high = middle } else { low = middle } } iteration++ } pos.t = middle return pos } /** * Get the line width of the edge. Depends on width and whether one of the * connected nodes is selected. * @param {boolean} selected * @param {boolean} hover * @returns {number} width * @private */ getLineWidth(selected, hover) { if (selected === true) { return Math.max(this.selectionWidth, 0.3 / this.body.view.scale) } else { if (hover === true) { return Math.max(this.hoverWidth, 0.3 / this.body.view.scale) } else { return Math.max(this.options.width, 0.3 / this.body.view.scale) } } } // prettier-ignore /** * * @param {CanvasRenderingContext2D} ctx * @param {ArrowOptions} values * @param {boolean} selected - Unused * @param {boolean} hover - Unused * @returns {string} */ getColor(ctx, values, selected, hover) { // eslint-disable-line no-unused-vars if (values.inheritsColor !== false) { // when this is a loop edge, just use the 'from' method if (values.inheritsColor === 'both' && this.from.id !== this.to.id) { let grd = ctx.createLinearGradient( this.from.x, this.from.y, this.to.x, this.to.y ) let fromColor, toColor fromColor = this.from.options.color.highlight.border toColor = this.to.options.color.highlight.border if (this.from.selected === false && this.to.selected === false) { fromColor = util.overrideOpacity( this.from.options.color.border, values.opacity ) toColor = util.overrideOpacity( this.to.options.color.border, values.opacity ) } else if (this.from.selected === true && this.to.selected === false) { toColor = this.to.options.color.border } else if (this.from.selected === false && this.to.selected === true) { fromColor = this.from.options.color.border } grd.addColorStop(0, fromColor) grd.addColorStop(1, toColor) // -------------------- this returns -------------------- // return grd } if (values.inheritsColor === 'to') { return util.overrideOpacity( this.to.options.color.border, values.opacity ) } else { // "from" return util.overrideOpacity( this.from.options.color.border, values.opacity ) } } else { return util.overrideOpacity(values.color, values.opacity) } } /** * Draw a line from a node to itself, a circle * * @param {CanvasRenderingContext2D} ctx * @param {Array} values * @param {number} x * @param {number} y * @param {number} radius * @private */ _circle(ctx, values, x, y, radius) { // draw shadow if enabled this.enableShadow(ctx, values) // draw a circle ctx.beginPath() ctx.arc(x, y, radius, 0, 2 * Math.PI, false) ctx.stroke() // disable shadows for other elements. this.disableShadow(ctx, values) } // prettier-ignore /** * Calculate the distance between a point (x3,y3) and a line segment from (x1,y1) to (x2,y2). * (x3,y3) is the point. * * http://stackoverflow.com/questions/849211/shortest-distancae-between-a-point-and-a-line-segment * * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @param {number} x3 * @param {number} y3 * @param {Node} via * @param {Array} values * @returns {number} */ getDistanceToEdge(x1, y1, x2, y2, x3, y3, via, values) { // eslint-disable-line no-unused-vars let returnValue = 0 if (this.from != this.to) { returnValue = this._getDistanceToEdge(x1, y1, x2, y2, x3, y3, via) } else { let [x, y, radius] = this._getCircleData(undefined) let dx = x - x3 let dy = y - y3 returnValue = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius) } return returnValue } /** * * @param {number} x1 * @param {number} y1 * @param {number} x2 * @param {number} y2 * @param {number} x3 * @param {number} y3 * @returns {number} * @private */ _getDistanceToLine(x1, y1, x2, y2, x3, y3) { let px = x2 - x1 let py = y2 - y1 let something = px * px + py * py let u = ((x3 - x1) * px + (y3 - y1) * py) / something if (u > 1) { u = 1 } else if (u < 0) { u = 0 } let x = x1 + u * px let y = y1 + u * py let dx = x - x3 let dy = y - y3 //# Note: If the actual distance does not matter, //# if you only want to compare what this function //# returns to other results of this function, you //# can just return the squared distance instead //# (i.e. remove the sqrt) to gain a little performance return Math.sqrt(dx * dx + dy * dy) } /** * @param {CanvasRenderingContext2D} ctx * @param {string} position * @param {Node} viaNode * @param {boolean} selected * @param {boolean} hover * @param {Array} values * @returns {{point: *, core: {x: number, y: number}, angle: *, length: number, type: *}} */ getArrowData(ctx, position, viaNode, selected, hover, values) { // set lets let angle let arrowPoint let node1 let node2 let guideOffset let scaleFactor let type let lineWidth = values.width if (position === 'from') { node1 = this.from node2 = this.to guideOffset = 0.1 scaleFactor = values.fromArrowScale type = values.fromArrowType } else if (position === 'to') { node1 = this.to node2 = this.from guideOffset = -0.1 scaleFactor = values.toArrowScale type = values.toArrowType } else { node1 = this.to node2 = this.from scaleFactor = values.middleArrowScale type = values.middleArrowType } // if not connected to itself if (node1 != node2) { if (position !== 'middle') { // draw arrow head if (this.options.smooth.enabled === true) { arrowPoint = this.findBorderPosition(node1, ctx, { via: viaNode }) let guidePos = this.getPoint( Math.max(0.0, Math.min(1.0, arrowPoint.t + guideOffset)), viaNode ) angle = Math.atan2( arrowPoint.y - guidePos.y, arrowPoint.x - guidePos.x ) } else { angle = Math.atan2(node1.y - node2.y, node1.x - node2.x) arrowPoint = this.findBorderPosition(node1, ctx) } } else { angle = Math.atan2(node1.y - node2.y, node1.x - node2.x) arrowPoint = this.getPoint(0.5, viaNode) // this is 0.6 to account for the size of the arrow. } } else { // draw circle let [x, y, radius] = this._getCircleData(ctx) if (position === 'from') { arrowPoint = this.findBorderPosition(this.from, ctx, { x, y, low: 0.25, high: 0.6, direction: -1 }) angle = arrowPoint.t * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI } else if (position === 'to') { arrowPoint = this.findBorderPosition(this.from, ctx, { x, y, low: 0.6, high: 1.0, direction: 1 }) angle = arrowPoint.t * -2 * Math.PI + 1.5 * Math.PI - 1.1 * Math.PI } else { arrowPoint = this._pointOnCircle(x, y, radius, 0.175) angle = 3.9269908169872414 // === 0.175 * -2 * Math.PI + 1.5 * Math.PI + 0.1 * Math.PI; } } if (position === 'middle' && scaleFactor < 0) lineWidth *= -1 // reversed middle arrow let length = 15 * scaleFactor + 3 * lineWidth // 3* lineWidth is the width of the edge. var xi = arrowPoint.x - length * 0.9 * Math.cos(angle) var yi = arrowPoint.y - length * 0.9 * Math.sin(angle) let arrowCore = { x: xi, y: yi } return { point: arrowPoint, core: arrowCore, angle: angle, length: length, type: type } } /** * * @param {CanvasRenderingContext2D} ctx * @param {ArrowOptions} values * @param {boolean} selected * @param {boolean} hover * @param {Object} arrowData */ drawArrowHead(ctx, values, selected, hover, arrowData) { // set style ctx.strokeStyle = this.getColor(ctx, values, selected, hover) ctx.fillStyle = ctx.strokeStyle ctx.lineWidth = values.width EndPoints.draw(ctx, arrowData) // draw shadow if enabled this.enableShadow(ctx, values) ctx.fill() // disable shadows for other elements. this.disableShadow(ctx, values) } /** * * @param {CanvasRenderingContext2D} ctx * @param {ArrowOptions} values */ enableShadow(ctx, values) { if (values.shadow === true) { ctx.shadowColor = values.shadowColor ctx.shadowBlur = values.shadowSize ctx.shadowOffsetX = values.shadowX ctx.shadowOffsetY = values.shadowY } } /** * * @param {CanvasRenderingContext2D} ctx * @param {ArrowOptions} values */ disableShadow(ctx, values) { if (values.shadow === true) { ctx.shadowColor = 'rgba(0,0,0,0)' ctx.shadowBlur = 0 ctx.shadowOffsetX = 0 ctx.shadowOffsetY = 0 } } /** * * @param {CanvasRenderingContext2D} ctx * @param {{toArrow: boolean, toArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), toArrowType: *, middleArrow: boolean, middleArrowScale: (number|allOptions.edges.arrows.middle.scaleFactor|{number}|Array), middleArrowType: (allOptions.edges.arrows.middle.type|{string}|string|*), fromArrow: boolean, fromArrowScale: (allOptions.edges.arrows.to.scaleFactor|{number}|allOptions.edges.arrows.middle.scaleFactor|allOptions.edges.arrows.from.scaleFactor|Array|number), fromArrowType: *, arrowStrikethrough: (*|boolean|allOptions.edges.arrowStrikethrough|{boolean}), color: undefined, inheritsColor: (string|string|string|allOptions.edges.color.inherit|{string, boolean}|Array|*), opacity: *, hidden: *, length: *, shadow: *, shadowColor: *, shadowSize: *, shadowX: *, shadowY: *, dashes: (*|boolean|Array|allOptions.edges.dashes|{boolean, array}), width: *}} values */ drawBackground(ctx, values) { if (values.background !== false) { let attrs = ['strokeStyle', 'lineWidth', 'dashes'] let origCtxAttr = {} // save original line attrs attrs.forEach(function(attrname) { origCtxAttr[attrname] = ctx[attrname] }) ctx.strokeStyle = values.backgroundColor ctx.lineWidth = values.backgroundSize this.setStrokeDashed(ctx, values.backgroundDashes) ctx.stroke() // restore original line attrs attrs.forEach(function(attrname) { ctx[attrname] = origCtxAttr[attrname] }) this.setStrokeDashed(ctx, values.dashes) } } /** * * @param {CanvasRenderingContext2D} ctx * @param {boolean|Array} dashes */ setStrokeDashed(ctx, dashes) { if (dashes !== false) { if (ctx.setLineDash !== undefined) { let pattern = [5, 5] if (Array.isArray(dashes) === true) { pattern = dashes } ctx.setLineDash(pattern) } else { console.warn( 'setLineDash is not supported in this browser. The dashed stroke cannot be used.' ) } } else { if (ctx.setLineDash !== undefined) { ctx.setLineDash([]) } else { console.warn( 'setLineDash is not supported in this browser. The dashed stroke cannot be used.' ) } } } } export default EdgeBase