Draw SVG lines that interact with other elements drawn by React.
Usage:
The script requires React and React-dom.
<script src='https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react.min.js'></script> <script src='https://cdnjs.cloudflare.com/ajax/libs/react/15.6.1/react-dom.min.js'></script>
Create an element for the app.
<div id="app">Loading...</div>
The example app.
"use strict"; 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; } var Component = React.Component; var findDOMNode = ReactDOM.findDOMNode; var slugify = function slugify(text) { return text.toString().toLowerCase().replace(/\s+/g, "-") // Replace spaces with - .replace(/[^\w\-]+/g, "") // Remove all non-word chars .replace(/\-\-+/g, "-") // Replace multiple - with single - .replace(/^-+/, "") // Trim - from start of text .replace(/-+$/, ""); // Trim - from end of text }; var data = [{ name: "Thing 1", position: "left", type: "A" }, { name: "Thing 2", position: "left", type: "B" }, { name: "Thing 3", position: "right", type: "A" }, { name: "Thing 4", position: "right", type: "B" }, { name: "Thing 5", position: "right", type: "B" }, { name: "Thing 6", position: "right", type: "A" }, { name: "Thing 7", position: "right", type: "B" }, { name: "Thing 8", position: "right", type: "A" }, { name: "Thing 9", position: "right", type: "B" }, { name: "Thing 10", position: "right", type: "A" }, { name: "Thing 11", position: "right", type: "B" }, { name: "Thing 12", position: "right", type: "A" }]; var Lines = function (_Component) { _inherits(Lines, _Component); function Lines() { _classCallCheck(this, Lines); var _this = _possibleConstructorReturn(this, _Component.call(this)); _this.svg = null; _this.container = null; return _this; } Lines.prototype.componentDidUpdate = function componentDidUpdate() { var _this2 = this; // Re-create all the lines. Since we depend on the rendered dom to decide // where to draw lines from and too, we CAN'T have react render the lines. // React is declarative - components should be a pure function of props // and state. But our lines are a function of the RENDERED DOM! // So, we have to do it manually... // in componentDidUpdate, AFTER react has done it's bit and rendered the dom, // we remove all previous lines, and then re-draw new ones. React, // and react's virtual DOM, will never know these lines exist. if (!this.props.lines || !this.props.container) return null; this.svg = findDOMNode(this); this.container = findDOMNode(this.props.container); while (this.svg.firstChild) {if (window.CP.shouldStopExecution(1)){break;} this.svg.removeChild(this.svg.firstChild); } window.CP.exitedLoop(1); this.props.lines.forEach(function (line) { _this2.renderLines(_this2.svg, line.from, line.to, line.direction); }); window.addEventListener("resize", function () { while (_this2.svg.firstChild) {if (window.CP.shouldStopExecution(2)){break;} _this2.svg.removeChild(_this2.svg.firstChild); } window.CP.exitedLoop(2); _this2.props.lines.forEach(function (line) { _this2.renderLines(_this2.svg, line.from, line.to, line.direction); }); }); }; Lines.prototype.getSVGPosFromScreenPos = function getSVGPosFromScreenPos(x, y) { var svg = this.svg; var position = undefined; if (svg.createSVGPoint) { var point = svg.createSVGPoint(); point.x = x; point.y = y; position = point.matrixTransform(svg.getScreenCTM().inverse()); } else { var svgRect = svg.getBoundingClientRect(); position = { x: x - svgRect.left - svgRect.clientLeft, y: y - svgRect.top - svgRect.clientTop }; } return position; }; Lines.prototype.linearScale = function linearScale(opts) { var istart = opts.domain[0], istop = opts.domain[1], ostart = opts.range[0], ostop = opts.range[1]; return function scale(value) { return ostart + (ostop - ostart) * ((value - istart) / (istop - istart)); }; }; Lines.prototype.renderLines = function renderLines(el, fromRef, toRefs, direction) { var _this3 = this; var fromRect = this.container.querySelector(fromRef).getBoundingClientRect(); var fromPos = direction === "left" ? this.getSVGPosFromScreenPos(fromRect.left, fromRect.top + fromRect.height / 2) : this.getSVGPosFromScreenPos(fromRect.right, fromRect.top + fromRect.height / 2); var toPositions = toRefs.map(function (ref) { var toRect = _this3.container.querySelector(ref).getBoundingClientRect(); return direction === "left" ? _this3.getSVGPosFromScreenPos(toRect.right, toRect.top + fromRect.height / 2) : _this3.getSVGPosFromScreenPos(toRect.left, toRect.top + fromRect.height / 2); }); // Get the maximum length between nodes var maxLength = toPositions.reduce(function (prev, next) { var a = Math.abs(next.y - fromPos.y); var b = Math.abs(next.x - fromPos.x); return Math.max(Math.sqrt(a * a + b * b), prev); }, 0); var minCurve = 0; var maxCurve = 0; // Change the curve depending on screen-size if (window.matchMedia("(min-width: 600px)").matches) { minCurve = 10; maxCurve = 30; } if (window.matchMedia("(min-width: 1000px)").matches) { minCurve = 30; maxCurve = 100; } var scale = this.linearScale({ domain: [maxLength, 0], range: [minCurve, maxCurve] }); return toPositions.forEach(function (toPos, i) { var a = Math.abs(toPos.y - fromPos.y); var b = Math.abs(toPos.x - fromPos.x); var length = Math.sqrt(a * a + b * b); var controlPos1 = direction === "left" ? fromPos.x - scale(length) : fromPos.x + scale(length); var path = direction === "left" ? "M" + fromPos.x + " " + fromPos.y + " C " + controlPos1 + " " + fromPos.y + ", " + (toPos.x + maxCurve) + " " + toPos.y + ", " + toPos.x + " " + toPos.y : "M" + fromPos.x + " " + fromPos.y + " C " + controlPos1 + " " + fromPos.y + ", " + (toPos.x - maxCurve) + " " + toPos.y + ", " + toPos.x + " " + toPos.y; var newPath = document.createElementNS("http://www.w3.org/2000/svg", "path"); //Create a path in SVG's namespace newPath.setAttribute("d", path); newPath.setAttribute("fill", "none"); newPath.setAttribute("stroke", "black"); newPath.setAttribute("stroke-width", 3); newPath.setAttribute("stroke-dasharray", length * 1.2); newPath.setAttribute("stroke-dashoffset", length * 1.2); newPath.setAttribute("class", "animatePath"); el.appendChild(newPath); }); }; Lines.prototype.render = function render() { return React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", shapeRendering: "geometricPrecision", className: "backgroundSVG", id: "svg-lines" }); }; return Lines; }(Component); var App = function (_Component2) { _inherits(App, _Component2); function App() { _classCallCheck(this, App); var _this4 = _possibleConstructorReturn(this, _Component2.call(this)); _this4.state = {}; return _this4; } App.prototype.componentWillMount = function componentWillMount() { // Some data this.data = data; }; App.prototype.getLines = function getLines() { var _this5 = this; if (!this.state.activeItem) return; var invertedDirection = this.state.activeItem.position === "left" ? "right" : "left"; var relatedItems = this.data.filter(function (d) { return d.position === invertedDirection && d.type === _this5.state.activeItem.type; }).map(function (d) { return "#" + slugify(d.name); }); return [{ from: "#" + slugify(this.state.activeItem.name), direction: invertedDirection, to: relatedItems }]; }; App.prototype.renderInteractables = function renderInteractables(position) { var _this6 = this; return this.data.filter(function (d) { return d.position === position; }).map(function (interactable) { var activeClass = _this6.state.activeItem && _this6.state.activeItem.name === interactable.name ? "isActive" : ""; return React.createElement( "div", { id: slugify(interactable.name), className: "interactable " + activeClass, onClick: function onClick(_) { _this6.setState({ activeItem: interactable }); } }, interactable.name ); }); }; App.prototype.render = function render() { var _this7 = this; return React.createElement( "div", { className: "stage", ref: function ref(d) { _this7.stage = d; } }, React.createElement(Lines, { container: this.stage, lines: this.getLines() }), React.createElement( "div", { className: "row" }, React.createElement( "div", { className: "column" }, this.renderInteractables("left") ), React.createElement( "div", { className: "column" }, this.renderInteractables("right") ) ) ); }; return App; }(Component); ReactDOM.render(React.createElement(App, null), document.getElementById("app"));
Preview:
Download Details:
Author: Mike
Live Demo: View The Demo
Download Link: Download The Source Code
Official Website: https://codepen.io/MadeByMike/pen/JOMrEN
License: MIT