UNPKG

almus-d3-graph

Version:

React component to build interactive and configurable graphs with d3 effortlessly

632 lines (500 loc) 25.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _react = require("react"); var _react2 = _interopRequireDefault(_react); var _d3Drag = require("d3-drag"); var _d3Force = require("d3-force"); var _d3Selection = require("d3-selection"); var _d3Zoom = require("d3-zoom"); var _graph = require("./graph.const"); var _graph2 = _interopRequireDefault(_graph); var _graph3 = require("./graph.config"); var _graph4 = _interopRequireDefault(_graph3); var _err = require("../../err"); var _err2 = _interopRequireDefault(_err); var _collapse = require("./collapse.helper"); var collapseHelper = _interopRequireWildcard(_collapse); var _graph5 = require("./graph.helper"); var graphHelper = _interopRequireWildcard(_graph5); var _graph6 = require("./graph.renderer"); var graphRenderer = _interopRequireWildcard(_graph6); var _utils = require("../../utils"); var _utils2 = _interopRequireDefault(_utils); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** * Graph component is the main component for react-d3-graph components, its interface allows its user * to build the graph once the user provides the data, configuration (optional) and callback interactions (also optional). * The code for the [live example](https://danielcaldas.github.io/react-d3-graph/sandbox/index.html) * can be consulted [here](https://github.com/danielcaldas/react-d3-graph/blob/master/sandbox/Sandbox.jsx) * @example * import { Graph } from 'react-d3-graph'; * * // graph payload (with minimalist structure) * const data = { * nodes: [ * {id: 'Harry'}, * {id: 'Sally'}, * {id: 'Alice'} * ], * links: [ * {source: 'Harry', target: 'Sally'}, * {source: 'Harry', target: 'Alice'}, * ] * }; * * // the graph configuration, you only need to pass down properties * // that you want to override, otherwise default ones will be used * const myConfig = { * nodeHighlightBehavior: true, * node: { * color: 'lightgreen', * size: 120, * highlightStrokeColor: 'blue' * }, * link: { * highlightColor: 'lightblue' * } * }; * * // graph event callbacks * const onClickGraph = function() { * window.alert('Clicked the graph background'); * }; * * const onClickNode = function(nodeId) { * window.alert('Clicked node ${nodeId}'); * }; * * const onDoubleClickNode = function(nodeId) { * window.alert('Double clicked node ${nodeId}'); * }; * * const onRightClickNode = function(event, nodeId) { * window.alert('Right clicked node ${nodeId}'); * }; * * const onMouseOverNode = function(nodeId) { * window.alert(`Mouse over node ${nodeId}`); * }; * * const onMouseOutNode = function(nodeId) { * window.alert(`Mouse out node ${nodeId}`); * }; * * const onClickLink = function(source, target) { * window.alert(`Clicked link between ${source} and ${target}`); * }; * * const onRightClickLink = function(event, source, target) { * window.alert('Right clicked link between ${source} and ${target}'); * }; * * const onMouseOverLink = function(source, target) { * window.alert(`Mouse over in link between ${source} and ${target}`); * }; * * const onMouseOutLink = function(source, target) { * window.alert(`Mouse out link between ${source} and ${target}`); * }; * * <Graph * id='graph-id' // id is mandatory, if no id is defined rd3g will throw an error * data={data} * config={myConfig} * onClickGraph={onClickGraph} * onClickNode={onClickNode} * onDoubleClickNode={onDoubleClickNode} * onRightClickNode={onRightClickNode} * onClickLink={onClickLink} * onRightClickLink={onRightClickLink} * onMouseOverNode={onMouseOverNode} * onMouseOutNode={onMouseOutNode} * onMouseOverLink={onMouseOverLink} * onMouseOutLink={onMouseOutLink}/> */ var Graph = function (_React$Component) { _inherits(Graph, _React$Component); _createClass(Graph, [{ key: "_graphForcesConfig", /** * Sets d3 tick function and configures other d3 stuff such as forces and drag events. * @returns {undefined} */ value: function _graphForcesConfig() { this.state.simulation.nodes(this.state.d3Nodes).on("tick", this._tick); var forceLink = (0, _d3Force.forceLink)(this.state.d3Links).id(function (l) { return l.id; }).distance(this.state.config.d3.linkLength).strength(this.state.config.d3.linkStrength); this.state.simulation.force(_graph2.default.LINK_CLASS_NAME, forceLink); var customNodeDrag = (0, _d3Drag.drag)().on("start", this._onDragStart).on("drag", this._onDragMove).on("end", this._onDragEnd); (0, _d3Selection.select)("#" + this.state.id + "-" + _graph2.default.GRAPH_WRAPPER_ID).selectAll(".node").call(customNodeDrag); } /** * Handles d3 drag 'end' event. * @returns {undefined} */ /** * Obtain a set of properties which will be used to perform the focus and zoom animation if * required. In case there's not a focus and zoom animation in progress, it should reset the * transition duration to zero and clear transformation styles. * @returns {Object} - Focus and zoom animation properties. */ /** * Handles d3 'drag' event. * {@link https://github.com/d3/d3-drag/blob/master/README.md#drag_subject|more about d3 drag} * @param {Object} ev - if not undefined it will contain event data. * @param {number} index - index of the node that is being dragged. * @param {Array.<Object>} nodeList - array of d3 nodes. This list of nodes is provided by d3, each * node contains all information that was previously fed by rd3g. * @returns {undefined} */ /** * Handles d3 drag 'start' event. * @returns {undefined} */ /** * Sets nodes and links highlighted value. * @param {string} id - the id of the node to highlight. * @param {boolean} [value=false] - the highlight value to be set (true or false). * @returns {undefined} */ /** * The tick function simply calls React set state in order to update component and render nodes * along time as d3 calculates new node positioning. * @param {Object} state - new state to pass on. * @param {Function} [cb] - optional callback to fed in to {@link setState()|https://reactjs.org/docs/react-component.html#setstate}. * @returns {undefined} */ /** * Configures zoom upon graph with default or user provided values.<br/> * NOTE: in order for users to be able to double click on nodes, we * are disabling the native dblclick.zoom from d3 that performs a zoom * whenever a user double clicks on top of the graph. * {@link https://github.com/d3/d3-zoom#zoom} * @returns {undefined} */ /** * Handler for 'zoom' event within zoom config. * @returns {Object} returns the transformed elements within the svg graph area. */ /** * Calls the callback passed to the component. * @param {Object} e - The event of onClick handler. * @returns {undefined} */ /** * Collapses the nodes, then checks if the click is doubled and calls the callback passed to the component. * @param {string} clickedNodeId - The id of the node where the click was performed. * @returns {undefined} */ /** * Handles mouse over node event. * @param {string} id - id of the node that participates in the event. * @returns {undefined} */ /** * Handles mouse out node event. * @param {string} id - id of the node that participates in the event. * @returns {undefined} */ /** * Handles mouse over link event. * @param {string} source - id of the source node that participates in the event. * @param {string} target - id of the target node that participates in the event. * @returns {undefined} */ /** * Handles mouse out link event. * @param {string} source - id of the source node that participates in the event. * @param {string} target - id of the target node that participates in the event. * @returns {undefined} */ /** * Calls d3 simulation.stop().<br/> * {@link https://github.com/d3/d3-force#simulation_stop} * @returns {undefined} */ /** * This method resets all nodes fixed positions by deleting the properties fx (fixed x) * and fy (fixed y). Following this, a simulation is triggered in order to force nodes to go back * to their original positions (or at least new positions according to the d3 force parameters). * @returns {undefined} */ /** * Calls d3 simulation.restart().<br/> * {@link https://github.com/d3/d3-force#simulation_restart} * @returns {undefined} */ }]); function Graph(props) { _classCallCheck(this, Graph); var _this = _possibleConstructorReturn(this, (Graph.__proto__ || Object.getPrototypeOf(Graph)).call(this, props)); _this._generateFocusAnimationProps = function () { var focusedNodeId = _this.state.focusedNodeId; // In case an older animation was still not complete, clear previous timeout to ensure the new one is not cancelled if (_this.state.enableFocusAnimation) { if (_this.focusAnimationTimeout) { clearTimeout(_this.focusAnimationTimeout); } _this.focusAnimationTimeout = setTimeout(function () { return _this.setState({ enableFocusAnimation: false }); }, _this.state.config.focusAnimationDuration * 1000); } var transitionDuration = _this.state.enableFocusAnimation ? _this.state.config.focusAnimationDuration : 0; return { style: { transitionDuration: transitionDuration + "s" }, transform: focusedNodeId ? _this.state.focusTransformation : null }; }; _this._onDragEnd = function () { return !_this.state.config.staticGraph && _this.state.config.automaticRearrangeAfterDropNode && _this.state.simulation.alphaTarget(_this.state.config.d3.alphaTarget).restart(); }; _this._onDragMove = function (ev, index, nodeList) { var id = nodeList[index].id; if (!_this.state.config.staticGraph) { // this is where d3 and react bind var draggedNode = _this.state.nodes[id]; draggedNode.x += _d3Selection.event.dx; draggedNode.y += _d3Selection.event.dy; // set nodes fixing coords fx and fy draggedNode["fx"] = draggedNode.x; draggedNode["fy"] = draggedNode.y; _this._tick(); } }; _this._onDragStart = function () { _this.pauseSimulation(); if (_this.state.enableFocusAnimation) { _this.setState({ enableFocusAnimation: false }); } }; _this._setNodeHighlightedValue = function (id) { var value = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; return _this._tick(graphHelper.updateNodeHighlightedValue(_this.state.nodes, _this.state.links, _this.state.config, id, value)); }; _this._tick = function () { var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; var cb = arguments[1]; return cb ? _this.setState(state, cb) : _this.setState(state); }; _this._zoomConfig = function () { (0, _d3Selection.select)("#" + _this.state.id + "-" + _graph2.default.GRAPH_WRAPPER_ID).call((0, _d3Zoom.zoom)().scaleExtent([_this.state.config.minZoom, _this.state.config.maxZoom]).on("zoom", _this._zoomed)).on("dblclick.zoom", null); }; _this._zoomed = function () { var transform = _d3Selection.event.transform; (0, _d3Selection.selectAll)("#" + _this.state.id + "-" + _graph2.default.GRAPH_CONTAINER_ID).attr("transform", transform); _this.state.config.panAndZoom && _this.setState({ transform: transform.k }); }; _this.onClickGraph = function (e) { if (_this.state.enableFocusAnimation) { _this.setState({ enableFocusAnimation: false }); } // Only trigger the graph onClickHandler, if not clicked a node or link. // toUpperCase() is added as a precaution, as the documentation says tagName should always // return in UPPERCASE, but chrome returns lowercase var tagName = e.target && e.target.tagName; var name = e.target && e.target.attributes && e.target.attributes.name && e.target.attributes.name.value; var svgContainerName = "svg-container-" + _this.state.id; if (tagName.toUpperCase() === "SVG" && name === svgContainerName) { _this.props.onClickGraph && _this.props.onClickGraph(); } }; _this.onClickNode = function (clickedNodeId) { if (_this.state.config.collapsible) { var leafConnections = collapseHelper.getTargetLeafConnections(clickedNodeId, _this.state.links, _this.state.config); var links = collapseHelper.toggleLinksMatrixConnections(_this.state.links, leafConnections, _this.state.config); var d3Links = collapseHelper.toggleLinksConnections(_this.state.d3Links, links); _this._tick({ links: links, d3Links: d3Links }, function () { return _this.props.onClickNode && _this.props.onClickNode(clickedNodeId); }); } else { if (!_this.nodeClickTimer) { _this.nodeClickTimer = setTimeout(function () { _this.props.onClickNode && _this.props.onClickNode(clickedNodeId); _this.nodeClickTimer = null; }, _graph2.default.TTL_DOUBLE_CLICK_IN_MS); } else { _this.props.onDoubleClickNode && _this.props.onDoubleClickNode(clickedNodeId); _this.nodeClickTimer = clearTimeout(_this.nodeClickTimer); } } }; _this.onMouseOverNode = function (id) { _this.props.onMouseOverNode && _this.props.onMouseOverNode(id); _this.state.config.nodeHighlightBehavior && _this._setNodeHighlightedValue(id, true); }; _this.onMouseOutNode = function (id) { _this.props.onMouseOutNode && _this.props.onMouseOutNode(id); _this.state.config.nodeHighlightBehavior && _this._setNodeHighlightedValue(id, false); }; _this.onMouseOverLink = function (source, target) { _this.props.onMouseOverLink && _this.props.onMouseOverLink(source, target); if (_this.state.config.linkHighlightBehavior) { var highlightedLink = { source: source, target: target }; _this._tick({ highlightedLink: highlightedLink }); } }; _this.onMouseOutLink = function (source, target) { _this.props.onMouseOutLink && _this.props.onMouseOutLink(source, target); if (_this.state.config.linkHighlightBehavior) { var highlightedLink = undefined; _this._tick({ highlightedLink: highlightedLink }); } }; _this.pauseSimulation = function () { return _this.state.simulation.stop(); }; _this.resetNodesPositions = function () { if (!_this.state.config.staticGraph) { for (var nodeId in _this.state.nodes) { var node = _this.state.nodes[nodeId]; if (node.fx && node.fy) { Reflect.deleteProperty(node, "fx"); Reflect.deleteProperty(node, "fy"); } } _this.state.simulation.alphaTarget(_this.state.config.d3.alphaTarget).restart(); _this._tick(); } }; _this.restartSimulation = function () { return !_this.state.config.staticGraph && _this.state.simulation.restart(); }; if (!_this.props.id) { _utils2.default.throwErr(_this.constructor.name, _err2.default.GRAPH_NO_ID_PROP); } _this.focusAnimationTimeout = null; _this.state = graphHelper.initializeGraphState(_this.props, _this.state); return _this; } /** * @deprecated * `componentWillReceiveProps` has a replacement method in react v16.3 onwards. * that is getDerivedStateFromProps. * But one needs to be aware that if an anti pattern of `componentWillReceiveProps` is * in place for this implementation the migration might not be that easy. * See {@link https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html}. * @param {Object} nextProps - props. * @returns {undefined} */ _createClass(Graph, [{ key: "componentWillReceiveProps", value: function componentWillReceiveProps(nextProps) { var _graphHelper$checkFor = graphHelper.checkForGraphElementsChanges(nextProps, this.state), graphElementsUpdated = _graphHelper$checkFor.graphElementsUpdated, newGraphElements = _graphHelper$checkFor.newGraphElements; var state = graphElementsUpdated ? graphHelper.initializeGraphState(nextProps, this.state) : this.state; var newConfig = nextProps.config || {}; var _graphHelper$checkFor2 = graphHelper.checkForGraphConfigChanges(nextProps, this.state), configUpdated = _graphHelper$checkFor2.configUpdated, d3ConfigUpdated = _graphHelper$checkFor2.d3ConfigUpdated; var config = configUpdated ? _utils2.default.merge(_graph4.default, newConfig) : this.state.config; // in order to properly update graph data we need to pause eventual d3 ongoing animations newGraphElements && this.pauseSimulation(); var transform = newConfig.panAndZoom !== this.state.config.panAndZoom ? 1 : this.state.transform; var focusedNodeId = nextProps.data.focusedNodeId; var d3FocusedNode = this.state.d3Nodes.find(function (node) { return "" + node.id === "" + focusedNodeId; }); var focusTransformation = graphHelper.getCenterAndZoomTransformation(d3FocusedNode, this.state.config); var enableFocusAnimation = this.props.data.focusedNodeId !== nextProps.data.focusedNodeId; this.setState(_extends({}, state, { config: config, configUpdated: configUpdated, d3ConfigUpdated: d3ConfigUpdated, newGraphElements: newGraphElements, transform: transform, focusedNodeId: focusedNodeId, enableFocusAnimation: enableFocusAnimation, focusTransformation: focusTransformation })); } }, { key: "componentDidUpdate", value: function componentDidUpdate() { // if the property staticGraph was activated we want to stop possible ongoing simulation var shouldPause = this.state.config.staticGraph || this.state.config.staticGraphWithDragAndDrop; if (shouldPause) { this.pauseSimulation(); } if (!this.state.config.staticGraph && (this.state.newGraphElements || this.state.d3ConfigUpdated)) { this._graphForcesConfig(); if (!this.state.config.staticGraphWithDragAndDrop) { this.restartSimulation(); } this.setState({ newGraphElements: false, d3ConfigUpdated: false }); } if (this.state.configUpdated) { this._zoomConfig(); this.setState({ configUpdated: false }); } } }, { key: "componentDidMount", value: function componentDidMount() { if (!this.state.config.staticGraph) { this._graphForcesConfig(); } // graph zoom and drag&drop all network this._zoomConfig(); } }, { key: "componentWillUnmount", value: function componentWillUnmount() { this.pauseSimulation(); this.nodeClickTimer && clearTimeout(this.nodeClickTimer); } }, { key: "render", value: function render() { var _graphRenderer$render = graphRenderer.renderGraph(this.state.nodes, { onClickNode: this.onClickNode, onDoubleClickNode: this.onDoubleClickNode, onRightClickNode: this.props.onRightClickNode, onMouseOverNode: this.onMouseOverNode, onMouseOut: this.onMouseOutNode }, this.state.d3Links, this.state.links, { onClickLink: this.props.onClickLink, onRightClickLink: this.props.onRightClickLink, onMouseOverLink: this.onMouseOverLink, onMouseOutLink: this.onMouseOutLink }, this.state.config, this.state.highlightedNode, this.state.highlightedLink, this.state.transform), nodes = _graphRenderer$render.nodes, links = _graphRenderer$render.links, defs = _graphRenderer$render.defs; var svgStyle = { height: this.state.config.height, width: this.state.config.width }; var containerProps = this._generateFocusAnimationProps(); return _react2.default.createElement( "div", { id: this.state.id + "-" + _graph2.default.GRAPH_WRAPPER_ID }, _react2.default.createElement( "svg", { name: "svg-container-" + this.state.id, style: svgStyle, onClick: this.onClickGraph }, defs, _react2.default.createElement( "g", _extends({ id: this.state.id + "-" + _graph2.default.GRAPH_CONTAINER_ID }, containerProps), links, nodes ) ) ); } }]); return Graph; }(_react2.default.Component); exports.default = Graph;