skybox 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,330 @@
1
+ (function() {
2
+ var flow = d3.flow();
3
+ var darkColors = ["#000", "#1f77b4"];
4
+ var colors = d3.scale.category20();
5
+ var root = {id:"enter", depth:0};
6
+ var nodes = [root], links = [];
7
+ var chart = null, svg = null, g = {};
8
+
9
+ var highlightDepth = -1;
10
+ var easingType = "quad-in";
11
+
12
+ // Start with a simple session start query.
13
+ var query = {
14
+ selections:[{
15
+ fields: [{aggregationType:"count"}],
16
+ groups: [{expression:"action_id"}],
17
+ conditions: [{type:"on", action:"enter"}]
18
+ }],
19
+ sessionIdleTime:7200
20
+ };
21
+
22
+
23
+ //----------------------------------------------------------------------------
24
+ //
25
+ // Initialization
26
+ //
27
+ //----------------------------------------------------------------------------
28
+
29
+ // Initializes the view.
30
+ function init() {
31
+ // Setup the SVG container for the visualization.
32
+ chart = $("#chart")[0];
33
+ svg = d3.select(chart).append("svg");
34
+ g = {
35
+ link:svg.append("g"),
36
+ node:svg.append("g")
37
+ };
38
+
39
+ // Add listeners.
40
+ $(window).resize(window_onResize)
41
+ $(document).on("click", document_onClick);
42
+
43
+ // Update!
44
+ update();
45
+ load(root);
46
+ }
47
+
48
+
49
+ //----------------------------------------------------------------------------
50
+ //
51
+ // Methods
52
+ //
53
+ //----------------------------------------------------------------------------
54
+
55
+ //--------------------------------------
56
+ // Layout
57
+ //--------------------------------------
58
+
59
+ // Updates the view.
60
+ function update(options) {
61
+ // Update the dimensions of the visualization.
62
+ flow.width($(chart).width());
63
+ flow.height(window.innerHeight - $(chart).offset().top - 40);
64
+
65
+ // Layout data.
66
+ flow.layout(nodes, links, options);
67
+
68
+ // Update SVG container.
69
+ var margin = flow.margin();
70
+ svg.attr("width", flow.width()).attr("height", flow.height());
71
+ g.link.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
72
+ g.node.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
73
+
74
+ // Layout links.
75
+ g.link.selectAll(".link").data(links, function(d) { return d.key; })
76
+ .call(function(link) {
77
+ var enter = link.enter(), exit = link.exit();
78
+ link.transition().ease(easingType)
79
+ .call(flow.links.position)
80
+ .attr("stroke-dashoffset", 0);
81
+
82
+ enter.append("path").attr("class", "link")
83
+ .call(flow.links.position)
84
+ .each(function(path) {
85
+ var totalLength = this.getTotalLength();
86
+ d3.select(this)
87
+ .attr("stroke-dasharray", totalLength + " " + totalLength)
88
+ .attr("stroke-dashoffset", totalLength)
89
+ .transition().ease(easingType)
90
+ .delay(function(d) { return 250 + (d.target.index*100)})
91
+ .duration(250)
92
+ .attr("stroke-dashoffset", 0)
93
+ .each("end", function(d) { d3.select(this).attr("stroke-dasharray", "none") });
94
+ });
95
+
96
+ exit.remove();
97
+ });
98
+
99
+ // Layout nodes.
100
+ var node = g.node.selectAll(".node").data(nodes, function(d) { return d.key })
101
+ .call(function(node) {
102
+ var enter = node.enter(), exit = node.exit();
103
+
104
+ // Update selection.
105
+ node.selectAll("rect")
106
+ .transition().ease(easingType)
107
+ .call(flow.nodes.position)
108
+ .style("fill", nodeFillColor)
109
+ .style("fill-opacity", 1)
110
+ .style("stroke-opacity", 1);
111
+ node.selectAll(".title")
112
+ .transition().ease(easingType)
113
+ .call(flow.nodes.title.position)
114
+ .attr("display", function(d) { return (d.height > 20 ? "block" : "none"); })
115
+ .style("fill-opacity", 1);
116
+
117
+ // Enter selection.
118
+ var g = enter.append("g").attr("class", "node")
119
+ .on("click", node_onClick)
120
+ .on("mouseover", node_onMouseOver);
121
+ var rect = g.append("rect");
122
+ rect.style("fill", nodeFillColor)
123
+ .style("fill-opacity", function(d) { return (d.depth == 0 ? 1 : 0); })
124
+ .style("stroke", function(d) { return d3.rgb(nodeFillColor(d)).darker(2); })
125
+ .style("stroke-opacity", function(d) { return (d.depth == 0 ? 1 : 0); })
126
+ .call(flow.nodes.position)
127
+ .transition().ease(easingType).delay(nodeDelay)
128
+ .style("fill-opacity", 1)
129
+ .style("stroke-opacity", 1)
130
+ var title = g.append("text")
131
+ .attr("class", "title")
132
+ .attr("dy", "1em")
133
+ .style("fill", nodeTextColor)
134
+ .style("fill-opacity", function(d) { return (d.depth == 0 ? 1 : 0); })
135
+ .attr("display", function(d) { return (d.height > 20 ? "block" : "none"); })
136
+ .call(flow.nodes.title.position)
137
+ .text(nodeTitle)
138
+ .transition().ease(easingType).delay(nodeDelay)
139
+ .style("fill-opacity", 1)
140
+
141
+ // Exit selection.
142
+ exit.remove();
143
+ });
144
+
145
+ // Update the query text.
146
+ updateQueryText();
147
+ }
148
+
149
+ /**
150
+ * Updates the query text.
151
+ */
152
+ function updateQueryText() {
153
+ var html = "Query: " + skybox.query.html(query);
154
+ $("#query-text").html(html);
155
+
156
+ // Setup the selection popover.
157
+ $("#query-text .selection").popover({
158
+ html:true, placement:"bottom",
159
+ title:"Update Selection",
160
+ content:
161
+ '<form>' +
162
+ ' <div class="control-group">' +
163
+ ' <label class="control-label" for="selectionFields">Fields</label>' +
164
+ ' <div class="controls">' +
165
+ ' <span class="selection-fields uneditable-input">count()</span>' +
166
+ ' </div>' +
167
+ ' </div>' +
168
+ ' <div class="control-group">' +
169
+ ' <label class="control-label" for="selectionGroupBy">Group By</label>' +
170
+ ' <div class="controls">' +
171
+ ' <select class="selection-group">' +
172
+ [{name:"action_id"}].concat(skybox.properties()).map(function(i) {return '<option>' + i.name + '</option>'}).join("") +
173
+ ' </select>' +
174
+ ' </div>' +
175
+ ' </div>' +
176
+ '</form>'
177
+ });
178
+ }
179
+
180
+ function nodeDelay(node) {
181
+ return 500 + (node.index*100);
182
+ }
183
+
184
+ function nodeFillColor(node) {
185
+ var color;
186
+ switch(node.id) {
187
+ case "exit": color = "#000"; break;
188
+ case "other": color = "lightgray"; break;
189
+ default: color = colors(node.id);
190
+ }
191
+ return color;
192
+ }
193
+
194
+ function nodeTextColor(node) {
195
+ var fillColor = nodeFillColor(node);
196
+ return (darkColors.indexOf(fillColor) != -1 ? "#f2f2f2" : "#000");
197
+ }
198
+
199
+ function nodeTitle(node) {
200
+ var action = skybox.actions.find(node.id);
201
+ return Humanize.truncate((action ? action.name : ""), 16);
202
+ }
203
+
204
+
205
+ //--------------------------------------
206
+ // Data
207
+ //--------------------------------------
208
+
209
+ /**
210
+ * Runs the current query against the server, sets the returned data and
211
+ * updates the UI.
212
+ *
213
+ * @param {Object} source The node that caused the load to occur.
214
+ */
215
+ function load(source) {
216
+ $(".loading").show();
217
+
218
+ // Execute the query.
219
+ var xhr = $.post("/query", JSON.stringify({table:skybox.table(), query:query}), function(data) {
220
+ var level = {nodes:[], links:[]}
221
+ skybox.data.normalize(data, level.nodes, level.links, {limit:6});
222
+
223
+ level.links.forEach(function(link) { link.source = source; });
224
+ level.nodes.forEach(function(node) { node.depth = source.depth + 1; });
225
+ if(source.value == undefined) source.value = d3.sum(level.nodes, function(d) { return d.value; })
226
+
227
+ nodes = nodes.concat(level.nodes);
228
+ links = links.concat(level.links);
229
+
230
+ update();
231
+ })
232
+ // Notify the user if the query fails for some reason.
233
+ .fail(function() {
234
+ alert("Unable to load query data.");
235
+ })
236
+ .always(function() {
237
+ $(".loading").hide();
238
+ });
239
+
240
+ return xhr;
241
+ }
242
+
243
+
244
+ //----------------------------------------------------------------------------
245
+ //
246
+ // Events
247
+ //
248
+ //----------------------------------------------------------------------------
249
+
250
+ //--------------------------------------
251
+ // Node
252
+ //--------------------------------------
253
+
254
+ /**
255
+ * Appends an 'After' condition to the query for a node and re-queries.
256
+ */
257
+ function node_onClick(node) {
258
+ if(node.id == "exit" || node.id == "other") return;
259
+
260
+ // Remove nodes higher than current node's depth.
261
+ nodes = nodes.filter(function(n) { return n.depth <= node.depth; });
262
+ update({suppressRescaling:true});
263
+
264
+ // Clear out conditions after this depth and append new condition.
265
+ if(node.depth > 0) {
266
+ query.selections[0].conditions = query.selections[0].conditions.slice(0, node.depth);
267
+ query.selections[0].conditions.push({type:"after", action:{id:parseInt(node.id)}, within:{quantity:1, unit:"step"}});
268
+ }
269
+ else {
270
+ query.selections[0].conditions = query.selections[0].conditions.slice(0, 1);
271
+ }
272
+ load(node)
273
+ }
274
+
275
+ /**
276
+ * Shows a tooltip on mouse over.
277
+ */
278
+ function node_onMouseOver(node) {
279
+ var action = skybox.actions.find(node.id);
280
+ $(this).tooltip({
281
+ html: true, container:"body",
282
+ placement: (node.depth == 0 ? "right" : "left"),
283
+ title:
284
+ Humanize.truncate(action.name, 30) + "<br/>" +
285
+ "Count: " + Humanize.intcomma(node.value)
286
+ });
287
+ $(this).tooltip("show");
288
+ }
289
+
290
+
291
+ //--------------------------------------
292
+ // Window
293
+ //--------------------------------------
294
+
295
+ /**
296
+ * Updates the view whenever the window is resized.
297
+ */
298
+ function window_onResize() {
299
+ update();
300
+ }
301
+
302
+ /**
303
+ * Removes all popovers on click.
304
+ */
305
+ function document_onClick() {
306
+ if($(event.target).attr("rel") == "popover") return;
307
+ if($(event.target).parents(".popover").length == 0) {
308
+ $("*").popover("hide");
309
+ }
310
+ }
311
+
312
+
313
+ //----------------------------------------------------------------------------
314
+ //
315
+ // Public Interface
316
+ //
317
+ //----------------------------------------------------------------------------
318
+
319
+ skybox.explore = {
320
+ init:init,
321
+ update:update,
322
+ };
323
+
324
+ })();
325
+
326
+
327
+ // Initialize the Explore view once the page has loaded.
328
+ skybox.ready(function() {
329
+ skybox.explore.init();
330
+ });
@@ -0,0 +1,289 @@
1
+ (function() {
2
+ var table = null;
3
+ var actions = [];
4
+ var properties = [];
5
+
6
+ //----------------------------------------------------------------------------
7
+ //
8
+ // Methods
9
+ //
10
+ //----------------------------------------------------------------------------
11
+
12
+ //--------------------------------------
13
+ // Core
14
+ //--------------------------------------
15
+
16
+ /**
17
+ * Attaches a listener for when all required data is loaded.
18
+ *
19
+ * @param {Function} callback The function to execute when the view is ready.
20
+ */
21
+ function ready(callback) {
22
+ // Clears dependencies and checks for completion.
23
+ var dependencies = ["actions", "properties"];
24
+ var removedep = function(dependency) {
25
+ dependencies = dependencies.filter(function(i) { return i != dependency; });
26
+ if(dependencies.length == 0) {
27
+ callback();
28
+ }
29
+ };
30
+
31
+ // Initialize and wait for dependencies.
32
+ $(document).ready(function() {
33
+ actions_load().done(function() { removedep("actions"); });
34
+ properties_load().done(function() { removedep("properties"); });
35
+ });
36
+ }
37
+
38
+
39
+ //--------------------------------------
40
+ // Table
41
+ //--------------------------------------
42
+
43
+ /**
44
+ * Sets or retrieves the current table.
45
+ */
46
+ function table_get(_) {
47
+ if (!arguments.length) return table;
48
+ table = _;
49
+ }
50
+
51
+
52
+ //--------------------------------------
53
+ // Actions
54
+ //--------------------------------------
55
+
56
+ /**
57
+ * Sets or retrieves the actions.
58
+ */
59
+ function actions_get(_) {
60
+ if (!arguments.length) return actions;
61
+ actions = _;
62
+ }
63
+
64
+ /**
65
+ * Retrieves an action object by id.
66
+ */
67
+ function actions_find(id) {
68
+ for(var i=0; i<actions.length; i++) {
69
+ if(actions[i].id == id || actions[i].name == id) {
70
+ return actions[i];
71
+ }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Loads action data.
77
+ *
78
+ * @return {Object} The XHR used to load the action data.
79
+ */
80
+ function actions_load() {
81
+ var xhr = $.getJSON("/actions", {table:table}, function(data) {
82
+ actions = [
83
+ {id:"enter", name:"Session Start"},
84
+ {id:"exit", name:"Session End"},
85
+ {id:"other", name:"Other"}
86
+ ].concat(data);
87
+ })
88
+ .fail(function() {
89
+ alert("Unable to load action data");
90
+ });
91
+ return xhr;
92
+ }
93
+
94
+
95
+ //--------------------------------------
96
+ // Properties
97
+ //--------------------------------------
98
+
99
+ /**
100
+ * Sets or retrieves the properties.
101
+ */
102
+ function properties_get(_) {
103
+ if (!arguments.length) return properties;
104
+ properties = _;
105
+ }
106
+
107
+ /**
108
+ * Retrieves a property object by id.
109
+ */
110
+ function properties_find(id) {
111
+ for(var i=0; i<properties.length; i++) {
112
+ if(properties[i].id == id || properties[i].name == id) {
113
+ return properties[i];
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Loads property data.
120
+ *
121
+ * @return {Object} The XHR used to load the property data.
122
+ */
123
+ function properties_load() {
124
+ var xhr = $.getJSON("/properties", {table:table}, function(data) {
125
+ properties = data;
126
+ })
127
+ .fail(function() {
128
+ alert("Unable to load property data");
129
+ });
130
+ return xhr;
131
+ }
132
+
133
+
134
+ //--------------------------------------
135
+ // Data
136
+ //--------------------------------------
137
+
138
+ /**
139
+ * Converts the data object returned from a Sky query and converts it into a
140
+ * collection of nodes and links.
141
+ *
142
+ * @param {Object} data The data returned from a Sky query.
143
+ * @param {Array} nodes An array to append nodes to.
144
+ * @param {Array} links An array to append links to.
145
+ */
146
+ function data_normalize(data, nodes, links, options) {
147
+ if(!data) return;
148
+ if(!options) options = {};
149
+
150
+ // Generate nodes from keys.
151
+ for(var key in data) {
152
+ nodes.push({id:key, value:data[key].count});
153
+ }
154
+ nodes = nodes.sort(function(a,b) { return b.value-a.value;});
155
+
156
+ // Limit the number of items if specified.
157
+ if(!isNaN(options.limit) && nodes.length > options.limit) {
158
+ var others = nodes.splice(options.limit, nodes.length-options.limit);
159
+ var other = {id:"other", value:d3.sum(others, function(d) { return d.value; })};
160
+ nodes.push(other);
161
+ }
162
+
163
+ // Generate links from nodes.
164
+ for(i=0; i<nodes.length; i++) {
165
+ var node = nodes[i];
166
+ links.push({target:node, value:node.value});
167
+ }
168
+ }
169
+
170
+
171
+ //--------------------------------------
172
+ // Query
173
+ //--------------------------------------
174
+
175
+ /**
176
+ * Converts a query into a human readable HTML string.
177
+ *
178
+ * @param {Object} query The query.
179
+ *
180
+ * @return {String} The HTML string.
181
+ */
182
+ function query_html(query) {
183
+ return (query.selections.length > 0 ? query_selection_html(query.selections[0]) : "");
184
+ }
185
+
186
+ /**
187
+ * Converts a selection into a human readable HTML string.
188
+ *
189
+ * @param {Object} selection The selection object.
190
+ *
191
+ * @return {String} The HTML string.
192
+ */
193
+ function query_selection_html(selection) {
194
+ var fields_html = query_selection_fields_html(selection);
195
+ var conditions_html = query_selection_conditions_html(selection);
196
+
197
+ var html = [];
198
+ if(fields_html) html.push(fields_html);
199
+ if(conditions_html) html.push(conditions_html);
200
+ return html.join(" ") + ".";
201
+ }
202
+
203
+ /**
204
+ * Converts the fields/groups of a selection into a human readable HTML string.
205
+ *
206
+ * @param {Object} selection The selection object.
207
+ *
208
+ * @return {String} The HTML string.
209
+ */
210
+ function query_selection_fields_html(selection) {
211
+ // Generate the field/group section.
212
+ var html = "Find the "
213
+ html += '<span rel="popover" class="selection">';
214
+ switch(selection.fields[0].aggregationType) {
215
+ case "count": html += "number of ";
216
+ }
217
+ html += (selection.groups[0].expression == "action_id" ? "actions performed" : selection.groups[0].expression);
218
+ html += "</span>"
219
+
220
+ return html;
221
+ }
222
+
223
+ /**
224
+ * Converts the conditions of a selection into a human readable HTML string.
225
+ *
226
+ * @param {Object} selection The selection object.
227
+ *
228
+ * @return {String} The HTML string.
229
+ */
230
+ function query_selection_conditions_html(selection) {
231
+ if(!selection.conditions || selection.conditions.length == 0) return null;
232
+
233
+ var htmls = [];
234
+ for(i=0; i<selection.conditions.length; i++) {
235
+ var condition = selection.conditions[i];
236
+
237
+
238
+ var html = '<span class="condition" data-condition-index="' + i + '">';
239
+ html += condition.type + " ";
240
+ if(condition.action == "enter") {
241
+ html += "session start";
242
+ }
243
+ else {
244
+ var action = actions_find(condition.action.id);
245
+ html += "<em>" + (action ? "'" + action.name + "'" : "&lt;action&gt;") + "</em>";
246
+ }
247
+ html += "</span>"
248
+ htmls.push(html);
249
+ }
250
+ switch(selection.fields[0].aggregationType) {
251
+ case "count": html += "The number of ";
252
+ }
253
+ html += (selection.groups[0].expression == "action_id" ? "actions performed" : selection.groups[0].expression);
254
+ html += "</span>"
255
+
256
+ return htmls.join(" and ");
257
+ }
258
+
259
+
260
+
261
+
262
+ //----------------------------------------------------------------------------
263
+ //
264
+ // Public Interface
265
+ //
266
+ //----------------------------------------------------------------------------
267
+
268
+ skybox = {
269
+ ready:ready,
270
+ table:table_get,
271
+ query:{
272
+ html:query_html
273
+ },
274
+ data:{
275
+ normalize:data_normalize
276
+ }
277
+ };
278
+
279
+ // Actions namespace.
280
+ skybox.actions = actions_get,
281
+ skybox.actions.find = actions_find;
282
+ skybox.actions.load = actions_load;
283
+
284
+ // Properties namespace.
285
+ skybox.properties = properties_get,
286
+ skybox.properties.find = properties_find;
287
+ skybox.properties.load = properties_load;
288
+
289
+ })();