UNPKG

@cbinsights/react-d3-graph

Version:

React component to build interactive and configurable graphs with d3 effortlessly

478 lines (371 loc) 18.1 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 _graph5 = require('./graph.renderer'); var graphRenderer = _interopRequireWildcard(_graph5); var _graph6 = require('./graph.helper'); var graphHelper = _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; } // Some d3 constant values var D3_CONST = { FORCE_LINK_STRENGTH: 1, LINK_IDEAL_DISTANCE: 100, SIMULATION_ALPHA_TARGET: 0.05 }; /** * 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 onClickNode = function(nodeId) { * window.alert('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 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} * onClickNode={onClickNode} * onClickLink={onClickLink} * 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(D3_CONST.LINK_IDEAL_DISTANCE).strength(D3_CONST.FORCE_LINK_STRENGTH); 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} */ /** * 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. * @returns {undefined} */ /** * Configures zoom upon graph with default or user provided values.<br/> * {@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. */ /** * 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._onDragEnd = function () { return !_this.state.config.staticGraph && _this.state.config.automaticRearrangeAfterDropNode && _this.state.simulation.alphaTarget(D3_CONST.SIMULATION_ALPHA_TARGET).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 () { return _this.pauseSimulation(); }; _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] : {}; return _this.setState(state); }; _this._zoomConfig = function () { return (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)); }; _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.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) { _this.state.highlightedLink = { source: source, target: target }; _this._tick(); } }; _this.onMouseOutLink = function (source, target) { _this.props.onMouseOutLink && _this.props.onMouseOutLink(source, target); if (_this.state.config.linkHighlightBehavior) { _this.state.highlightedLink = undefined; _this._tick(); } }; _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(D3_CONST.SIMULATION_ALPHA_TARGET).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.state = graphHelper.initializeGraphState(_this.props, _this.state); return _this; } _createClass(Graph, [{ key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { var newGraphElements = nextProps.data.nodes.length !== this.state.nodesInputSnapshot.length || nextProps.data.links.length !== this.state.linksInputSnapshot.length || !_utils2.default.isDeepEqual(nextProps.data, { nodes: this.state.nodesInputSnapshot, links: this.state.linksInputSnapshot }); var configUpdated = !_utils2.default.isObjectEmpty(nextProps.config) && !_utils2.default.isDeepEqual(nextProps.config, this.state.config); var state = newGraphElements ? graphHelper.initializeGraphState(nextProps, this.state) : this.state; var config = configUpdated ? _utils2.default.merge(_graph4.default, nextProps.config || {}) : this.state.config; // in order to properly update graph data we need to pause eventual d3 ongoing animations newGraphElements && this.pauseSimulation(); var transform = nextProps.config.panAndZoom !== this.state.config.panAndZoom ? 1 : this.state.transform; this.setState(_extends({}, state, { config: config, newGraphElements: newGraphElements, configUpdated: configUpdated, transform: transform })); } }, { key: 'componentDidUpdate', value: function componentDidUpdate() { // if the property staticGraph was activated we want to stop possible ongoing simulation this.state.config.staticGraph && this.pauseSimulation(); if (!this.state.config.staticGraph && this.state.newGraphElements) { this._graphForcesConfig(); this.restartSimulation(); this.setState({ newGraphElements: 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(); } }, { key: 'render', value: function render() { var _graphRenderer$buildG = graphRenderer.buildGraph(this.state.nodes, { onClickNode: this.props.onClickNode, onMouseOverNode: this.onMouseOverNode, onMouseOut: this.onMouseOutNode }, this.state.d3Links, this.state.links, { onClickLink: this.props.onClickLink, onMouseOverLink: this.onMouseOverLink, onMouseOutLink: this.onMouseOutLink }, this.state.config, this.state.highlightedNode, this.state.highlightedLink, this.state.transform), nodes = _graphRenderer$buildG.nodes, links = _graphRenderer$buildG.links; var svgStyle = { height: this.state.config.height, width: this.state.config.width }; return _react2.default.createElement( 'div', { id: this.state.id + '-' + _graph2.default.GRAPH_WRAPPER_ID }, _react2.default.createElement( 'svg', { style: svgStyle }, _react2.default.createElement( 'g', { id: this.state.id + '-' + _graph2.default.GRAPH_CONTAINER_ID }, links, nodes ) ) ); } }]); return Graph; }(_react2.default.Component); exports.default = Graph;