Skip to content
This repository was archived by the owner on Mar 7, 2020. It is now read-only.

Commit b7c6bbc

Browse files
author
Sebastian Stephan
committed
Add session swimlane directive. #26
1 parent a2d86e1 commit b7c6bbc

File tree

3 files changed

+401
-0
lines changed

3 files changed

+401
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,372 @@
1+
/**
2+
* Created by sebastian on 11/22/15.
3+
*/
4+
5+
'use strict';
6+
7+
angular.module('probrAnalysisApp')
8+
.directive('probrSessionSwimlane', function () {
9+
return {
10+
restrict: 'E',
11+
scope: {
12+
sessions: '='
13+
},
14+
templateUrl: 'components/probr/probrSessionSwimlane/probrSessionSwimlane.html',
15+
link: function (scope, element, attrs) {
16+
17+
var svg = d3.select(element[0]).append("svg")
18+
.attr('viewBox', '0 0 1000 1000')
19+
.attr('preserveAspectRatio', 'xMidYMid')
20+
.attr('shape-rendering', 'crispEdges')
21+
.style('width', '100%');
22+
23+
scope.$watch('sessions', function (newVal, oldVal) {
24+
var sessions = newVal;
25+
26+
// Sessions are undefined at first run
27+
if (sessions == undefined) {
28+
return;
29+
}
30+
31+
// Create 'real' date objects
32+
sessions.forEach(function(s) {
33+
s.startTimestamp = new Date(s.startTimestamp);
34+
s.endTimestamp = new Date(s.endTimestamp);
35+
});
36+
37+
// Get array of unique devices
38+
var devices = d3.set(sessions.map(function(s) {
39+
return s.mac_address;
40+
})).values();
41+
42+
// Margin and sizes
43+
// Not pixel based, since use uf ViewBox
44+
// 1000 corresponds to full width of SVG
45+
var mainLaneHeight = 40
46+
, miniLaneHeight = 15
47+
, spaceBetweenGraphs = 60
48+
, textColumnWidth = 100;
49+
50+
var margin = {top: 100, right: 15, bottom: 50, left: 15}
51+
, width = 1000 - margin.left - margin.right
52+
, graphWidth = width - textColumnWidth
53+
, mainHeight = devices.length * mainLaneHeight
54+
, miniHeight = devices.length * miniLaneHeight
55+
, height = miniHeight + mainHeight + spaceBetweenGraphs;
56+
57+
// Expand the SVG to fit new data (variable height)
58+
svg.attr('viewBox',
59+
'0 0 1000 ' + (height + margin.top + margin.bottom) );
60+
61+
// Clip path (cut off overlapping rects in mainGraph)
62+
svg.append('defs').append('clipPath')
63+
.attr('id', 'clip')
64+
.append('rect')
65+
.attr('width', graphWidth)
66+
.attr('height', mainHeight);
67+
68+
// Scales
69+
var miniX = d3.time.scale()
70+
.domain([d3.time.sunday(
71+
d3.min(sessions, function(d){return d.startTimestamp})),
72+
d3.max(sessions, function(d){return d.endTimestamp})
73+
])
74+
.range([0, graphWidth]);
75+
var miniY = d3.scale.ordinal()
76+
.domain(devices)
77+
.rangeBands([0, miniHeight], 0, 0);
78+
var miniYPadded = d3.scale.ordinal()
79+
.domain(devices)
80+
.rangeBands([0, miniHeight], 0.4, 0.2);
81+
82+
var mainX = d3.time.scale().range([0, graphWidth]);
83+
var mainY = d3.scale.ordinal()
84+
.domain(devices)
85+
.rangeBands([0, mainHeight]);
86+
var mainYPadded = d3.scale.ordinal()
87+
.domain(devices)
88+
.rangeBands([0, mainHeight], 0.4, 0.2);
89+
90+
// Group for margin
91+
var plot = svg.append('g')
92+
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
93+
94+
// Main Graph
95+
var main = plot.append('g')
96+
.attr('transform', 'translate(' + textColumnWidth + ',0)')
97+
.attr('class', 'main');
98+
99+
// Mini Graph
100+
var mini = plot.append('g')
101+
.attr('transform', 'translate('
102+
+ textColumnWidth + ','
103+
+ (mainHeight + spaceBetweenGraphs) + ')')
104+
.attr('class', 'mini');
105+
106+
// Debug rects
107+
/*
108+
svg.append("rect")
109+
.attr("x", 0)
110+
.attr("y", 0)
111+
.attr("width", margin.left)
112+
.attr("height", height + margin.top + margin.bottom)
113+
.attr("fill", "red");
114+
svg.append("rect")
115+
.attr("x", 0)
116+
.attr("y", 0)
117+
.attr("width", width + margin.left + margin.top)
118+
.attr("height", margin.top)
119+
.attr("fill", "red");
120+
svg.append("rect")
121+
.attr("x", margin.left + width)
122+
.attr("y", 0)
123+
.attr("width", margin.right)
124+
.attr("height", height + margin.top + margin.bottom)
125+
.attr("fill", "red");
126+
svg.append("rect")
127+
.attr("x", 0)
128+
.attr("y", margin.top + height)
129+
.attr("width", width + margin.left + margin.top)
130+
.attr("height", margin.bottom)
131+
.attr("fill", "red");
132+
main.append("rect")
133+
.attr("x", 0)
134+
.attr("y", 0)
135+
.attr("width", graphWidth)
136+
.attr("height", mainHeight)
137+
.attr("fill", "yellow");
138+
mini.append("rect")
139+
.attr("x", 0)
140+
.attr("y", 0)
141+
.attr("width", graphWidth)
142+
.attr("height", miniHeight)
143+
.attr("fill", "green");
144+
*/
145+
146+
// Draw mac addresses for main graph
147+
main.append('g').selectAll('.laneText')
148+
.data(devices)
149+
.enter().append('text')
150+
.text(function(d) { return d; })
151+
.attr('x', -10)
152+
.attr('y', function(d) { return mainY(d) + mainY.rangeBand()/2 })
153+
.attr('text-anchor', 'end')
154+
.attr('dominant-baseline', 'middle')
155+
.attr('class', 'laneText');
156+
157+
// Draw the lanes for the main graph
158+
main.append('g').selectAll('.laneLines')
159+
.data(devices)
160+
.enter().append('line')
161+
.attr('x1', 0)
162+
.attr('y1', function(d) { return mainY(d); })
163+
.attr('x2', graphWidth)
164+
.attr('y2', function(d) { return mainY(d); })
165+
.attr('stroke', 'lightgray')
166+
.attr('class', 'laneLines');
167+
168+
// Draw mac addresses for mini graph
169+
mini.append('g').selectAll('.laneText')
170+
.data(devices)
171+
.enter().append('text')
172+
.text(function(d) { return d; })
173+
.attr('x', -10)
174+
.attr('y', function(d) { return miniY(d) + miniY.rangeBand()/2; })
175+
.attr('text-anchor', 'end')
176+
.attr('dominant-baseline', 'middle')
177+
.attr('class', 'laneText');
178+
179+
// Draw the lanes for the mini graph
180+
mini.append('g').selectAll('.laneLines')
181+
.data(devices)
182+
.enter().append('line')
183+
.attr('x1', 0)
184+
.attr('y1', function(d) { return miniY(d); })
185+
.attr('x2', graphWidth)
186+
.attr('y2', function(d) { return miniY(d); })
187+
.attr('stroke', 'lightgray')
188+
.attr('class', 'laneLines');
189+
190+
// Axis for mini graph
191+
var xMiniAxisTop = d3.svg.axis()
192+
.scale(miniX)
193+
.orient('top')
194+
.ticks(d3.time.weeks, 1)
195+
.tickFormat(d3.time.format('%b W%W'))
196+
.tickSize(12, 0, 0);
197+
mini.append('g')
198+
.attr('class', 'mini axis weeks')
199+
.call(xMiniAxisTop)
200+
.selectAll('text')
201+
.attr('y', -6) // half of ticksize
202+
.attr('dy', 0)
203+
.attr('x', 5)
204+
.attr('dominant-baseline', 'middle')
205+
.style('text-anchor', 'start');
206+
var xMiniAxisBottomDays = d3.svg.axis()
207+
.scale(miniX)
208+
.orient('bottom')
209+
.ticks(d3.time.days, 1)
210+
.tickFormat(d3.time.format('%d'))
211+
.tickSize(12, 0, 0);
212+
mini.append('g')
213+
.attr('class', 'mini axis days')
214+
.attr('transform', 'translate(0,' + miniHeight + ')')
215+
.call(xMiniAxisBottomDays)
216+
.selectAll('text')
217+
.attr('y', 6) // half of ticksize
218+
.attr('dy', 0)
219+
.attr('x', function(d) {
220+
var now = new Date();
221+
return miniX(now) - miniX(d3.time.hour.offset(now, -12));
222+
})
223+
.attr('dominant-baseline', 'middle');
224+
var xMiniAxisBottomMonths = d3.svg.axis()
225+
.scale(miniX)
226+
.orient('bottom')
227+
.ticks(d3.time.months, 1)
228+
.tickFormat(d3.time.format('%b'))
229+
.tickSize(25, 0, 0);
230+
mini.append('g')
231+
.attr('class', 'mini axis months')
232+
.attr('transform', 'translate(0,' + miniHeight + ')')
233+
.call(xMiniAxisBottomMonths)
234+
.selectAll('text')
235+
.attr('x', 5)
236+
.attr('y', 25) // ticksize
237+
.attr('dy', 0)
238+
.attr('dominant-baseline', 'alphabetic')
239+
.style('text-anchor', 'start');
240+
241+
// Axis for main graph
242+
var xMainAxisTop = d3.svg.axis()
243+
.scale(mainX)
244+
.orient('top')
245+
.ticks(d3.time.days, 1)
246+
.tickFormat(d3.time.format('%a %d'))
247+
.tickSize(6, 0, 0);
248+
main.append('g')
249+
.attr('class', 'main axis days')
250+
.call(xMainAxisTop);
251+
var xMainAxisBottom = d3.svg.axis()
252+
.scale(mainX)
253+
.orient('bottom')
254+
.ticks(d3.time.hours, 3)
255+
.tickFormat(d3.time.format('%H:%M'))
256+
.tickSize(6, 0, 0);
257+
main.append('g')
258+
.attr('class', 'main axis hours')
259+
.attr('transform', 'translate(0,' + mainHeight + ')')
260+
.call(xMainAxisBottom);
261+
262+
// Draw rectangles for mini graph
263+
mini.append('g').selectAll('miniItems')
264+
.data(sessions)
265+
.enter().append('rect')
266+
.attr('class', 'miniItem')
267+
.attr('x', function(d) { return miniX(d.startTimestamp); })
268+
.attr('y', function(d) { return miniYPadded(d.mac_address); })
269+
.attr('width', function(d) { return miniX(d.endTimestamp)-miniX(d.startTimestamp); })
270+
.attr('height', function(d) { return miniYPadded.rangeBand(); });
271+
272+
// Space for later main rectangles
273+
// Clipping for rects that overlap graph plot area
274+
var itemRects = main.append('g')
275+
.attr('clip-path', 'url(#clip)');
276+
277+
// Selection area (invisible)
278+
mini.append('rect')
279+
.attr('pointer-events', 'painted')
280+
.attr('width', graphWidth)
281+
.attr('height', miniHeight)
282+
.attr('visibility', 'hidden')
283+
.on('mouseup', moveBrush);
284+
285+
// Draw selection area
286+
var brush = d3.svg.brush()
287+
.x(miniX)
288+
.extent([d3.time.monday(new Date()),d3.time.saturday.ceil(new Date())])
289+
.on("brush", display);
290+
291+
mini.append('g')
292+
.attr('class', 'brush')
293+
.call(brush)
294+
.selectAll('rect')
295+
.attr('y', 1)
296+
.attr('height', miniHeight - 1);
297+
298+
299+
display();
300+
301+
function display () {
302+
var minExtent = d3.time.hour(brush.extent()[0])
303+
, maxExtent = d3.time.hour(brush.extent()[1]);
304+
var visibleSessions = sessions.filter(function (d) {
305+
return (d.startTimestamp > minExtent && d.startTimestamp < maxExtent)
306+
|| (d.endTimestamp > minExtent && d.endTimestamp < maxExtent)
307+
|| (d.startTimestamp < minExtent && d.endTimestamp > maxExtent);
308+
});
309+
310+
// Snap to hours
311+
mini.select('.brush').call(brush.extent([minExtent, maxExtent]));
312+
313+
if ((maxExtent - minExtent) >= 259200000) { // > 3 days
314+
xMainAxisTop.ticks(d3.days, 1);
315+
xMainAxisBottom.ticks(d3.time.hours, 6);
316+
}
317+
else if ((maxExtent - minExtent) >= 86400000) { // > 1 day
318+
xMainAxisTop.ticks(d3.days, 1);
319+
xMainAxisBottom.ticks(d3.time.hours, 3);
320+
}
321+
else if ((maxExtent - minExtent) > 28800000) { // > 8 hours
322+
xMainAxisTop.ticks(d3.days, 1);
323+
xMainAxisBottom.ticks(d3.time.hours, 1);
324+
}
325+
else if ((maxExtent - minExtent) > 10800000) { // > 3 hours
326+
xMainAxisTop.ticks(d3.days, 1);
327+
xMainAxisBottom.ticks(d3.time.minutes, 30);
328+
}
329+
else {
330+
xMainAxisTop.ticks(d3.days, 1);
331+
xMainAxisBottom.ticks(d3.time.minutes, 10);
332+
}
333+
334+
335+
// Update scale
336+
mainX.domain([minExtent, maxExtent]);
337+
338+
// Update axis
339+
main.select('.main.axis.days').call(xMainAxisTop);
340+
main.select('.main.axis.hours').call(xMainAxisBottom);
341+
342+
// upate the item rects
343+
var rects = itemRects.selectAll('rect')
344+
.data(visibleSessions, function (d) { return d._id; })
345+
.attr('x', function(d) { return mainX(d.startTimestamp); })
346+
.attr('width', function(d) { return mainX(d.endTimestamp) - mainX(d.startTimestamp); });
347+
348+
rects.enter().append('rect')
349+
.attr('x', function(d) { return mainX(d.startTimestamp); })
350+
.attr('y', function(d) { return mainYPadded(d.mac_address); })
351+
.attr('width', function(d) { return mainX(d.endTimestamp) - mainX(d.startTimestamp); })
352+
.attr('height', function(d) { return mainYPadded.rangeBand(); })
353+
.attr('class', 'mainItem');
354+
355+
rects.exit().remove();
356+
357+
}
358+
359+
function moveBrush () {
360+
var origin = d3.mouse(this)
361+
, point = mainX.invert(origin[0])
362+
, halfExtent = (brush.extent()[1].getTime() - brush.extent()[0].getTime()) / 2
363+
, start = new Date(point.getTime() - halfExtent)
364+
, end = new Date(point.getTime() + halfExtent);
365+
brush.extent([start,end]);
366+
display();
367+
}
368+
369+
});
370+
}
371+
};
372+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<h1>Sessions</h1>

0 commit comments

Comments
 (0)