SVG Based Interactive Lines In React

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:

SVG Based Interactive Lines In React

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

Add Comment