Util = (new function() { this.circleAt = function(context, point,radius) { context.save(); context.beginPath(); context.arc(point.x, point.y, radius, 0, 2 * Math.PI, false); context.fill(); context.restore(); }; this.drawArrowHead = function(context, from, to) { var headlen = 10; // length of head in pixels var angle = Math.atan2(to.y-from.y,to.x-from.x); var alx = to.x-headlen*Math.cos(angle-Math.PI/6), aly = to.y-headlen*Math.sin(angle-Math.PI/6); var arx = to.x-headlen*Math.cos(angle+Math.PI/6), ary = to.y-headlen*Math.sin(angle+Math.PI/6); var asx = to.x-headlen*.7*Math.cos(angle), asy = to.y-headlen*.7*Math.sin(angle); context.moveTo(to.x,to.y); context.lineTo(alx,aly); context.lineTo(asx,asy); context.lineTo(arx,ary); context.lineTo(to.x,to.y); context.fill(); }; this.slope=function(from,to) { return Math.abs((from.y-to.y) / (from.x-to.x)); }; return this; }); function Basic3ByPortModel() { return this; } Basic3ByPortModel.prototype.centerOfRect = function(r) { return { x: r.left + ((r.right - r.left) / 2), y: r.top + ((r.bottom-r.top)/2) }; }; Basic3ByPortModel.prototype.rightCenterOfRect = function(rect) { return { x: rect.right, y: this.centerOfRect(rect).y }; }; Basic3ByPortModel.prototype.leftCenterOfRect = function(rect) { return { x: rect.left, y: this.centerOfRect(rect).y }; }; Basic3ByPortModel.prototype.topCenterOfRect = function(rect){ return { x: this.centerOfRect(rect).x, y: rect.top }; }; Basic3ByPortModel.prototype.bottomCenterOfRect = function(rect){ return { x: this.centerOfRect(rect).x, y: rect.bottom }; }; Basic3ByPortModel.prototype.calculateSlot = function(r1,r2) { var m = this.marker(this.centerOfRect(r1).y, this.centerOfRect(r2).y) + this.marker(this.centerOfRect(r1).x, this.centerOfRect(r2).x); return m; }; Basic3ByPortModel.prototype.marker = function(fromValue, toValue) { if (fromValue === toValue) { return "="; } else if (toValue < fromValue) { return "<"; } else { return ">"; } }; Basic3ByPortModel.prototype.getPorts = function(r1,r2) { // find the port locations that would be used to connect // two rects. var toslots = { "<<" : { from: this.leftCenterOfRect.bind(this), to: this.rightCenterOfRect.bind(this) }, "<<+" : { from: this.leftCenterOfRect.bind(this), to: this.bottomCenterOfRect.bind(this) }, "<=" : { from: this.topCenterOfRect.bind(this), to: this.bottomCenterOfRect.bind(this) }, "<>" : { from: this.rightCenterOfRect.bind(this), to: this.leftCenterOfRect.bind(this) }, "<>+" : { from: this.rightCenterOfRect.bind(this), to: this.bottomCenterOfRect.bind(this) }, "=<" : { from: this.leftCenterOfRect.bind(this), to: this.rightCenterOfRect.bind(this) }, "==" : {}, "=>" : { from: this.rightCenterOfRect.bind(this), to: this.leftCenterOfRect.bind(this) }, "><" : { from: this.leftCenterOfRect.bind(this), to: this.rightCenterOfRect.bind(this) }, "><+" : { from: this.leftCenterOfRect.bind(this), to: this.topCenterOfRect.bind(this) }, ">=" : { from: this.bottomCenterOfRect.bind(this), to: this.topCenterOfRect.bind(this) }, ">>": { from: this.rightCenterOfRect.bind(this), to: this.leftCenterOfRect.bind(this) }, ">>+": { from: this.rightCenterOfRect.bind(this), to: this.topCenterOfRect.bind(this) } }; var slot = this.calculateSlot(r1,r2); var ports = [ toslots[slot].from(r1), toslots[slot].to(r2) ]; var s = Util.slope(ports[0],ports[1]); if (s > 1.5 && s !== Infinity) { slot += "+"; } ports = [ toslots[slot].from(r1), toslots[slot].to(r2) ]; return ports; }; var ProgressBar = React.createClass({ mixins: [React.addons.PureRenderMixin], render: function() { return <div className="progress">{this.props.percent}%</div>; } }); var Pill = React.createClass({ props:{}, render: function() { return (<div onClick={this.clicked} className="pill" id={this.props.id}><span>{this.props.id}</span></div>); }, clicked: function(evt) { alert('clicked ' + this.props.id); } }); var FeatureBreakdownGrid = React.createClass({ render: function() { return ( <table id="tbl"> <tr> <th></th> <th>Iteration 1</th> <th>Iteration 2</th> <th>Iteration 3</th> <th>Iteration 4</th> </tr> <tr> <td>Name 0</td> <td><ProgressBar percent="23"/></td> <td></td> <td><ProgressBar percent="23"/></td> <td><ProgressBar percent="23"/></td> </tr> <tr> <td></td> <td><Pill id="F0"/></td> <td></td> <td><Pill id="F1"/></td> <td><Pill id="F2"/></td> </tr> <tr> <td>Whoever</td> <td><ProgressBar percent="23"/></td> <td></td> <td></td> <td><ProgressBar percent="23"/></td> </tr> <tr> <td></td> <td><Pill id="F3"/></td> <td></td> <td></td> <td><Pill id="F5"/></td> </tr> <tr> <td>Name 1</td> <td></td> <td></td> <td><ProgressBar percent="23"/></td> <td></td> </tr> <tr> <td></td> <td></td> <td></td> <td> <Pill id="F123"/> </td> <td></td> </tr> <tr> <td>Name 2</td> <td><ProgressBar percent="23"/></td> <td></td> <td><ProgressBar percent="23"/></td> <td><ProgressBar percent="23"/></td> </tr> <tr> <td></td> <td><Pill id="F6"/></td> <td></td> <td><Pill id="F7"/></td> <td><Pill id="F8"/></td> </tr> </table> ); } }); var DependenciesOverlay = React.createClass({ props: { highlight:React.PropTypes.bool, visible:React.PropTypes.bool }, updateDimensions: function() { this.setState( {width: $(window).width(), height: $(window).height()} ); }, componentWillMount: function() { this.updateDimensions(); }, componentWillUnmount: function() { window.removeEventListener("resize", this.updateDimensions); }, render: function() { var style = { position: "absolute", zIndex: 300 } // style.pointerEvents = 'none'; if (this.props.highlight) { style.backgroundColor = 'red'; } var children = React.addons.cloneWithProps(this.props.children, { ref:"grid" }); return (<div><canvas onClick={this.propagateClick} ref="canvas" style={style} id="can"></canvas>{children}</div>); }, componentDidUpdate: function ( prevProps, prevState) { this.drawOverlay(); }, propagateClick: function(evt) { console.log("propagateClick",evt); var canvas = this.refs.canvas.getDOMNode(); var d = canvas.style.display; canvas.style.display = 'none'; var under = document.elementFromPoint(evt.clientX, evt.clientY); canvas.style.display = d; under.click(evt); }, componentDidMount: function() { window.addEventListener("resize", this.updateDimensions); this.updateDimensions(); }, drawOverlay: function() { var grid = this.refs.grid.getDOMNode(); var gridRect = grid.getBoundingClientRect(); var canvas = this.refs.canvas.getDOMNode(), context = canvas.getContext('2d'); canvas.width = gridRect.width; canvas.height = gridRect.height; canvas.style.left = (gridRect.left+window.scrollX) + "px"; canvas.style.top = (gridRect.top+window.scrollY) + "px"; if (!this.props.visible) return; context.save(); // account for scrolled content var x = this.props.deps.map(function(d) { return d.split("->"); }).filter(function(d) { return !!!this.props.dep || d[0] === this.props.dep; }.bind(this)); context.translate(-gridRect.left,-gridRect.top); x.forEach(function(d) { this.drawDependencies(context, d[0], d[1]); }.bind(this)); context.restore(); }, drawDependencies: function(context, fromid, toid) { var fromElement = document.getElementById(fromid); var toElement = document.getElementById(toid); var ports = new Basic3ByPortModel().getPorts( fromElement.getBoundingClientRect(), toElement.getBoundingClientRect()); ports[0].name = fromid; ports[1].name = toid; this.drawDependencyLine(context, ports[0], ports[1]); }, drawDependencyLine: function(context, from, to) { context.save(); context.beginPath(); if (context.setLineDash) { context.setLineDash([5,2]); } context.moveTo(from.x, from.y); context.lineTo(to.x, to.y); context.lineWidth=2; context.stroke(); context.restore(); Util.circleAt(context, from,3); Util.drawArrowHead(context, from, to); } }); var Page = React.createClass({ getInitialState: function() { return { highlight:false, visible:true }; }, render: function() { return (<div> <DependenciesOverlay visible={this.state.visible} highlight={this.state.highlight} dep={""} deps={this.props.deps}> <FeatureBreakdownGrid/> </DependenciesOverlay> <button onClick={this.toggleHighlight}>Toggle Canvas BG</button> <button onClick={this.toggleVisible}>Toggle Visible</button> </div>); }, toggleHighlight: function() { var s = this.state; s.highlight = !s.highlight; this.setState(s); console.log("state",s); }, toggleVisible: function() { var s = this.state; s.visible = !s.visible; this.setState(s); console.log("state",s); } }); var deps = Immutable.List() .push("F123->F8") .push("F123->F0") .push("F123->F1") .push("F123->F2") .push("F123->F3") .push("F123->F5") .push("F123->F6") .push("F123->F7") .push("F123->F8") .push("F6->F7") .push("F1->F0"); React.render(<Page deps={deps}/>, document.body);
<div id="output"></div> <script src="https://fb.me/react-js-fiddle-integration.js"></script>