An experiment.
A Pen by Jase Smith on CodePen.
An experiment.
A Pen by Jase Smith on CodePen.
<div ng-app="app" ng-controller="AppController" ng-class="{'x-ray': xRay, 'horizontal': horiz}"> | |
<h1>Proof of Concept</h1> | |
<p>Using only CSS & HTML to draw the diagram, aside from using Angular to build the HTML and hook on the magic calculations. <b>No canvas, no SVG.</b></p> | |
<p>Tested in Chrome, Safari, Firefox. And it seems to also work in IE 10+</p> | |
<p><label><input type="checkbox" ng-model="xRay" /> X-Ray Goggles</label></p> | |
<spork config="config" model="model"></spork> | |
</div> |
// utility methods | |
var getNumbers = function(target) { | |
var numbers = {}; | |
if (target) { | |
numbers = { | |
t: target.offsetTop, | |
r: target.offsetLeft + target.offsetWidth, | |
b: target.offsetTop + target.offsetHeight, | |
l: target.offsetLeft, | |
w: target.offsetWidth, | |
h: target.offsetHeight | |
}; | |
// find x|y center | |
numbers.cx = (numbers.l + (numbers.w / 2)); | |
numbers.cy = (numbers.t + (numbers.h / 2)); | |
} | |
return numbers; | |
}; | |
var getMetrics = function(el, parent, rtl, factor) { | |
rtl = rtl || false; | |
factor = factor || false; | |
var en = getNumbers(el); | |
var pn = getNumbers(parent); | |
var p1 = { | |
x: en.cx, | |
y: en.cy | |
}; | |
var p2 = { | |
x: pn.cx, | |
y: pn.cy | |
}; | |
// angle in radians | |
var rads = Math.atan2(p2.y - p1.y, p2.x - p1.x); | |
// angle in degrees | |
var degs = rads * 180 / Math.PI; | |
if (rtl) { | |
// this because if the line orientation is right to left | |
degs += 180; | |
} | |
// length of line between two points | |
// last operation as this alters the number set | |
var lens = Math.sqrt(((p1.x -= p2.x) * p1.x) + ((p1.y -= p2.y) * p1.y)); | |
if (factor) { | |
// for if we want the line length to extend beyond the node | |
lens = lens * factor; | |
} | |
// returns object of calculations | |
return { | |
lens: lens, | |
rads: rads, | |
degs: degs | |
}; | |
}; | |
angular.module('app', []) | |
.controller('AppController', ['$scope', function($scope) { | |
// nodes data model | |
$scope.model = [{ | |
id: 0, | |
children: [{ | |
id: 1, | |
children: [{ | |
id: 11, | |
children: [{ | |
id: 111 | |
}, { | |
id: 112 | |
}] | |
}, { | |
id: 12 | |
}] | |
}, | |
{ | |
id: 2, | |
children: [{ | |
id: 21 | |
}, { | |
id: 22 | |
}, { | |
id: 23, | |
children: [{ | |
id: 231 | |
}, { | |
id: 232 | |
}, { | |
id: 233 | |
}] | |
}, { | |
id: 24 | |
}] | |
}, | |
] | |
}]; | |
}]) | |
.directive('spork', [function spork() { | |
return { | |
restrict: 'E', | |
replace: true, | |
scope: { | |
config: '=?', | |
model: '=' | |
}, | |
controller: ['$scope', function controller($scope) { | |
var me = this; | |
$scope.connector = function connector(el, parent) { | |
el = angular.element('#node-' + el)[0]; | |
parent = angular.element('#node-' + parent)[0]; | |
var metrics = getMetrics(el, parent, true); | |
return { | |
width: Math.round(metrics.lens) + 'px', | |
transform: 'translateY(-50%) rotate(' + Math.round(metrics.degs) + 'deg)' | |
}; | |
}; | |
}], | |
template: '' + | |
'<div class="spork">' + | |
'<ul>' + | |
'<li ng-repeat="a in model">' + | |
'<div id="node-{{a.id}}">' + | |
'<div class="node"><span class="label">{{a.id}}</span></div>' + | |
'</div>' + | |
'<ul ng-if="a.children.length">' + | |
'<li ng-repeat="b in a.children">' + | |
'<div id="node-{{b.id}}">' + | |
'<div class="node"><span class="label">{{b.id}}</span></div>' + | |
'<div class="line" ng-style="connector(b.id, a.id)"></div>' + | |
'</div>' + | |
'<ul ng-if="b.children.length">' + | |
'<li ng-repeat="c in b.children">' + | |
'<div id="node-{{c.id}}">' + | |
'<div class="node"><span class="label">{{c.id}}</span></div>' + | |
'<div class="line" ng-style="connector(c.id, b.id)"></div>' + | |
'</div>' + | |
'<ul ng-if="c.children.length">' + | |
'<li ng-repeat="d in c.children">' + | |
'<div id="node-{{d.id}}">' + | |
'<div class="node"><span class="label">{{d.id}}</span></div>' + | |
'<div class="line" ng-style="connector(d.id, c.id)"></div>' + | |
'</div>' + | |
'</li>' + | |
'</ul>' + | |
'</li>' + | |
'</ul>' + | |
'</li>' + | |
'</ul>' + | |
'</li>' + | |
'</ul>' + | |
'</div>' | |
}; | |
}]); |
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.2/underscore-min.js"></script> |
// | |
.spork { | |
position: relative; | |
ul { | |
margin: 0; | |
padding: 0; | |
list-style: none; | |
flex: 1 0 auto; | |
display: flex; | |
flex-direction: column; | |
align-items: flex-start; | |
} | |
li { | |
flex: 0 0 auto; | |
margin: 0 .5em; | |
display: flex; | |
flex-direction: row; | |
align-items: center; | |
transition: .2s; | |
> div { | |
margin: .5em; | |
position: relative; | |
flex: 0 0 auto; | |
} | |
} | |
.node { | |
position: relative; | |
z-index: 2; | |
padding: 2em; | |
background: skyblue; | |
/* box-shadow: 0 0 0 .2em currentColor inset; */ | |
border: 2px solid; | |
border-radius: 100%; | |
transition: .2s; | |
cursor: default; | |
} | |
.label { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
pointer-events: none; | |
} | |
.line { | |
position: absolute; | |
z-index: 1; | |
padding: .2em; | |
background: currentColor; | |
top: 50%; | |
right: 50%; | |
transform-origin: right center; | |
transition: .2s; | |
pointer-events: none; | |
} | |
} | |
// playground | |
.spork { | |
ul { | |
margin: .5em; | |
} | |
li { | |
margin: .5em; | |
border-radius: .3em; | |
li { | |
font-size: .85em; | |
} | |
&:hover { | |
> div { | |
.node, | |
.node+.line { | |
background: mix(#fff, yellowGreen, 5%); | |
} | |
.node { | |
z-index: 3; | |
transform: scale(1.2); | |
border-color: #fff; | |
&:hover { | |
transform: scale(2) !important; | |
box-shadow: 0 0 0 .3em fade-out(yellowGreen, .7); | |
} | |
+ .line { | |
padding: .4em; | |
} | |
} | |
} | |
} | |
} | |
} | |
// electrical wiring | |
.x-ray .spork li { | |
background: rgba(#000,.1); | |
.node { | |
opacity: .7; | |
} | |
} | |
// basics | |
body { | |
font-family: sans-serif; | |
font-size: 15px; | |
} | |
.spork *, .spork *::after, .spork *::before { | |
box-sizing: border-box; | |
} |