Description:
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"));