import { select } from 'd3-selection'; import { zoom, zoomTransform } from 'd3-zoom'; import { drag } from 'd3-drag'; import { min, max } from 'd3-array'; import { throttle } from 'lodash-es'; import * as TWEEN from '@tweenjs/tween.js'; import Kapsule from 'kapsule'; import accessorFn from 'accessor-fn'; import ColorTracker from 'canvas-color-tracker'; import { forceSimulation, forceLink, forceManyBody, forceCenter, forceRadial } from 'd3-force-3d'; import { Bezier } from 'bezier-js'; import indexBy from 'index-array-by'; import { scaleOrdinal } from 'd3-scale'; import { schemePaired } from 'd3-scale-chromatic'; function styleInject(css, ref) { if (ref === void 0) ref = {}; var insertAt = ref.insertAt; if (!css || typeof document === 'undefined') { return; } var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; if (insertAt === 'top') { if (head.firstChild) { head.insertBefore(style, head.firstChild); } else { head.appendChild(style); } } else { head.appendChild(style); } if (style.styleSheet) { style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } } var css_248z = ".force-graph-container canvas {\n display: block;\n user-select: none;\n outline: none;\n -webkit-tap-highlight-color: transparent;\n}\n\n.force-graph-container .graph-tooltip {\n position: absolute;\n top: 0;\n font-family: sans-serif;\n font-size: 16px;\n padding: 4px;\n border-radius: 3px;\n color: #eee;\n background: rgba(0,0,0,0.65);\n visibility: hidden; /* by default */\n}\n\n.force-graph-container .clickable {\n cursor: pointer;\n}\n\n.force-graph-container .grabbable {\n cursor: move;\n cursor: grab;\n cursor: -moz-grab;\n cursor: -webkit-grab;\n}\n\n.force-graph-container .grabbable:active {\n cursor: grabbing;\n cursor: -moz-grabbing;\n cursor: -webkit-grabbing;\n}\n"; styleInject(css_248z); function _iterableToArrayLimit(arr, i) { var _i = null == arr ? null : "undefined" != typeof Symbol && arr[Symbol.iterator] || arr["@@iterator"]; if (null != _i) { var _s, _e, _x, _r, _arr = [], _n = !0, _d = !1; try { if (_x = (_i = _i.call(arr)).next, 0 === i) { if (Object(_i) !== _i) return; _n = !1; } else for (; !(_n = (_s = _x.call(_i)).done) && (_arr.push(_s.value), _arr.length !== i); _n = !0); } catch (err) { _d = !0, _e = err; } finally { try { if (!_n && null != _i.return && (_r = _i.return(), Object(_r) !== _r)) return; } finally { if (_d) throw _e; } } return _arr; } } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread2(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } function _construct(Parent, args, Class) { if (_isNativeReflectConstruct()) { _construct = Reflect.construct.bind(); } else { _construct = function _construct(Parent, args, Class) { var a = [null]; a.push.apply(a, args); var Constructor = Function.bind.apply(Parent, a); var instance = new Constructor(); if (Class) _setPrototypeOf(instance, Class.prototype); return instance; }; } return _construct.apply(null, arguments); } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter); } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); } var autoColorScale = scaleOrdinal(schemePaired); // Autoset attribute colorField by colorByAccessor property // If an object has already a color, don't set it // Objects can be nodes or links function autoColorObjects(objects, colorByAccessor, colorField) { if (!colorByAccessor || typeof colorField !== 'string') return; objects.filter(function (obj) { return !obj[colorField]; }).forEach(function (obj) { obj[colorField] = autoColorScale(colorByAccessor(obj)); }); } function getDagDepths (_ref, idAccessor) { var nodes = _ref.nodes, links = _ref.links; var _ref2 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, _ref2$nodeFilter = _ref2.nodeFilter, nodeFilter = _ref2$nodeFilter === void 0 ? function () { return true; } : _ref2$nodeFilter, _ref2$onLoopError = _ref2.onLoopError, onLoopError = _ref2$onLoopError === void 0 ? function (loopIds) { throw "Invalid DAG structure! Found cycle in node path: ".concat(loopIds.join(' -> '), "."); } : _ref2$onLoopError; // linked graph var graph = {}; nodes.forEach(function (node) { return graph[idAccessor(node)] = { data: node, out: [], depth: -1, skip: !nodeFilter(node) }; }); links.forEach(function (_ref3) { var source = _ref3.source, target = _ref3.target; var sourceId = getNodeId(source); var targetId = getNodeId(target); if (!graph.hasOwnProperty(sourceId)) throw "Missing source node with id: ".concat(sourceId); if (!graph.hasOwnProperty(targetId)) throw "Missing target node with id: ".concat(targetId); var sourceNode = graph[sourceId]; var targetNode = graph[targetId]; sourceNode.out.push(targetNode); function getNodeId(node) { return _typeof(node) === 'object' ? idAccessor(node) : node; } }); var foundLoops = []; traverse(Object.values(graph)); var nodeDepths = Object.assign.apply(Object, [{}].concat(_toConsumableArray(Object.entries(graph).filter(function (_ref4) { var _ref5 = _slicedToArray(_ref4, 2), node = _ref5[1]; return !node.skip; }).map(function (_ref6) { var _ref7 = _slicedToArray(_ref6, 2), id = _ref7[0], node = _ref7[1]; return _defineProperty({}, id, node.depth); })))); return nodeDepths; function traverse(nodes) { var nodeStack = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; var currentDepth = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; var _loop = function _loop() { var node = nodes[i]; if (nodeStack.indexOf(node) !== -1) { var loop = [].concat(_toConsumableArray(nodeStack.slice(nodeStack.indexOf(node))), [node]).map(function (d) { return idAccessor(d.data); }); if (!foundLoops.some(function (foundLoop) { return foundLoop.length === loop.length && foundLoop.every(function (id, idx) { return id === loop[idx]; }); })) { foundLoops.push(loop); onLoopError(loop); } return "continue"; } if (currentDepth > node.depth) { // Don't unnecessarily revisit chunks of the graph node.depth = currentDepth; traverse(node.out, [].concat(_toConsumableArray(nodeStack), [node]), currentDepth + (node.skip ? 0 : 1)); } }; for (var i = 0, l = nodes.length; i < l; i++) { var _ret = _loop(); if (_ret === "continue") continue; } } } // var DAG_LEVEL_NODE_RATIO = 2; // whenever styling props are changed that require a canvas redraw var notifyRedraw = function notifyRedraw(_, state) { return state.onNeedsRedraw && state.onNeedsRedraw(); }; var updDataPhotons = function updDataPhotons(_, state) { if (!state.isShadow) { // Add photon particles var linkParticlesAccessor = accessorFn(state.linkDirectionalParticles); state.graphData.links.forEach(function (link) { var numPhotons = Math.round(Math.abs(linkParticlesAccessor(link))); if (numPhotons) { link.__photons = _toConsumableArray(Array(numPhotons)).map(function () { return {}; }); } else { delete link.__photons; } }); } }; var CanvasForceGraph = Kapsule({ props: { graphData: { "default": { nodes: [], links: [] }, onChange: function onChange(_, state) { state.engineRunning = false; // Pause simulation updDataPhotons(_, state); } }, dagMode: { onChange: function onChange(dagMode, state) { // td, bu, lr, rl, radialin, radialout !dagMode && (state.graphData.nodes || []).forEach(function (n) { return n.fx = n.fy = undefined; }); // unfix nodes when disabling dag mode } }, dagLevelDistance: {}, dagNodeFilter: { "default": function _default(node) { return true; } }, onDagError: { triggerUpdate: false }, nodeRelSize: { "default": 4, triggerUpdate: false, onChange: notifyRedraw }, // area per val unit nodeId: { "default": 'id' }, nodeVal: { "default": 'val', triggerUpdate: false, onChange: notifyRedraw }, nodeColor: { "default": 'color', triggerUpdate: false, onChange: notifyRedraw }, nodeAutoColorBy: {}, nodeCanvasObject: { triggerUpdate: false, onChange: notifyRedraw }, nodeCanvasObjectMode: { "default": function _default() { return 'replace'; }, triggerUpdate: false, onChange: notifyRedraw }, nodeVisibility: { "default": true, triggerUpdate: false, onChange: notifyRedraw }, linkSource: { "default": 'source' }, linkTarget: { "default": 'target' }, linkVisibility: { "default": true, triggerUpdate: false, onChange: notifyRedraw }, linkColor: { "default": 'color', triggerUpdate: false, onChange: notifyRedraw }, linkAutoColorBy: {}, linkLineDash: { triggerUpdate: false, onChange: notifyRedraw }, linkWidth: { "default": 1, triggerUpdate: false, onChange: notifyRedraw }, linkCurvature: { "default": 0, triggerUpdate: false, onChange: notifyRedraw }, linkCanvasObject: { triggerUpdate: false, onChange: notifyRedraw }, linkCanvasObjectMode: { "default": function _default() { return 'replace'; }, triggerUpdate: false, onChange: notifyRedraw }, linkDirectionalArrowLength: { "default": 0, triggerUpdate: false, onChange: notifyRedraw }, linkDirectionalArrowColor: { triggerUpdate: false, onChange: notifyRedraw }, linkDirectionalArrowRelPos: { "default": 0.5, triggerUpdate: false, onChange: notifyRedraw }, // value between 0<>1 indicating the relative pos along the (exposed) line linkDirectionalParticles: { "default": 0, triggerUpdate: false, onChange: updDataPhotons }, // animate photons travelling in the link direction linkDirectionalParticleSpeed: { "default": 0.01, triggerUpdate: false }, // in link length ratio per frame linkDirectionalParticleWidth: { "default": 4, triggerUpdate: false }, linkDirectionalParticleColor: { triggerUpdate: false }, globalScale: { "default": 1, triggerUpdate: false }, d3AlphaMin: { "default": 0, triggerUpdate: false }, d3AlphaDecay: { "default": 0.0228, triggerUpdate: false, onChange: function onChange(alphaDecay, state) { state.forceLayout.alphaDecay(alphaDecay); } }, d3AlphaTarget: { "default": 0, triggerUpdate: false, onChange: function onChange(alphaTarget, state) { state.forceLayout.alphaTarget(alphaTarget); } }, d3VelocityDecay: { "default": 0.4, triggerUpdate: false, onChange: function onChange(velocityDecay, state) { state.forceLayout.velocityDecay(velocityDecay); } }, warmupTicks: { "default": 0, triggerUpdate: false }, // how many times to tick the force engine at init before starting to render cooldownTicks: { "default": Infinity, triggerUpdate: false }, cooldownTime: { "default": 15000, triggerUpdate: false }, // ms onUpdate: { "default": function _default() {}, triggerUpdate: false }, onFinishUpdate: { "default": function _default() {}, triggerUpdate: false }, onEngineTick: { "default": function _default() {}, triggerUpdate: false }, onEngineStop: { "default": function _default() {}, triggerUpdate: false }, onNeedsRedraw: { triggerUpdate: false }, isShadow: { "default": false, triggerUpdate: false } }, methods: { // Expose d3 forces for external manipulation d3Force: function d3Force(state, forceName, forceFn) { if (forceFn === undefined) { return state.forceLayout.force(forceName); // Force getter } state.forceLayout.force(forceName, forceFn); // Force setter return this; }, d3ReheatSimulation: function d3ReheatSimulation(state) { state.forceLayout.alpha(1); this.resetCountdown(); return this; }, // reset cooldown state resetCountdown: function resetCountdown(state) { state.cntTicks = 0; state.startTickTime = new Date(); state.engineRunning = true; return this; }, isEngineRunning: function isEngineRunning(state) { return !!state.engineRunning; }, tickFrame: function tickFrame(state) { !state.isShadow && layoutTick(); paintLinks(); !state.isShadow && paintArrows(); !state.isShadow && paintPhotons(); paintNodes(); return this; // function layoutTick() { if (state.engineRunning) { if (++state.cntTicks > state.cooldownTicks || new Date() - state.startTickTime > state.cooldownTime || state.d3AlphaMin > 0 && state.forceLayout.alpha() < state.d3AlphaMin) { state.engineRunning = false; // Stop ticking graph state.onEngineStop(); } else { state.forceLayout.tick(); // Tick it state.onEngineTick(); } } } function paintNodes() { var getVisibility = accessorFn(state.nodeVisibility); var getVal = accessorFn(state.nodeVal); var getColor = accessorFn(state.nodeColor); var getNodeCanvasObjectMode = accessorFn(state.nodeCanvasObjectMode); var ctx = state.ctx; // Draw wider nodes by 1px on shadow canvas for more precise hovering (due to boundary anti-aliasing) var padAmount = state.isShadow / state.globalScale; var visibleNodes = state.graphData.nodes.filter(getVisibility); ctx.save(); visibleNodes.forEach(function (node) { var nodeCanvasObjectMode = getNodeCanvasObjectMode(node); if (state.nodeCanvasObject && (nodeCanvasObjectMode === 'before' || nodeCanvasObjectMode === 'replace')) { // Custom node before/replace paint state.nodeCanvasObject(node, ctx, state.globalScale); if (nodeCanvasObjectMode === 'replace') { ctx.restore(); return; } } // Draw wider nodes by 1px on shadow canvas for more precise hovering (due to boundary anti-aliasing) var r = Math.sqrt(Math.max(0, getVal(node) || 1)) * state.nodeRelSize + padAmount; ctx.beginPath(); ctx.arc(node.x, node.y, r, 0, 2 * Math.PI, false); ctx.fillStyle = getColor(node) || 'rgba(31, 120, 180, 0.92)'; ctx.fill(); if (state.nodeCanvasObject && nodeCanvasObjectMode === 'after') { // Custom node after paint state.nodeCanvasObject(node, state.ctx, state.globalScale); } }); ctx.restore(); } function paintLinks() { var getVisibility = accessorFn(state.linkVisibility); var getColor = accessorFn(state.linkColor); var getWidth = accessorFn(state.linkWidth); var getLineDash = accessorFn(state.linkLineDash); var getCurvature = accessorFn(state.linkCurvature); var getLinkCanvasObjectMode = accessorFn(state.linkCanvasObjectMode); var ctx = state.ctx; // Draw wider lines by 2px on shadow canvas for more precise hovering (due to boundary anti-aliasing) var padAmount = state.isShadow * 2; var visibleLinks = state.graphData.links.filter(getVisibility); visibleLinks.forEach(calcLinkControlPoints); // calculate curvature control points for all visible links var beforeCustomLinks = [], afterCustomLinks = [], defaultPaintLinks = visibleLinks; if (state.linkCanvasObject) { var replaceCustomLinks = [], otherCustomLinks = []; visibleLinks.forEach(function (d) { return ({ before: beforeCustomLinks, after: afterCustomLinks, replace: replaceCustomLinks }[getLinkCanvasObjectMode(d)] || otherCustomLinks).push(d); }); defaultPaintLinks = [].concat(_toConsumableArray(beforeCustomLinks), afterCustomLinks, otherCustomLinks); beforeCustomLinks = beforeCustomLinks.concat(replaceCustomLinks); } // Custom link before paints ctx.save(); beforeCustomLinks.forEach(function (link) { return state.linkCanvasObject(link, ctx, state.globalScale); }); ctx.restore(); // Bundle strokes per unique color/width/dash for performance optimization var linksPerColor = indexBy(defaultPaintLinks, [getColor, getWidth, getLineDash]); ctx.save(); Object.entries(linksPerColor).forEach(function (_ref) { var _ref2 = _slicedToArray(_ref, 2), color = _ref2[0], linksPerWidth = _ref2[1]; var lineColor = !color || color === 'undefined' ? 'rgba(0,0,0,0.15)' : color; Object.entries(linksPerWidth).forEach(function (_ref3) { var _ref4 = _slicedToArray(_ref3, 2), width = _ref4[0], linesPerLineDash = _ref4[1]; var lineWidth = (width || 1) / state.globalScale + padAmount; Object.entries(linesPerLineDash).forEach(function (_ref5) { var _ref6 = _slicedToArray(_ref5, 2); _ref6[0]; var links = _ref6[1]; var lineDashSegments = getLineDash(links[0]); ctx.beginPath(); links.forEach(function (link) { var start = link.source; var end = link.target; if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link ctx.moveTo(start.x, start.y); var controlPoints = link.__controlPoints; if (!controlPoints) { // Straight line ctx.lineTo(end.x, end.y); } else { // Use quadratic curves for regular lines and bezier for loops ctx[controlPoints.length === 2 ? 'quadraticCurveTo' : 'bezierCurveTo'].apply(ctx, _toConsumableArray(controlPoints).concat([end.x, end.y])); } }); ctx.strokeStyle = lineColor; ctx.lineWidth = lineWidth; ctx.setLineDash(lineDashSegments || []); ctx.stroke(); }); }); }); ctx.restore(); // Custom link after paints ctx.save(); afterCustomLinks.forEach(function (link) { return state.linkCanvasObject(link, ctx, state.globalScale); }); ctx.restore(); // function calcLinkControlPoints(link) { var curvature = getCurvature(link); if (!curvature) { // straight line link.__controlPoints = null; return; } var start = link.source; var end = link.target; if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link var l = Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2)); // line length if (l > 0) { var a = Math.atan2(end.y - start.y, end.x - start.x); // line angle var d = l * curvature; // control point distance var cp = { // control point x: (start.x + end.x) / 2 + d * Math.cos(a - Math.PI / 2), y: (start.y + end.y) / 2 + d * Math.sin(a - Math.PI / 2) }; link.__controlPoints = [cp.x, cp.y]; } else { // Same point, draw a loop var _d = curvature * 70; link.__controlPoints = [end.x, end.y - _d, end.x + _d, end.y]; } } } function paintArrows() { var ARROW_WH_RATIO = 1.6; var ARROW_VLEN_RATIO = 0.2; var getLength = accessorFn(state.linkDirectionalArrowLength); var getRelPos = accessorFn(state.linkDirectionalArrowRelPos); var getVisibility = accessorFn(state.linkVisibility); var getColor = accessorFn(state.linkDirectionalArrowColor || state.linkColor); var getNodeVal = accessorFn(state.nodeVal); var ctx = state.ctx; ctx.save(); state.graphData.links.filter(getVisibility).forEach(function (link) { var arrowLength = getLength(link); if (!arrowLength || arrowLength < 0) return; var start = link.source; var end = link.target; if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link var startR = Math.sqrt(Math.max(0, getNodeVal(start) || 1)) * state.nodeRelSize; var endR = Math.sqrt(Math.max(0, getNodeVal(end) || 1)) * state.nodeRelSize; var arrowRelPos = Math.min(1, Math.max(0, getRelPos(link))); var arrowColor = getColor(link) || 'rgba(0,0,0,0.28)'; var arrowHalfWidth = arrowLength / ARROW_WH_RATIO / 2; // Construct bezier for curved lines var bzLine = link.__controlPoints && _construct(Bezier, [start.x, start.y].concat(_toConsumableArray(link.__controlPoints), [end.x, end.y])); var getCoordsAlongLine = bzLine ? function (t) { return bzLine.get(t); } // get position along bezier line : function (t) { return { // straight line: interpolate linearly x: start.x + (end.x - start.x) * t || 0, y: start.y + (end.y - start.y) * t || 0 }; }; var lineLen = bzLine ? bzLine.length() : Math.sqrt(Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2)); var posAlongLine = startR + arrowLength + (lineLen - startR - endR - arrowLength) * arrowRelPos; var arrowHead = getCoordsAlongLine(posAlongLine / lineLen); var arrowTail = getCoordsAlongLine((posAlongLine - arrowLength) / lineLen); var arrowTailVertex = getCoordsAlongLine((posAlongLine - arrowLength * (1 - ARROW_VLEN_RATIO)) / lineLen); var arrowTailAngle = Math.atan2(arrowHead.y - arrowTail.y, arrowHead.x - arrowTail.x) - Math.PI / 2; ctx.beginPath(); ctx.moveTo(arrowHead.x, arrowHead.y); ctx.lineTo(arrowTail.x + arrowHalfWidth * Math.cos(arrowTailAngle), arrowTail.y + arrowHalfWidth * Math.sin(arrowTailAngle)); ctx.lineTo(arrowTailVertex.x, arrowTailVertex.y); ctx.lineTo(arrowTail.x - arrowHalfWidth * Math.cos(arrowTailAngle), arrowTail.y - arrowHalfWidth * Math.sin(arrowTailAngle)); ctx.fillStyle = arrowColor; ctx.fill(); }); ctx.restore(); } function paintPhotons() { var getNumPhotons = accessorFn(state.linkDirectionalParticles); var getSpeed = accessorFn(state.linkDirectionalParticleSpeed); var getDiameter = accessorFn(state.linkDirectionalParticleWidth); var getVisibility = accessorFn(state.linkVisibility); var getColor = accessorFn(state.linkDirectionalParticleColor || state.linkColor); var ctx = state.ctx; ctx.save(); state.graphData.links.filter(getVisibility).forEach(function (link) { var numCyclePhotons = getNumPhotons(link); if (!link.hasOwnProperty('__photons') || !link.__photons.length) return; var start = link.source; var end = link.target; if (!start || !end || !start.hasOwnProperty('x') || !end.hasOwnProperty('x')) return; // skip invalid link var particleSpeed = getSpeed(link); var photons = link.__photons || []; var photonR = Math.max(0, getDiameter(link) / 2) / Math.sqrt(state.globalScale); var photonColor = getColor(link) || 'rgba(0,0,0,0.28)'; ctx.fillStyle = photonColor; // Construct bezier for curved lines var bzLine = link.__controlPoints ? _construct(Bezier, [start.x, start.y].concat(_toConsumableArray(link.__controlPoints), [end.x, end.y])) : null; var cyclePhotonIdx = 0; var needsCleanup = false; // whether some photons need to be removed from list photons.forEach(function (photon) { var singleHop = !!photon.__singleHop; if (!photon.hasOwnProperty('__progressRatio')) { photon.__progressRatio = singleHop ? 0 : cyclePhotonIdx / numCyclePhotons; } !singleHop && cyclePhotonIdx++; // increase regular photon index photon.__progressRatio += particleSpeed; if (photon.__progressRatio >= 1) { if (!singleHop) { photon.__progressRatio = photon.__progressRatio % 1; } else { needsCleanup = true; return; } } var photonPosRatio = photon.__progressRatio; var coords = bzLine ? bzLine.get(photonPosRatio) // get position along bezier line : { // straight line: interpolate linearly x: start.x + (end.x - start.x) * photonPosRatio || 0, y: start.y + (end.y - start.y) * photonPosRatio || 0 }; ctx.beginPath(); ctx.arc(coords.x, coords.y, photonR, 0, 2 * Math.PI, false); ctx.fill(); }); if (needsCleanup) { // remove expired single hop photons link.__photons = link.__photons.filter(function (photon) { return !photon.__singleHop || photon.__progressRatio <= 1; }); } }); ctx.restore(); } }, emitParticle: function emitParticle(state, link) { if (link) { !link.__photons && (link.__photons = []); link.__photons.push({ __singleHop: true }); // add a single hop particle } return this; } }, stateInit: function stateInit() { return { forceLayout: forceSimulation().force('link', forceLink()).force('charge', forceManyBody()).force('center', forceCenter()).force('dagRadial', null).stop(), engineRunning: false }; }, init: function init(canvasCtx, state) { // Main canvas object to manipulate state.ctx = canvasCtx; }, update: function update(state) { state.engineRunning = false; // Pause simulation state.onUpdate(); if (state.nodeAutoColorBy !== null) { // Auto add color to uncolored nodes autoColorObjects(state.graphData.nodes, accessorFn(state.nodeAutoColorBy), state.nodeColor); } if (state.linkAutoColorBy !== null) { // Auto add color to uncolored links autoColorObjects(state.graphData.links, accessorFn(state.linkAutoColorBy), state.linkColor); } // parse links state.graphData.links.forEach(function (link) { link.source = link[state.linkSource]; link.target = link[state.linkTarget]; }); // Feed data to force-directed layout state.forceLayout.stop().alpha(1) // re-heat the simulation .nodes(state.graphData.nodes); // add links (if link force is still active) var linkForce = state.forceLayout.force('link'); if (linkForce) { linkForce.id(function (d) { return d[state.nodeId]; }).links(state.graphData.links); } // setup dag force constraints var nodeDepths = state.dagMode && getDagDepths(state.graphData, function (node) { return node[state.nodeId]; }, { nodeFilter: state.dagNodeFilter, onLoopError: state.onDagError || undefined }); var maxDepth = Math.max.apply(Math, _toConsumableArray(Object.values(nodeDepths || []))); var dagLevelDistance = state.dagLevelDistance || state.graphData.nodes.length / (maxDepth || 1) * DAG_LEVEL_NODE_RATIO * (['radialin', 'radialout'].indexOf(state.dagMode) !== -1 ? 0.7 : 1); // Fix nodes to x,y for dag mode if (state.dagMode) { var getFFn = function getFFn(fix, invert) { return function (node) { return !fix ? undefined : (nodeDepths[node[state.nodeId]] - maxDepth / 2) * dagLevelDistance * (invert ? -1 : 1); }; }; var fxFn = getFFn(['lr', 'rl'].indexOf(state.dagMode) !== -1, state.dagMode === 'rl'); var fyFn = getFFn(['td', 'bu'].indexOf(state.dagMode) !== -1, state.dagMode === 'bu'); state.graphData.nodes.filter(state.dagNodeFilter).forEach(function (node) { node.fx = fxFn(node); node.fy = fyFn(node); }); } // Use radial force for radial dags state.forceLayout.force('dagRadial', ['radialin', 'radialout'].indexOf(state.dagMode) !== -1 ? forceRadial(function (node) { var nodeDepth = nodeDepths[node[state.nodeId]] || -1; return (state.dagMode === 'radialin' ? maxDepth - nodeDepth : nodeDepth) * dagLevelDistance; }).strength(function (node) { return state.dagNodeFilter(node) ? 1 : 0; }) : null); for (var i = 0; i < state.warmupTicks && !(state.d3AlphaMin > 0 && state.forceLayout.alpha() < state.d3AlphaMin); i++) { state.forceLayout.tick(); } // Initial ticks before starting to render this.resetCountdown(); state.onFinishUpdate(); } }); function linkKapsule (kapsulePropNames, kapsuleType) { var propNames = kapsulePropNames instanceof Array ? kapsulePropNames : [kapsulePropNames]; var dummyK = new kapsuleType(); // To extract defaults return { linkProp: function linkProp(prop) { // link property config return { "default": dummyK[prop](), onChange: function onChange(v, state) { propNames.forEach(function (propName) { return state[propName][prop](v); }); }, triggerUpdate: false }; }, linkMethod: function linkMethod(method) { // link method pass-through return function (state) { for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } var returnVals = []; propNames.forEach(function (propName) { var kapsuleInstance = state[propName]; var returnVal = kapsuleInstance[method].apply(kapsuleInstance, args); if (returnVal !== kapsuleInstance) { returnVals.push(returnVal); } }); return returnVals.length ? returnVals[0] : this; // chain based on the parent object, not the inner kapsule }; } }; } var HOVER_CANVAS_THROTTLE_DELAY = 800; // ms to throttle shadow canvas updates for perf improvement var ZOOM2NODES_FACTOR = 4; // Expose config from forceGraph var bindFG = linkKapsule('forceGraph', CanvasForceGraph); var bindBoth = linkKapsule(['forceGraph', 'shadowGraph'], CanvasForceGraph); var linkedProps = Object.assign.apply(Object, _toConsumableArray(['nodeColor', 'nodeAutoColorBy', 'nodeCanvasObject', 'nodeCanvasObjectMode', 'linkColor', 'linkAutoColorBy', 'linkLineDash', 'linkWidth', 'linkCanvasObject', 'linkCanvasObjectMode', 'linkDirectionalArrowLength', 'linkDirectionalArrowColor', 'linkDirectionalArrowRelPos', 'linkDirectionalParticles', 'linkDirectionalParticleSpeed', 'linkDirectionalParticleWidth', 'linkDirectionalParticleColor', 'dagMode', 'dagLevelDistance', 'dagNodeFilter', 'onDagError', 'd3AlphaMin', 'd3AlphaDecay', 'd3VelocityDecay', 'warmupTicks', 'cooldownTicks', 'cooldownTime', 'onEngineTick', 'onEngineStop'].map(function (p) { return _defineProperty({}, p, bindFG.linkProp(p)); })).concat(_toConsumableArray(['nodeRelSize', 'nodeId', 'nodeVal', 'nodeVisibility', 'linkSource', 'linkTarget', 'linkVisibility', 'linkCurvature'].map(function (p) { return _defineProperty({}, p, bindBoth.linkProp(p)); })))); var linkedMethods = Object.assign.apply(Object, _toConsumableArray(['d3Force', 'd3ReheatSimulation', 'emitParticle'].map(function (p) { return _defineProperty({}, p, bindFG.linkMethod(p)); }))); function adjustCanvasSize(state) { if (state.canvas) { var curWidth = state.canvas.width; var curHeight = state.canvas.height; if (curWidth === 300 && curHeight === 150) { // Default canvas dimensions curWidth = curHeight = 0; } var pxScale = window.devicePixelRatio; // 2 on retina displays curWidth /= pxScale; curHeight /= pxScale; // Resize canvases [state.canvas, state.shadowCanvas].forEach(function (canvas) { // Element size canvas.style.width = "".concat(state.width, "px"); canvas.style.height = "".concat(state.height, "px"); // Memory size (scaled to avoid blurriness) canvas.width = state.width * pxScale; canvas.height = state.height * pxScale; // Normalize coordinate system to use css pixels (on init only) if (!curWidth && !curHeight) { canvas.getContext('2d').scale(pxScale, pxScale); } }); // Relative center panning based on 0,0 var k = zoomTransform(state.canvas).k; state.zoom.translateBy(state.zoom.__baseElem, (state.width - curWidth) / 2 / k, (state.height - curHeight) / 2 / k); state.needsRedraw = true; } } function resetTransform(ctx) { var pxRatio = window.devicePixelRatio; ctx.setTransform(pxRatio, 0, 0, pxRatio, 0, 0); } function clearCanvas(ctx, width, height) { ctx.save(); resetTransform(ctx); // reset transform ctx.clearRect(0, 0, width, height); ctx.restore(); //restore transforms } // var forceGraph = Kapsule({ props: _objectSpread2({ width: { "default": window.innerWidth, onChange: function onChange(_, state) { return adjustCanvasSize(state); }, triggerUpdate: false }, height: { "default": window.innerHeight, onChange: function onChange(_, state) { return adjustCanvasSize(state); }, triggerUpdate: false }, graphData: { "default": { nodes: [], links: [] }, onChange: function onChange(d, state) { [{ type: 'Node', objs: d.nodes }, { type: 'Link', objs: d.links }].forEach(hexIndex); state.forceGraph.graphData(d); state.shadowGraph.graphData(d); function hexIndex(_ref4) { var type = _ref4.type, objs = _ref4.objs; objs.filter(function (d) { if (!d.hasOwnProperty('__indexColor')) return true; var cur = state.colorTracker.lookup(d.__indexColor); return !cur || !cur.hasOwnProperty('d') || cur.d !== d; }).forEach(function (d) { // store object lookup color d.__indexColor = state.colorTracker.register({ type: type, d: d }); }); } }, triggerUpdate: false }, backgroundColor: { onChange: function onChange(color, state) { state.canvas && color && (state.canvas.style.background = color); }, triggerUpdate: false }, nodeLabel: { "default": 'name', triggerUpdate: false }, nodePointerAreaPaint: { onChange: function onChange(paintFn, state) { state.shadowGraph.nodeCanvasObject(!paintFn ? null : function (node, ctx, globalScale) { return paintFn(node, node.__indexColor, ctx, globalScale); }); state.flushShadowCanvas && state.flushShadowCanvas(); }, triggerUpdate: false }, linkPointerAreaPaint: { onChange: function onChange(paintFn, state) { state.shadowGraph.linkCanvasObject(!paintFn ? null : function (link, ctx, globalScale) { return paintFn(link, link.__indexColor, ctx, globalScale); }); state.flushShadowCanvas && state.flushShadowCanvas(); }, triggerUpdate: false }, linkLabel: { "default": 'name', triggerUpdate: false }, linkHoverPrecision: { "default": 4, triggerUpdate: false }, minZoom: { "default": 0.01, onChange: function onChange(minZoom, state) { state.zoom.scaleExtent([minZoom, state.zoom.scaleExtent()[1]]); }, triggerUpdate: false }, maxZoom: { "default": 1000, onChange: function onChange(maxZoom, state) { state.zoom.scaleExtent([state.zoom.scaleExtent()[0], maxZoom]); }, triggerUpdate: false }, enableNodeDrag: { "default": true, triggerUpdate: false }, enableZoomInteraction: { "default": true, triggerUpdate: false }, enablePanInteraction: { "default": true, triggerUpdate: false }, enableZoomPanInteraction: { "default": true, triggerUpdate: false }, // to be deprecated enablePointerInteraction: { "default": true, onChange: function onChange(_, state) { state.hoverObj = null; }, triggerUpdate: false }, autoPauseRedraw: { "default": true, triggerUpdate: false }, onNodeDrag: { "default": function _default() {}, triggerUpdate: false }, onNodeDragEnd: { "default": function _default() {}, triggerUpdate: false }, onNodeClick: { triggerUpdate: false }, onNodeRightClick: { triggerUpdate: false }, onNodeHover: { triggerUpdate: false }, onLinkClick: { triggerUpdate: false }, onLinkRightClick: { triggerUpdate: false }, onLinkHover: { triggerUpdate: false }, onBackgroundClick: { triggerUpdate: false }, onBackgroundRightClick: { triggerUpdate: false }, onZoom: { triggerUpdate: false }, onZoomEnd: { triggerUpdate: false }, onRenderFramePre: { triggerUpdate: false }, onRenderFramePost: { triggerUpdate: false } }, linkedProps), aliases: { // Prop names supported for backwards compatibility stopAnimation: 'pauseAnimation' }, methods: _objectSpread2({ graph2ScreenCoords: function graph2ScreenCoords(state, x, y) { var t = zoomTransform(state.canvas); return { x: x * t.k + t.x, y: y * t.k + t.y }; }, screen2GraphCoords: function screen2GraphCoords(state, x, y) { var t = zoomTransform(state.canvas); return { x: (x - t.x) / t.k, y: (y - t.y) / t.k }; }, centerAt: function centerAt(state, x, y, transitionDuration) { if (!state.canvas) return null; // no canvas yet // setter if (x !== undefined || y !== undefined) { var finalPos = Object.assign({}, x !== undefined ? { x: x } : {}, y !== undefined ? { y: y } : {}); if (!transitionDuration) { // no animation setCenter(finalPos); } else { new TWEEN.Tween(getCenter()).to(finalPos, transitionDuration).easing(TWEEN.Easing.Quadratic.Out).onUpdate(setCenter).start(); } return this; } // getter return getCenter(); // function getCenter() { var t = zoomTransform(state.canvas); return { x: (state.width / 2 - t.x) / t.k, y: (state.height / 2 - t.y) / t.k }; } function setCenter(_ref5) { var x = _ref5.x, y = _ref5.y; state.zoom.translateTo(state.zoom.__baseElem, x === undefined ? getCenter().x : x, y === undefined ? getCenter().y : y); state.needsRedraw = true; } }, zoom: function zoom(state, k, transitionDuration) { if (!state.canvas) return null; // no canvas yet // setter if (k !== undefined) { if (!transitionDuration) { // no animation setZoom(k); } else { new TWEEN.Tween({ k: getZoom() }).to({ k: k }, transitionDuration).easing(TWEEN.Easing.Quadratic.Out).onUpdate(function (_ref6) { var k = _ref6.k; return setZoom(k); }).start(); } return this; } // getter return getZoom(); // function getZoom() { return zoomTransform(state.canvas).k; } function setZoom(k) { state.zoom.scaleTo(state.zoom.__baseElem, k); state.needsRedraw = true; } }, zoomToFit: function zoomToFit(state) { var transitionDuration = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; var padding = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 10; for (var _len = arguments.length, bboxArgs = new Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) { bboxArgs[_key - 3] = arguments[_key]; } var bbox = this.getGraphBbox.apply(this, bboxArgs); if (bbox) { var center = { x: (bbox.x[0] + bbox.x[1]) / 2, y: (bbox.y[0] + bbox.y[1]) / 2 }; var zoomK = Math.max(1e-12, Math.min(1e12, (state.width - padding * 2) / (bbox.x[1] - bbox.x[0]), (state.height - padding * 2) / (bbox.y[1] - bbox.y[0]))); this.centerAt(center.x, center.y, transitionDuration); this.zoom(zoomK, transitionDuration); } return this; }, getGraphBbox: function getGraphBbox(state) { var nodeFilter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () { return true; }; var getVal = accessorFn(state.nodeVal); var getR = function getR(node) { return Math.sqrt(Math.max(0, getVal(node) || 1)) * state.nodeRelSize; }; var nodesPos = state.graphData.nodes.filter(nodeFilter).map(function (node) { return { x: node.x, y: node.y, r: getR(node) }; }); return !nodesPos.length ? null : { x: [min(nodesPos, function (node) { return node.x - node.r; }), max(nodesPos, function (node) { return node.x + node.r; })], y: [min(nodesPos, function (node) { return node.y - node.r; }), max(nodesPos, function (node) { return node.y + node.r; })] }; }, pauseAnimation: function pauseAnimation(state) { if (state.animationFrameRequestId) { cancelAnimationFrame(state.animationFrameRequestId); state.animationFrameRequestId = null; } return this; }, resumeAnimation: function resumeAnimation(state) { if (!state.animationFrameRequestId) { this._animationCycle(); } return this; }, _destructor: function _destructor() { this.pauseAnimation(); this.graphData({ nodes: [], links: [] }); } }, linkedMethods), stateInit: function stateInit() { return { lastSetZoom: 1, zoom: zoom(), forceGraph: new CanvasForceGraph(), shadowGraph: new CanvasForceGraph().cooldownTicks(0).nodeColor('__indexColor').linkColor('__indexColor').isShadow(true), colorTracker: new ColorTracker() // indexed objects for rgb lookup }; }, init: function init(domNode, state) { var _this = this; // Wipe DOM domNode.innerHTML = ''; // Container anchor for canvas and tooltip var container = document.createElement('div'); container.classList.add('force-graph-container'); container.style.position = 'relative'; domNode.appendChild(container); state.canvas = document.createElement('canvas'); if (state.backgroundColor) state.canvas.style.background = state.backgroundColor; container.appendChild(state.canvas); state.shadowCanvas = document.createElement('canvas'); // Show shadow canvas //state.shadowCanvas.style.position = 'absolute'; //state.shadowCanvas.style.top = '0'; //state.shadowCanvas.style.left = '0'; //container.appendChild(state.shadowCanvas); var ctx = state.canvas.getContext('2d'); var shadowCtx = state.shadowCanvas.getContext('2d', { willReadFrequently: true }); var pointerPos = { x: -1e12, y: -1e12 }; var getObjUnderPointer = function getObjUnderPointer() { var obj = null; var pxScale = window.devicePixelRatio; var px = pointerPos.x > 0 && pointerPos.y > 0 ? shadowCtx.getImageData(pointerPos.x * pxScale, pointerPos.y * pxScale, 1, 1) : null; // Lookup object per pixel color px && (obj = state.colorTracker.lookup(px.data)); return obj; }; // Setup node drag interaction select(state.canvas).call(drag().subject(function () { if (!state.enableNodeDrag) { return null; } var obj = getObjUnderPointer(); return obj && obj.type === 'Node' ? obj.d : null; // Only drag nodes }).on('start', function (ev) { var obj = ev.subject; obj.__initialDragPos = { x: obj.x, y: obj.y, fx: obj.fx, fy: obj.fy }; // keep engine running at low intensity throughout drag if (!ev.active) { obj.fx = obj.x; obj.fy = obj.y; // Fix points } // drag cursor state.canvas.classList.add('grabbable'); }).on('drag', function (ev) { var obj = ev.subject; var initPos = obj.__initialDragPos; var dragPos = ev; var k = zoomTransform(state.canvas).k; var translate = { x: initPos.x + (dragPos.x - initPos.x) / k - obj.x, y: initPos.y + (dragPos.y - initPos.y) / k - obj.y }; // Move fx/fy (and x/y) of nodes based on the scaled drag distance since the drag start ['x', 'y'].forEach(function (c) { return obj["f".concat(c)] = obj[c] = initPos[c] + (dragPos[c] - initPos[c]) / k; }); // prevent freeze while dragging state.forceGraph.d3AlphaTarget(0.3) // keep engine running at low intensity throughout drag .resetCountdown(); // prevent freeze while dragging state.isPointerDragging = true; obj.__dragged = true; state.onNodeDrag(obj, translate); }).on('end', function (ev) { var obj = ev.subject; var initPos = obj.__initialDragPos; var translate = { x: obj.x - initPos.x, y: obj.y - initPos.y }; if (initPos.fx === undefined) { obj.fx = undefined; } if (initPos.fy === undefined) { obj.fy = undefined; } delete obj.__initialDragPos; if (state.forceGraph.d3AlphaTarget()) { state.forceGraph.d3AlphaTarget(0) // release engine low intensity .resetCountdown(); // let the engine readjust after releasing fixed nodes } // drag cursor state.canvas.classList.remove('grabbable'); state.isPointerDragging = false; if (obj.__dragged) { delete obj.__dragged; state.onNodeDragEnd(obj, translate); } })); // Setup zoom / pan interaction state.zoom(state.zoom.__baseElem = select(state.canvas)); // Attach controlling elem for easy access state.zoom.__baseElem.on('dblclick.zoom', null); // Disable double-click to zoom state.zoom.filter(function (ev) { return ( // disable zoom interaction !ev.button && state.enableZoomPanInteraction && (state.enableZoomInteraction || ev.type !== 'wheel') && (state.enablePanInteraction || ev.type === 'wheel') ); }).on('zoom', function (ev) { var t = ev.transform; [ctx, shadowCtx].forEach(function (c) { resetTransform(c); c.translate(t.x, t.y); c.scale(t.k, t.k); }); state.onZoom && state.onZoom(_objectSpread2(_objectSpread2({}, t), _this.centerAt())); // report x,y coordinates relative to canvas center state.needsRedraw = true; }).on('end', function (ev) { return state.onZoomEnd && state.onZoomEnd(_objectSpread2(_objectSpread2({}, ev.transform), _this.centerAt())); }); adjustCanvasSize(state); state.forceGraph.onNeedsRedraw(function () { return state.needsRedraw = true; }).onFinishUpdate(function () { // re-zoom, if still in default position (not user modified) if (zoomTransform(state.canvas).k === state.lastSetZoom && state.graphData.nodes.length) { state.zoom.scaleTo(state.zoom.__baseElem, state.lastSetZoom = ZOOM2NODES_FACTOR / Math.cbrt(state.graphData.nodes.length)); state.needsRedraw = true; } }); // Setup tooltip var toolTipElem = document.createElement('div'); toolTipElem.classList.add('graph-tooltip'); container.appendChild(toolTipElem); // Capture pointer coords on move or touchstart ['pointermove', 'pointerdown'].forEach(function (evType) { return container.addEventListener(evType, function (ev) { if (evType === 'pointerdown') { state.isPointerPressed = true; // track click state state.pointerDownEvent = ev; } // detect pointer drag on canvas pan !state.isPointerDragging && ev.type === 'pointermove' && state.onBackgroundClick // only bother detecting drags this way if background clicks are enabled (so they don't trigger accidentally on canvas panning) && (ev.pressure > 0 || state.isPointerPressed) // ev.pressure always 0 on Safari, so we use the isPointerPressed tracker && (ev.pointerType !== 'touch' || ev.movementX === undefined || [ev.movementX, ev.movementY].some(function (m) { return Math.abs(m) > 1; })) // relax drag trigger sensitivity on touch events && (state.isPointerDragging = true); // update the pointer pos var offset = getOffset(container); pointerPos.x = ev.pageX - offset.left; pointerPos.y = ev.pageY - offset.top; // Move tooltip toolTipElem.style.top = "".concat(pointerPos.y, "px"); toolTipElem.style.left = "".concat(pointerPos.x, "px"); // adjust horizontal position to not exceed canvas boundaries toolTipElem.style.transform = "translate(-".concat(pointerPos.x / state.width * 100, "%, ").concat( // flip to above if near bottom state.height - pointerPos.y < 100 ? 'calc(-100% - 8px)' : '21px', ")"); // function getOffset(el) { var rect = el.getBoundingClientRect(), scrollLeft = window.pageXOffset || document.documentElement.scrollLeft, scrollTop = window.pageYOffset || document.documentElement.scrollTop; return { top: rect.top + scrollTop, left: rect.left + scrollLeft }; } }, { passive: true }); }); // Handle click/touch events on nodes/links container.addEventListener('pointerup', function (ev) { state.isPointerPressed = false; if (state.isPointerDragging) { state.isPointerDragging = false; return; // don't trigger click events after pointer drag (pan / node drag functionality) } var cbEvents = [ev, state.pointerDownEvent]; requestAnimationFrame(function () { // trigger click events asynchronously, to allow hoverObj to be set (on frame) if (ev.button === 0) { // mouse left-click or touch if (state.hoverObj) { var fn = state["on".concat(state.hoverObj.type, "Click")]; fn && fn.apply(void 0, [state.hoverObj.d].concat(cbEvents)); } else { state.onBackgroundClick && state.onBackgroundClick.apply(state, cbEvents); } } if (ev.button === 2) { // mouse right-click if (state.hoverObj) { var _fn = state["on".concat(state.hoverObj.type, "RightClick")]; _fn && _fn.apply(void 0, [state.hoverObj.d].concat(cbEvents)); } else { state.onBackgroundRightClick && state.onBackgroundRightClick.apply(state, cbEvents); } } }); }, { passive: true }); container.addEventListener('contextmenu', function (ev) { if (!state.onBackgroundRightClick && !state.onNodeRightClick && !state.onLinkRightClick) return true; // default contextmenu behavior ev.preventDefault(); return false; }); state.forceGraph(ctx); state.shadowGraph(shadowCtx); // var refreshShadowCanvas = throttle(function () { // wipe canvas clearCanvas(shadowCtx, state.width, state.height); // Adjust link hover area state.shadowGraph.linkWidth(function (l) { return accessorFn(state.linkWidth)(l) + state.linkHoverPrecision; }); // redraw var t = zoomTransform(state.canvas); state.shadowGraph.globalScale(t.k).tickFrame(); }, HOVER_CANVAS_THROTTLE_DELAY); state.flushShadowCanvas = refreshShadowCanvas.flush; // hook to immediately invoke shadow canvas paint // Kick-off renderer (this._animationCycle = function animate() { // IIFE var doRedraw = !state.autoPauseRedraw || !!state.needsRedraw || state.forceGraph.isEngineRunning() || state.graphData.links.some(function (d) { return d.__photons && d.__photons.length; }); state.needsRedraw = false; if (state.enablePointerInteraction) { // Update tooltip and trigger onHover events var obj = !state.isPointerDragging ? getObjUnderPointer() : null; // don't hover during drag if (obj !== state.hoverObj) { var prevObj = state.hoverObj; var prevObjType = prevObj ? prevObj.type : null; var objType = obj ? obj.type : null; if (prevObjType && prevObjType !== objType) { // Hover out var fn = state["on".concat(prevObjType, "Hover")]; fn && fn(null, prevObj.d); } if (objType) { // Hover in var _fn2 = state["on".concat(objType, "Hover")]; _fn2 && _fn2(obj.d, prevObjType === objType ? prevObj.d : null); } var tooltipContent = obj ? accessorFn(state["".concat(obj.type.toLowerCase(), "Label")])(obj.d) || '' : ''; toolTipElem.style.visibility = tooltipContent ? 'visible' : 'hidden'; toolTipElem.innerHTML = tooltipContent; // set pointer if hovered object is clickable state.canvas.classList[obj && state["on".concat(objType, "Click")] || !obj && state.onBackgroundClick ? 'add' : 'remove']('clickable'); state.hoverObj = obj; } doRedraw && refreshShadowCanvas(); } if (doRedraw) { // Wipe canvas clearCanvas(ctx, state.width, state.height); // Frame cycle var globalScale = zoomTransform(state.canvas).k; state.onRenderFramePre && state.onRenderFramePre(ctx, globalScale); state.forceGraph.globalScale(globalScale).tickFrame(); state.onRenderFramePost && state.onRenderFramePost(ctx, globalScale); } TWEEN.update(); // update canvas animation tweens state.animationFrameRequestId = requestAnimationFrame(animate); })(); }, update: function updateFn(state) {} }); export { forceGraph as default };