sigma
Version:
A JavaScript library aimed at visualizing graphs of thousands of nodes and edges.
1,380 lines (1,292 loc) • 133 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var index = require('./index-88310d0d.cjs.dev.js');
var inherits = require('./inherits-04acba6b.cjs.dev.js');
var types_dist_sigmaTypes = require('../types/dist/sigma-types.cjs.dev.js');
var normalization = require('./normalization-02a974d4.cjs.dev.js');
var settings_dist_sigmaSettings = require('../settings/dist/sigma-settings.cjs.dev.js');
var colors = require('./colors-fe6de9d2.cjs.dev.js');
var data = require('./data-24ae515b.cjs.dev.js');
require('events');
require('graphology-utils/is-graph');
/**
* Defaults.
*/
var DEFAULT_ZOOMING_RATIO = 1.5;
/**
* Event types.
*/
/**
* Camera class
*/
var Camera = /*#__PURE__*/function (_TypedEventEmitter) {
function Camera() {
var _this;
inherits._classCallCheck(this, Camera);
_this = inherits._callSuper(this, Camera);
// State
index._defineProperty(_this, "x", 0.5);
index._defineProperty(_this, "y", 0.5);
index._defineProperty(_this, "angle", 0);
index._defineProperty(_this, "ratio", 1);
index._defineProperty(_this, "minRatio", null);
index._defineProperty(_this, "maxRatio", null);
index._defineProperty(_this, "enabledZooming", true);
index._defineProperty(_this, "enabledPanning", true);
index._defineProperty(_this, "enabledRotation", true);
index._defineProperty(_this, "clean", null);
index._defineProperty(_this, "nextFrame", null);
index._defineProperty(_this, "previousState", null);
index._defineProperty(_this, "enabled", true);
_this.previousState = _this.getState();
return _this;
}
/**
* Static method used to create a Camera object with a given state.
*/
inherits._inherits(Camera, _TypedEventEmitter);
return inherits._createClass(Camera, [{
key: "enable",
value:
/**
* Method used to enable the camera.
*/
function enable() {
this.enabled = true;
return this;
}
/**
* Method used to disable the camera.
*/
}, {
key: "disable",
value: function disable() {
this.enabled = false;
return this;
}
/**
* Method used to retrieve the camera's current state.
*/
}, {
key: "getState",
value: function getState() {
return {
x: this.x,
y: this.y,
angle: this.angle,
ratio: this.ratio
};
}
/**
* Method used to check whether the camera has the given state.
*/
}, {
key: "hasState",
value: function hasState(state) {
return this.x === state.x && this.y === state.y && this.ratio === state.ratio && this.angle === state.angle;
}
/**
* Method used to retrieve the camera's previous state.
*/
}, {
key: "getPreviousState",
value: function getPreviousState() {
var state = this.previousState;
if (!state) return null;
return {
x: state.x,
y: state.y,
angle: state.angle,
ratio: state.ratio
};
}
/**
* Method used to check minRatio and maxRatio values.
*/
}, {
key: "getBoundedRatio",
value: function getBoundedRatio(ratio) {
var r = ratio;
if (typeof this.minRatio === "number") r = Math.max(r, this.minRatio);
if (typeof this.maxRatio === "number") r = Math.min(r, this.maxRatio);
return r;
}
/**
* Method used to check various things to return a legit state candidate.
*/
}, {
key: "validateState",
value: function validateState(state) {
var validatedState = {};
if (this.enabledPanning && typeof state.x === "number") validatedState.x = state.x;
if (this.enabledPanning && typeof state.y === "number") validatedState.y = state.y;
if (this.enabledZooming && typeof state.ratio === "number") validatedState.ratio = this.getBoundedRatio(state.ratio);
if (this.enabledRotation && typeof state.angle === "number") validatedState.angle = state.angle;
return this.clean ? this.clean(index._objectSpread2(index._objectSpread2({}, this.getState()), validatedState)) : validatedState;
}
/**
* Method used to check whether the camera is currently being animated.
*/
}, {
key: "isAnimated",
value: function isAnimated() {
return !!this.nextFrame;
}
/**
* Method used to set the camera's state.
*/
}, {
key: "setState",
value: function setState(state) {
if (!this.enabled) return this;
// Keeping track of last state
this.previousState = this.getState();
var validState = this.validateState(state);
if (typeof validState.x === "number") this.x = validState.x;
if (typeof validState.y === "number") this.y = validState.y;
if (typeof validState.ratio === "number") this.ratio = validState.ratio;
if (typeof validState.angle === "number") this.angle = validState.angle;
// Emitting
if (!this.hasState(this.previousState)) this.emit("updated", this.getState());
return this;
}
/**
* Method used to update the camera's state using a function.
*/
}, {
key: "updateState",
value: function updateState(updater) {
this.setState(updater(this.getState()));
return this;
}
/**
* Method used to animate the camera.
*/
}, {
key: "animate",
value: function animate(state) {
var _this2 = this;
var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var callback = arguments.length > 2 ? arguments[2] : undefined;
if (!callback) return new Promise(function (resolve) {
return _this2.animate(state, opts, resolve);
});
if (!this.enabled) return;
var options = index._objectSpread2(index._objectSpread2({}, normalization.ANIMATE_DEFAULTS), opts);
var validState = this.validateState(state);
var easing = typeof options.easing === "function" ? options.easing : normalization.easings[options.easing];
// State
var start = Date.now(),
initialState = this.getState();
// Function performing the animation
var _fn = function fn() {
var t = (Date.now() - start) / options.duration;
// The animation is over:
if (t >= 1) {
_this2.nextFrame = null;
_this2.setState(validState);
if (_this2.animationCallback) {
_this2.animationCallback.call(null);
_this2.animationCallback = undefined;
}
return;
}
var coefficient = easing(t);
var newState = {};
if (typeof validState.x === "number") newState.x = initialState.x + (validState.x - initialState.x) * coefficient;
if (typeof validState.y === "number") newState.y = initialState.y + (validState.y - initialState.y) * coefficient;
if (_this2.enabledRotation && typeof validState.angle === "number") newState.angle = initialState.angle + (validState.angle - initialState.angle) * coefficient;
if (typeof validState.ratio === "number") newState.ratio = initialState.ratio + (validState.ratio - initialState.ratio) * coefficient;
_this2.setState(newState);
_this2.nextFrame = requestAnimationFrame(_fn);
};
if (this.nextFrame) {
cancelAnimationFrame(this.nextFrame);
if (this.animationCallback) this.animationCallback.call(null);
this.nextFrame = requestAnimationFrame(_fn);
} else {
_fn();
}
this.animationCallback = callback;
}
/**
* Method used to zoom the camera.
*/
}, {
key: "animatedZoom",
value: function animatedZoom(factorOrOptions) {
if (!factorOrOptions) return this.animate({
ratio: this.ratio / DEFAULT_ZOOMING_RATIO
});
if (typeof factorOrOptions === "number") return this.animate({
ratio: this.ratio / factorOrOptions
});
return this.animate({
ratio: this.ratio / (factorOrOptions.factor || DEFAULT_ZOOMING_RATIO)
}, factorOrOptions);
}
/**
* Method used to unzoom the camera.
*/
}, {
key: "animatedUnzoom",
value: function animatedUnzoom(factorOrOptions) {
if (!factorOrOptions) return this.animate({
ratio: this.ratio * DEFAULT_ZOOMING_RATIO
});
if (typeof factorOrOptions === "number") return this.animate({
ratio: this.ratio * factorOrOptions
});
return this.animate({
ratio: this.ratio * (factorOrOptions.factor || DEFAULT_ZOOMING_RATIO)
}, factorOrOptions);
}
/**
* Method used to reset the camera.
*/
}, {
key: "animatedReset",
value: function animatedReset(options) {
return this.animate({
x: 0.5,
y: 0.5,
ratio: 1,
angle: 0
}, options);
}
/**
* Returns a new Camera instance, with the same state as the current camera.
*/
}, {
key: "copy",
value: function copy() {
return Camera.from(this.getState());
}
}], [{
key: "from",
value: function from(state) {
var camera = new Camera();
return camera.setState(state);
}
}]);
}(types_dist_sigmaTypes.TypedEventEmitter);
/**
* Captor utils functions
* ======================
*/
/**
* Extract the local X and Y coordinates from a mouse event or touch object. If
* a DOM element is given, it uses this element's offset to compute the position
* (this allows using events that are not bound to the container itself and
* still have a proper position).
*
* @param {event} e - A mouse event or touch object.
* @param {HTMLElement} dom - A DOM element to compute offset relatively to.
* @return {number} The local Y value of the mouse.
*/
function getPosition(e, dom) {
var bbox = dom.getBoundingClientRect();
return {
x: e.clientX - bbox.left,
y: e.clientY - bbox.top
};
}
/**
* Convert mouse coords to sigma coords.
*
* @param {event} e - A mouse event or touch object.
* @param {HTMLElement} dom - A DOM element to compute offset relatively to.
* @return {object}
*/
function getMouseCoords(e, dom) {
var res = index._objectSpread2(index._objectSpread2({}, getPosition(e, dom)), {}, {
sigmaDefaultPrevented: false,
preventSigmaDefault: function preventSigmaDefault() {
res.sigmaDefaultPrevented = true;
},
original: e
});
return res;
}
/**
* Takes a touch coords or a mouse coords, and always returns a clean mouse coords object.
*/
function cleanMouseCoords(e) {
var res = "x" in e ? e : index._objectSpread2(index._objectSpread2({}, e.touches[0] || e.previousTouches[0]), {}, {
original: e.original,
sigmaDefaultPrevented: e.sigmaDefaultPrevented,
preventSigmaDefault: function preventSigmaDefault() {
e.sigmaDefaultPrevented = true;
res.sigmaDefaultPrevented = true;
}
});
return res;
}
/**
* Convert mouse wheel event coords to sigma coords.
*
* @param {event} e - A wheel mouse event.
* @param {HTMLElement} dom - A DOM element to compute offset relatively to.
* @return {object}
*/
function getWheelCoords(e, dom) {
return index._objectSpread2(index._objectSpread2({}, getMouseCoords(e, dom)), {}, {
delta: getWheelDelta(e)
});
}
var MAX_TOUCHES = 2;
function getTouchesArray(touches) {
var arr = [];
for (var i = 0, l = Math.min(touches.length, MAX_TOUCHES); i < l; i++) arr.push(touches[i]);
return arr;
}
/**
* Convert touch coords to sigma coords.
*
* @param {event} e - A touch event.
* @param {Touch[]} previousTouches - An array of the previously stored touches.
* @param {HTMLElement} dom - A DOM element to compute offset relatively to.
* @return {object}
*/
function getTouchCoords(e, previousTouches, dom) {
var res = {
touches: getTouchesArray(e.touches).map(function (touch) {
return getPosition(touch, dom);
}),
previousTouches: previousTouches.map(function (touch) {
return getPosition(touch, dom);
}),
sigmaDefaultPrevented: false,
preventSigmaDefault: function preventSigmaDefault() {
res.sigmaDefaultPrevented = true;
},
original: e
};
return res;
}
/**
* Extract the wheel delta from a mouse event or touch object.
*
* @param {event} e - A mouse event or touch object.
* @return {number} The wheel delta of the mouse.
*/
function getWheelDelta(e) {
// TODO: check those ratios again to ensure a clean Chrome/Firefox compat
if (typeof e.deltaY !== "undefined") return e.deltaY * -3 / 360;
if (typeof e.detail !== "undefined") return e.detail / -9;
throw new Error("Captor: could not extract delta from event.");
}
/**
* Abstract class representing a captor like the user's mouse or touch controls.
*/
var Captor = /*#__PURE__*/function (_TypedEventEmitter) {
function Captor(container, renderer) {
var _this;
inherits._classCallCheck(this, Captor);
_this = inherits._callSuper(this, Captor);
// Properties
_this.container = container;
_this.renderer = renderer;
return _this;
}
inherits._inherits(Captor, _TypedEventEmitter);
return inherits._createClass(Captor);
}(types_dist_sigmaTypes.TypedEventEmitter);
var MOUSE_SETTINGS_KEYS = ["doubleClickTimeout", "doubleClickZoomingDuration", "doubleClickZoomingRatio", "dragTimeout", "draggedEventsTolerance", "inertiaDuration", "inertiaRatio", "zoomDuration", "zoomingRatio"];
var DEFAULT_MOUSE_SETTINGS = MOUSE_SETTINGS_KEYS.reduce(function (iter, key) {
return index._objectSpread2(index._objectSpread2({}, iter), {}, index._defineProperty({}, key, settings_dist_sigmaSettings.DEFAULT_SETTINGS[key]));
}, {});
/**
* Event types.
*/
/**
* Mouse captor class.
*
* @constructor
*/
var MouseCaptor = /*#__PURE__*/function (_Captor) {
function MouseCaptor(container, renderer) {
var _this;
inherits._classCallCheck(this, MouseCaptor);
_this = inherits._callSuper(this, MouseCaptor, [container, renderer]);
// Binding methods
// State
index._defineProperty(_this, "enabled", true);
index._defineProperty(_this, "draggedEvents", 0);
index._defineProperty(_this, "downStartTime", null);
index._defineProperty(_this, "lastMouseX", null);
index._defineProperty(_this, "lastMouseY", null);
index._defineProperty(_this, "isMouseDown", false);
index._defineProperty(_this, "isMoving", false);
index._defineProperty(_this, "movingTimeout", null);
index._defineProperty(_this, "startCameraState", null);
index._defineProperty(_this, "clicks", 0);
index._defineProperty(_this, "doubleClickTimeout", null);
index._defineProperty(_this, "currentWheelDirection", 0);
index._defineProperty(_this, "settings", DEFAULT_MOUSE_SETTINGS);
_this.handleClick = _this.handleClick.bind(_this);
_this.handleRightClick = _this.handleRightClick.bind(_this);
_this.handleDown = _this.handleDown.bind(_this);
_this.handleUp = _this.handleUp.bind(_this);
_this.handleMove = _this.handleMove.bind(_this);
_this.handleWheel = _this.handleWheel.bind(_this);
_this.handleLeave = _this.handleLeave.bind(_this);
_this.handleEnter = _this.handleEnter.bind(_this);
// Binding events
container.addEventListener("click", _this.handleClick, {
capture: false
});
container.addEventListener("contextmenu", _this.handleRightClick, {
capture: false
});
container.addEventListener("mousedown", _this.handleDown, {
capture: false
});
container.addEventListener("wheel", _this.handleWheel, {
capture: false
});
container.addEventListener("mouseleave", _this.handleLeave, {
capture: false
});
container.addEventListener("mouseenter", _this.handleEnter, {
capture: false
});
document.addEventListener("mousemove", _this.handleMove, {
capture: false
});
document.addEventListener("mouseup", _this.handleUp, {
capture: false
});
return _this;
}
inherits._inherits(MouseCaptor, _Captor);
return inherits._createClass(MouseCaptor, [{
key: "kill",
value: function kill() {
var container = this.container;
container.removeEventListener("click", this.handleClick);
container.removeEventListener("contextmenu", this.handleRightClick);
container.removeEventListener("mousedown", this.handleDown);
container.removeEventListener("wheel", this.handleWheel);
container.removeEventListener("mouseleave", this.handleLeave);
container.removeEventListener("mouseenter", this.handleEnter);
document.removeEventListener("mousemove", this.handleMove);
document.removeEventListener("mouseup", this.handleUp);
}
}, {
key: "handleClick",
value: function handleClick(e) {
var _this2 = this;
if (!this.enabled) return;
this.clicks++;
if (this.clicks === 2) {
this.clicks = 0;
if (typeof this.doubleClickTimeout === "number") {
clearTimeout(this.doubleClickTimeout);
this.doubleClickTimeout = null;
}
return this.handleDoubleClick(e);
}
setTimeout(function () {
_this2.clicks = 0;
_this2.doubleClickTimeout = null;
}, this.settings.doubleClickTimeout);
// NOTE: this is here to prevent click events on drag
if (this.draggedEvents < this.settings.draggedEventsTolerance) this.emit("click", getMouseCoords(e, this.container));
}
}, {
key: "handleRightClick",
value: function handleRightClick(e) {
if (!this.enabled) return;
this.emit("rightClick", getMouseCoords(e, this.container));
}
}, {
key: "handleDoubleClick",
value: function handleDoubleClick(e) {
if (!this.enabled) return;
e.preventDefault();
e.stopPropagation();
var mouseCoords = getMouseCoords(e, this.container);
this.emit("doubleClick", mouseCoords);
if (mouseCoords.sigmaDefaultPrevented) return;
// default behavior
var camera = this.renderer.getCamera();
var newRatio = camera.getBoundedRatio(camera.getState().ratio / this.settings.doubleClickZoomingRatio);
camera.animate(this.renderer.getViewportZoomedState(getPosition(e, this.container), newRatio), {
easing: "quadraticInOut",
duration: this.settings.doubleClickZoomingDuration
});
}
}, {
key: "handleDown",
value: function handleDown(e) {
if (!this.enabled) return;
// We only start dragging on left button
if (e.button === 0) {
this.startCameraState = this.renderer.getCamera().getState();
var _getPosition = getPosition(e, this.container),
x = _getPosition.x,
y = _getPosition.y;
this.lastMouseX = x;
this.lastMouseY = y;
this.draggedEvents = 0;
this.downStartTime = Date.now();
this.isMouseDown = true;
}
this.emit("mousedown", getMouseCoords(e, this.container));
}
}, {
key: "handleUp",
value: function handleUp(e) {
var _this3 = this;
if (!this.enabled || !this.isMouseDown) return;
var camera = this.renderer.getCamera();
this.isMouseDown = false;
if (typeof this.movingTimeout === "number") {
clearTimeout(this.movingTimeout);
this.movingTimeout = null;
}
var _getPosition2 = getPosition(e, this.container),
x = _getPosition2.x,
y = _getPosition2.y;
var cameraState = camera.getState(),
previousCameraState = camera.getPreviousState() || {
x: 0,
y: 0
};
if (this.isMoving) {
camera.animate({
x: cameraState.x + this.settings.inertiaRatio * (cameraState.x - previousCameraState.x),
y: cameraState.y + this.settings.inertiaRatio * (cameraState.y - previousCameraState.y)
}, {
duration: this.settings.inertiaDuration,
easing: "quadraticOut"
});
} else if (this.lastMouseX !== x || this.lastMouseY !== y) {
camera.setState({
x: cameraState.x,
y: cameraState.y
});
}
this.isMoving = false;
setTimeout(function () {
var shouldRefresh = _this3.draggedEvents > 0;
_this3.draggedEvents = 0;
// NOTE: this refresh is here to make sure `hideEdgesOnMove` can work
// when someone releases camera pan drag after having stopped moving.
// See commit: https://github.com/jacomyal/sigma.js/commit/cfd9197f70319109db6b675dd7c82be493ca95a2
// See also issue: https://github.com/jacomyal/sigma.js/issues/1290
// It could be possible to render instead of scheduling a refresh but for
// now it seems good enough.
if (shouldRefresh && _this3.renderer.getSetting("hideEdgesOnMove")) _this3.renderer.refresh();
}, 0);
this.emit("mouseup", getMouseCoords(e, this.container));
}
}, {
key: "handleMove",
value: function handleMove(e) {
var _this4 = this;
if (!this.enabled) return;
var mouseCoords = getMouseCoords(e, this.container);
// Always trigger a "mousemovebody" event, so that it is possible to develop
// a drag-and-drop effect that works even when the mouse is out of the
// container:
this.emit("mousemovebody", mouseCoords);
// Only trigger the "mousemove" event when the mouse is actually hovering
// the container, to avoid weirdly hovering nodes and/or edges when the
// mouse is not hover the container:
if (e.target === this.container || e.composedPath()[0] === this.container) {
this.emit("mousemove", mouseCoords);
}
if (mouseCoords.sigmaDefaultPrevented) return;
// Handle the case when "isMouseDown" all the time, to allow dragging the
// stage while the mouse is not hover the container:
if (this.isMouseDown) {
this.isMoving = true;
this.draggedEvents++;
if (typeof this.movingTimeout === "number") {
clearTimeout(this.movingTimeout);
}
this.movingTimeout = window.setTimeout(function () {
_this4.movingTimeout = null;
_this4.isMoving = false;
}, this.settings.dragTimeout);
var camera = this.renderer.getCamera();
var _getPosition3 = getPosition(e, this.container),
eX = _getPosition3.x,
eY = _getPosition3.y;
var lastMouse = this.renderer.viewportToFramedGraph({
x: this.lastMouseX,
y: this.lastMouseY
});
var mouse = this.renderer.viewportToFramedGraph({
x: eX,
y: eY
});
var offsetX = lastMouse.x - mouse.x,
offsetY = lastMouse.y - mouse.y;
var cameraState = camera.getState();
var x = cameraState.x + offsetX,
y = cameraState.y + offsetY;
camera.setState({
x: x,
y: y
});
this.lastMouseX = eX;
this.lastMouseY = eY;
e.preventDefault();
e.stopPropagation();
}
}
}, {
key: "handleLeave",
value: function handleLeave(e) {
this.emit("mouseleave", getMouseCoords(e, this.container));
}
}, {
key: "handleEnter",
value: function handleEnter(e) {
this.emit("mouseenter", getMouseCoords(e, this.container));
}
}, {
key: "handleWheel",
value: function handleWheel(e) {
var _this5 = this;
var camera = this.renderer.getCamera();
if (!this.enabled || !camera.enabledZooming) return;
var delta = getWheelDelta(e);
if (!delta) return;
var wheelCoords = getWheelCoords(e, this.container);
this.emit("wheel", wheelCoords);
if (wheelCoords.sigmaDefaultPrevented) {
e.preventDefault();
e.stopPropagation();
return;
}
// Default behavior
var currentRatio = camera.getState().ratio;
var ratioDiff = delta > 0 ? 1 / this.settings.zoomingRatio : this.settings.zoomingRatio;
var newRatio = camera.getBoundedRatio(currentRatio * ratioDiff);
var wheelDirection = delta > 0 ? 1 : -1;
var now = Date.now();
// Exit early without preventing default behavior when ratio doesn't change:
if (currentRatio === newRatio) return;
e.preventDefault();
e.stopPropagation();
// Cancel events that are too close each other and in the same direction:
if (this.currentWheelDirection === wheelDirection && this.lastWheelTriggerTime && now - this.lastWheelTriggerTime < this.settings.zoomDuration / 5) {
return;
}
camera.animate(this.renderer.getViewportZoomedState(getPosition(e, this.container), newRatio), {
easing: "quadraticOut",
duration: this.settings.zoomDuration
}, function () {
_this5.currentWheelDirection = 0;
});
this.currentWheelDirection = wheelDirection;
this.lastWheelTriggerTime = now;
}
}, {
key: "setSettings",
value: function setSettings(settings) {
this.settings = settings;
}
}]);
}(Captor);
var TOUCH_SETTINGS_KEYS = ["dragTimeout", "inertiaDuration", "inertiaRatio", "doubleClickTimeout", "doubleClickZoomingRatio", "doubleClickZoomingDuration", "tapMoveTolerance"];
var DEFAULT_TOUCH_SETTINGS = TOUCH_SETTINGS_KEYS.reduce(function (iter, key) {
return index._objectSpread2(index._objectSpread2({}, iter), {}, index._defineProperty({}, key, settings_dist_sigmaSettings.DEFAULT_SETTINGS[key]));
}, {});
/**
* Event types.
*/
/**
* Touch captor class.
*
* @constructor
*/
var TouchCaptor = /*#__PURE__*/function (_Captor) {
function TouchCaptor(container, renderer) {
var _this;
inherits._classCallCheck(this, TouchCaptor);
_this = inherits._callSuper(this, TouchCaptor, [container, renderer]);
// Binding methods:
index._defineProperty(_this, "enabled", true);
index._defineProperty(_this, "isMoving", false);
index._defineProperty(_this, "hasMoved", false);
index._defineProperty(_this, "touchMode", 0);
index._defineProperty(_this, "startTouchesPositions", []);
index._defineProperty(_this, "lastTouches", []);
index._defineProperty(_this, "lastTap", null);
index._defineProperty(_this, "settings", DEFAULT_TOUCH_SETTINGS);
_this.handleStart = _this.handleStart.bind(_this);
_this.handleLeave = _this.handleLeave.bind(_this);
_this.handleMove = _this.handleMove.bind(_this);
// Binding events
container.addEventListener("touchstart", _this.handleStart, {
capture: false
});
container.addEventListener("touchcancel", _this.handleLeave, {
capture: false
});
document.addEventListener("touchend", _this.handleLeave, {
capture: false,
passive: false
});
document.addEventListener("touchmove", _this.handleMove, {
capture: false,
passive: false
});
return _this;
}
inherits._inherits(TouchCaptor, _Captor);
return inherits._createClass(TouchCaptor, [{
key: "kill",
value: function kill() {
var container = this.container;
container.removeEventListener("touchstart", this.handleStart);
container.removeEventListener("touchcancel", this.handleLeave);
document.removeEventListener("touchend", this.handleLeave);
document.removeEventListener("touchmove", this.handleMove);
}
}, {
key: "getDimensions",
value: function getDimensions() {
return {
width: this.container.offsetWidth,
height: this.container.offsetHeight
};
}
}, {
key: "handleStart",
value: function handleStart(e) {
var _this2 = this;
if (!this.enabled) return;
e.preventDefault();
var touches = getTouchesArray(e.touches);
this.touchMode = touches.length;
this.startCameraState = this.renderer.getCamera().getState();
this.startTouchesPositions = touches.map(function (touch) {
return getPosition(touch, _this2.container);
});
// When there are two touches down, let's record distance and angle as well:
if (this.touchMode === 2) {
var _this$startTouchesPos = colors._slicedToArray(this.startTouchesPositions, 2),
_this$startTouchesPos2 = _this$startTouchesPos[0],
x0 = _this$startTouchesPos2.x,
y0 = _this$startTouchesPos2.y,
_this$startTouchesPos3 = _this$startTouchesPos[1],
x1 = _this$startTouchesPos3.x,
y1 = _this$startTouchesPos3.y;
this.startTouchesAngle = Math.atan2(y1 - y0, x1 - x0);
this.startTouchesDistance = Math.sqrt(Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2));
}
this.emit("touchdown", getTouchCoords(e, this.lastTouches, this.container));
this.lastTouches = touches;
this.lastTouchesPositions = this.startTouchesPositions;
}
}, {
key: "handleLeave",
value: function handleLeave(e) {
if (!this.enabled || !this.startTouchesPositions.length) return;
if (e.cancelable) e.preventDefault();
if (this.movingTimeout) {
this.isMoving = false;
clearTimeout(this.movingTimeout);
}
switch (this.touchMode) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
case 2:
if (e.touches.length === 1) {
this.handleStart(e);
e.preventDefault();
break;
}
/* falls through */
case 1:
if (this.isMoving) {
var camera = this.renderer.getCamera();
var cameraState = camera.getState(),
previousCameraState = camera.getPreviousState() || {
x: 0,
y: 0
};
camera.animate({
x: cameraState.x + this.settings.inertiaRatio * (cameraState.x - previousCameraState.x),
y: cameraState.y + this.settings.inertiaRatio * (cameraState.y - previousCameraState.y)
}, {
duration: this.settings.inertiaDuration,
easing: "quadraticOut"
});
}
this.hasMoved = false;
this.isMoving = false;
this.touchMode = 0;
break;
}
this.emit("touchup", getTouchCoords(e, this.lastTouches, this.container));
// When the last touch ends and there hasn't been too much movement, trigger a "tap" or "doubletap" event:
if (!e.touches.length) {
var position = getPosition(this.lastTouches[0], this.container);
var downPosition = this.startTouchesPositions[0];
var dSquare = Math.pow(position.x - downPosition.x, 2) + Math.pow(position.y - downPosition.y, 2);
if (!e.touches.length && dSquare < Math.pow(this.settings.tapMoveTolerance, 2)) {
// Only trigger "doubletap" when the last tap is recent enough:
if (this.lastTap && Date.now() - this.lastTap.time < this.settings.doubleClickTimeout) {
var touchCoords = getTouchCoords(e, this.lastTouches, this.container);
this.emit("doubletap", touchCoords);
this.lastTap = null;
if (!touchCoords.sigmaDefaultPrevented) {
var _camera = this.renderer.getCamera();
var newRatio = _camera.getBoundedRatio(_camera.getState().ratio / this.settings.doubleClickZoomingRatio);
_camera.animate(this.renderer.getViewportZoomedState(position, newRatio), {
easing: "quadraticInOut",
duration: this.settings.doubleClickZoomingDuration
});
}
}
// Else, trigger a normal "tap" event:
else {
var _touchCoords = getTouchCoords(e, this.lastTouches, this.container);
this.emit("tap", _touchCoords);
this.lastTap = {
time: Date.now(),
position: _touchCoords.touches[0] || _touchCoords.previousTouches[0]
};
}
}
}
this.lastTouches = getTouchesArray(e.touches);
this.startTouchesPositions = [];
}
}, {
key: "handleMove",
value: function handleMove(e) {
var _this3 = this;
if (!this.enabled || !this.startTouchesPositions.length) return;
e.preventDefault();
var touches = getTouchesArray(e.touches);
var touchesPositions = touches.map(function (touch) {
return getPosition(touch, _this3.container);
});
var lastTouches = this.lastTouches;
this.lastTouches = touches;
this.lastTouchesPositions = touchesPositions;
var touchCoords = getTouchCoords(e, lastTouches, this.container);
this.emit("touchmove", touchCoords);
if (touchCoords.sigmaDefaultPrevented) return;
// If a move was initiated at some point, and we get back to start point,
// we should still consider that we did move (which also happens after a
// multiple touch when only one touch remains in which case handleStart
// is recalled within handleLeave).
// Now, some mobile browsers report zero-distance moves so we also check that
// one of the touches did actually move from the origin position.
this.hasMoved || (this.hasMoved = touchesPositions.some(function (position, idx) {
var startPosition = _this3.startTouchesPositions[idx];
return startPosition && (position.x !== startPosition.x || position.y !== startPosition.y);
}));
// If there was no move, do not trigger touch moves behavior
if (!this.hasMoved) {
return;
}
this.isMoving = true;
if (this.movingTimeout) clearTimeout(this.movingTimeout);
this.movingTimeout = window.setTimeout(function () {
_this3.isMoving = false;
}, this.settings.dragTimeout);
var camera = this.renderer.getCamera();
var startCameraState = this.startCameraState;
var padding = this.renderer.getSetting("stagePadding");
switch (this.touchMode) {
case 1:
{
var _this$renderer$viewpo = this.renderer.viewportToFramedGraph((this.startTouchesPositions || [])[0]),
xStart = _this$renderer$viewpo.x,
yStart = _this$renderer$viewpo.y;
var _this$renderer$viewpo2 = this.renderer.viewportToFramedGraph(touchesPositions[0]),
x = _this$renderer$viewpo2.x,
y = _this$renderer$viewpo2.y;
camera.setState({
x: startCameraState.x + xStart - x,
y: startCameraState.y + yStart - y
});
break;
}
case 2:
{
/**
* Here is the thinking here:
*
* 1. We can find the new angle and ratio, by comparing the vector from "touch one" to "touch two" at the start
* of the d'n'd and now
*
* 2. We can use `Camera#viewportToGraph` inside formula to retrieve the new camera position, using the graph
* position of a touch at the beginning of the d'n'd (using `startCamera.viewportToGraph`) and the viewport
* position of this same touch now
*/
var newCameraState = {
x: 0.5,
y: 0.5,
angle: 0,
ratio: 1
};
var _touchesPositions$ = touchesPositions[0],
x0 = _touchesPositions$.x,
y0 = _touchesPositions$.y;
var _touchesPositions$2 = touchesPositions[1],
x1 = _touchesPositions$2.x,
y1 = _touchesPositions$2.y;
var angleDiff = Math.atan2(y1 - y0, x1 - x0) - this.startTouchesAngle;
var ratioDiff = Math.hypot(y1 - y0, x1 - x0) / this.startTouchesDistance;
// 1.
var newRatio = camera.getBoundedRatio(startCameraState.ratio / ratioDiff);
newCameraState.ratio = newRatio;
newCameraState.angle = startCameraState.angle + angleDiff;
// 2.
var dimensions = this.getDimensions();
var touchGraphPosition = this.renderer.viewportToFramedGraph((this.startTouchesPositions || [])[0], {
cameraState: startCameraState
});
var smallestDimension = Math.min(dimensions.width, dimensions.height) - 2 * padding;
var dx = smallestDimension / dimensions.width;
var dy = smallestDimension / dimensions.height;
var ratio = newRatio / smallestDimension;
// Align with center of the graph:
var _x = x0 - smallestDimension / 2 / dx;
var _y = y0 - smallestDimension / 2 / dy;
// Rotate:
var _ref = [_x * Math.cos(-newCameraState.angle) - _y * Math.sin(-newCameraState.angle), _y * Math.cos(-newCameraState.angle) + _x * Math.sin(-newCameraState.angle)];
_x = _ref[0];
_y = _ref[1];
newCameraState.x = touchGraphPosition.x - _x * ratio;
newCameraState.y = touchGraphPosition.y + _y * ratio;
camera.setState(newCameraState);
break;
}
}
}
}, {
key: "setSettings",
value: function setSettings(settings) {
this.settings = settings;
}
}]);
}(Captor);
function _arrayWithoutHoles(r) {
if (Array.isArray(r)) return colors._arrayLikeToArray(r);
}
function _iterableToArray(r) {
if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
}
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 _toConsumableArray(r) {
return _arrayWithoutHoles(r) || _iterableToArray(r) || colors._unsupportedIterableToArray(r) || _nonIterableSpread();
}
function _objectWithoutPropertiesLoose(r, e) {
if (null == r) return {};
var t = {};
for (var n in r) if ({}.hasOwnProperty.call(r, n)) {
if (-1 !== e.indexOf(n)) continue;
t[n] = r[n];
}
return t;
}
function _objectWithoutProperties(e, t) {
if (null == e) return {};
var o,
r,
i = _objectWithoutPropertiesLoose(e, t);
if (Object.getOwnPropertySymbols) {
var n = Object.getOwnPropertySymbols(e);
for (r = 0; r < n.length; r++) o = n[r], -1 === t.indexOf(o) && {}.propertyIsEnumerable.call(e, o) && (i[o] = e[o]);
}
return i;
}
/**
* Sigma.js Labels Heuristics
* ===========================
*
* Miscellaneous heuristics related to label display.
* @module
*/
/**
* Class representing a single candidate for the label grid selection.
*
* It also describes a deterministic way to compare two candidates to assess
* which one is better.
*/
var LabelCandidate = /*#__PURE__*/function () {
function LabelCandidate(key, size) {
inherits._classCallCheck(this, LabelCandidate);
this.key = key;
this.size = size;
}
return inherits._createClass(LabelCandidate, null, [{
key: "compare",
value: function compare(first, second) {
// First we compare by size
if (first.size > second.size) return -1;
if (first.size < second.size) return 1;
// Then since no two nodes can have the same key, we use it to
// deterministically tie-break by key
if (first.key > second.key) return 1;
// NOTE: this comparator cannot return 0
return -1;
}
}]);
}();
/**
* Class representing a 2D spatial grid divided into constant-size cells.
*/
var LabelGrid = /*#__PURE__*/function () {
function LabelGrid() {
inherits._classCallCheck(this, LabelGrid);
index._defineProperty(this, "width", 0);
index._defineProperty(this, "height", 0);
index._defineProperty(this, "cellSize", 0);
index._defineProperty(this, "columns", 0);
index._defineProperty(this, "rows", 0);
index._defineProperty(this, "cells", {});
}
return inherits._createClass(LabelGrid, [{
key: "resizeAndClear",
value: function resizeAndClear(dimensions, cellSize) {
this.width = dimensions.width;
this.height = dimensions.height;
this.cellSize = cellSize;
this.columns = Math.ceil(dimensions.width / cellSize);
this.rows = Math.ceil(dimensions.height / cellSize);
this.cells = {};
}
}, {
key: "getIndex",
value: function getIndex(pos) {
var xIndex = Math.floor(pos.x / this.cellSize);
var yIndex = Math.floor(pos.y / this.cellSize);
return yIndex * this.columns + xIndex;
}
}, {
key: "add",
value: function add(key, size, pos) {
var candidate = new LabelCandidate(key, size);
var index = this.getIndex(pos);
var cell = this.cells[index];
if (!cell) {
cell = [];
this.cells[index] = cell;
}
cell.push(candidate);
}
}, {
key: "organize",
value: function organize() {
for (var k in this.cells) {
var cell = this.cells[k];
cell.sort(LabelCandidate.compare);
}
}
}, {
key: "getLabelsToDisplay",
value: function getLabelsToDisplay(ratio, density) {
// TODO: work on visible nodes to optimize? ^ -> threshold outside so that memoization works?
// TODO: adjust threshold lower, but increase cells a bit?
// TODO: hunt for geom issue in disguise
// TODO: memoize while ratio does not move. method to force recompute
var cellArea = this.cellSize * this.cellSize;
var scaledCellArea = cellArea / ratio / ratio;
var scaledDensity = scaledCellArea * density / cellArea;
var labelsToDisplayPerCell = Math.ceil(scaledDensity);
var labels = [];
for (var k in this.cells) {
var cell = this.cells[k];
for (var i = 0; i < Math.min(labelsToDisplayPerCell, cell.length); i++) {
labels.push(cell[i].key);
}
}
return labels;
}
}]);
}();
/**
* Label heuristic selecting edge labels to display, based on displayed node
* labels
*
* @param {object} params - Parameters:
* @param {Set} displayedNodeLabels - Currently displayed node labels.
* @param {Set} highlightedNodes - Highlighted nodes.
* @param {Graph} graph - The rendered graph.
* @param {string} hoveredNode - Hovered node (optional)
* @return {Array} - The selected labels.
*/
function edgeLabelsToDisplayFromNodes(params) {
var graph = params.graph,
hoveredNode = params.hoveredNode,
highlightedNodes = params.highlightedNodes,
displayedNodeLabels = params.displayedNodeLabels;
var worthyEdges = [];
// TODO: the code below can be optimized using #.forEach and batching the code per adj
// We should display an edge's label if:
// - Any of its extremities is highlighted or hovered
// - Both of its extremities has its label shown
graph.forEachEdge(function (edge, _, source, target) {
if (source === hoveredNode || target === hoveredNode || highlightedNodes.has(source) || highlightedNodes.has(target) || displayedNodeLabels.has(source) && displayedNodeLabels.has(target)) {
worthyEdges.push(edge);
}
});
return worthyEdges;
}
/**
* Constants.
*/
var X_LABEL_MARGIN = 150;
var Y_LABEL_MARGIN = 50;
var hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* Important functions.
*/
function applyNodeDefaults(settings, key, data) {
if (!hasOwnProperty.call(data, "x") || !hasOwnProperty.call(data, "y")) throw new Error("Sigma: could not find a valid position (x, y) for node \"".concat(key, "\". All your nodes must have a number \"x\" and \"y\". Maybe your forgot to apply a layout or your \"nodeReducer\" is not returning the correct data?"));
if (!data.color) data.color = settings.defaultNodeColor;
if (!data.label && data.label !== "") data.label = null;
if (data.label !== undefined && data.label !== null) data.label = "" + data.label;else data.label = null;
if (!data.size) data.size = 2;
if (!hasOwnProperty.call(data, "hidden")) data.hidden = false;
if (!hasOwnProperty.call(data, "highlighted")) data.highlighted = false;
if (!hasOwnProperty.call(data, "forceLabel")) data.forceLabel = false;
if (!data.type || data.type === "") data.type = settings.defaultNodeType;
if (!data.zIndex) data.zIndex = 0;
return data;
}
function applyEdgeDefaults(settings, _key, data) {
if (!data.color) data.color = settings.defaultEdgeColor;
if (!data.label) data.label = "";
if (!data.size) data.size = 0.5;
if (!hasOwnProperty.call(data, "hidden")) data.hidden = false;
if (!hasOwnProperty.call(data, "forceLabel")) data.forceLabel = false;
if (!data.type || data.type === "") data.type = settings.defaultEdgeType;
if (!data.zIndex) data.zIndex = 0;
return data;
}
/**
* Main class.
*
* @constructor
* @param {Graph} graph - Graph to render.
* @param {HTMLElement} container - DOM container in which to render.
* @param {object} settings - Optional settings.
*/
var Sigma$1 = /*#__PURE__*/function (_TypedEventEmitter) {
function Sigma(graph, container) {
var _this;
var settings = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
inherits._classCallCheck(this, Sigma);
_this = inherits._callSuper(this, Sigma);
// Resolving settings
index._defineProperty(_this, "elements", {});
index._defineProperty(_this, "canvasContexts", {});
index._defineProperty(_this, "webGLContexts", {});
index._defineProperty(_this, "pickingLayers", new Set());
index._defineProperty(_this, "textures", {});
index._defineProperty(_this, "frameBuffers", {});
index._defineProperty(_this, "activeListeners", {});
index._defineProperty(_this, "labelGrid", new LabelGrid());
index._defineProperty(_this, "nodeDataCache", {});
index._defineProperty(_this, "edgeDataCache", {});
// Indices to keep track of the index of the item inside programs
index._defineProperty(_this, "nodeProgramIndex", {});
index._defineProperty(_this, "edgeProgramIndex", {});
index._defineProperty(_this, "nodesWithForcedLabels", new Set());
index._defineProperty(_this, "edgesWithForcedLabels", new Set());
index._defineProperty(_this, "nodeExtent", {
x: [0, 1],
y: [0, 1]
});
index._defineProperty(_this, "nodeZExtent", [Infinity, -Infinity]);
index._defineProperty(_this, "edgeZExtent", [Infinity, -Infinity]);
index._defineProperty(_this, "matrix", normalization.identity());
index._defineProperty(_this, "invMatrix", normalization.identity());
index._defineProperty(_this, "correctionRatio", 1);
index._defineProperty(_this, "customBBox", null);
index._defineProperty(_this, "normalizationFunction", normalization.createNormalizationFunction({
x: [0, 1],
y: [0, 1]
}));
// Cache:
index._defineProperty(_this, "graphToViewportRatio", 1);
index._defineProperty(_this, "itemIDsIndex", {});
index._defineProperty(_this, "nodeIndices", {});
index._defineProperty(_this, "edgeIndices", {});
// Starting dimensions and pixel ratio
index._defineProperty(_this, "width", 0);
index._defineProperty(_this, "height", 0);
index._defineProperty(_this, "pixelRatio", normalization.getPixelRatio());
index._defineProperty(_this, "pickingDownSizingRatio", 2 * _this.pixelRatio);
// Graph State
index._defineProperty(_this, "displayedNodeLabels", new Set());
index._defineProperty(_this, "displayedEdgeLabels", new Set());
index._defineProperty(_this, "highlightedNodes", new Set());
index._defineProperty(_this, "hoveredNode", null);
index._defineProperty(_this, "hoveredEdge", null);
// Internal states
index._defineProperty(_this, "renderFrame", null);
index._defineProperty(_this, "renderHighlightedNodesFrame", null);
index._defineProperty(_this, "needToProcess", false);
index._defineProperty(_this, "checkEdgesEventsFrame", null);
// Programs
index._defineProperty(_this, "nodePrograms", {});
index._defineProperty(_this, "nodeHoverPrograms", {});
index._defineProperty(_this, "edgePrograms", {});
_this.settings = settings_dist_sigmaSettings.resolveSettings(settings);
// Validating
settings_dist_sigmaSettings.validateSettings(_this.settings);
normalization.validateGraph(graph);
if (!(container instanceof HTMLElement)) throw new Error("Sigma: container should be an html element.");
// Properties
_this.graph = graph;
_this.container = container;
// Initializing contexts
_this.createWebGLContext("edges", {
picking: settings.enableEdgeEvents
});
_this.createCanvasContext("edgeLabels");
_this.createWebGLContext("nodes", {
picking: true
});
_this.createCanvasContext("labels");
_this.createCanvasContext("hovers");
_this.createWebGLContext("hoverNodes");
_this.createCanvasContext("mouse", {
style: {
touchAction: "none",
userSelect: "none"
}
});
// Initial resize
_this.resize();
// Loading programs
for (var type in _this.settings.nodeProgramClasses) {
_this.registerNodeProgram(type, _this.settings.nodeProgramClasses[type], _this.settings.nodeHoverProgramClasses[type]);
}
for (var _type in _this.settings.edgeProgramClasses) {
_this.registerEdgeProgram(_type, _this.settings.edgeProgramClasses[_type]);
}
// Initializing the camera
_this.camera = new Camera();
// Binding camera events
_this.bindCameraHandlers();
// Initializing captors
_this.mouseCaptor = new MouseCaptor(_this.elements.mouse, _this);
_this.mouseCaptor.setSettings(_this.settings);
_this.touchCaptor = new TouchCaptor(_this.elements.mouse, _this);
_this.touchCaptor.setSettings(_this.settings);
// Binding event handlers
_this.bindEventHandlers();
// Binding graph handlers
_this.bindGraphHandlers();
// Trigger eventual settings-related things
_this.handleSettingsUpdate();
// Processing data for the first time & render
_this.refresh();