skybox 0.2.3.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ YzJmZDAxZDY4OTQ4MzdhNDQ2ZWVjYmU2ZjFhYjIwODMyMzFlMTk1Zg==
5
+ data.tar.gz: !binary |-
6
+ N2I3NGI4MDNjZGVhMzQ4ZjEyOWI1MjU3NjE0ZGNhNmZiYjIyZDhjZQ==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ YmZhOWNiZWM1MzNiMWQ3MDk0MWEzMTAxZDZjNGU5OWNlZDdjZjhlZjVkMzMw
10
+ Njc4NTRkODMzODkxZDQ3NjQ2MWY2YjVlYWEzZjgzNDczYTA4NjM3M2VjYzAy
11
+ OGI4Yzg0MmVkM2E3ZWRiYTEzOTFiZGIyNTI4N2Y4YmQxOWZmMGQ=
12
+ data.tar.gz: !binary |-
13
+ MDFmMzc1M2NkNjJhMzBkZjBhZDE0NTk0ZWMzZWIwMzc0NTExYjVlMGRhZGM0
14
+ Y2RmMTZmYWIzNGFkMjkxN2FhNmJjZTlmZWUzODQ0OGIzOTZmYWY5OWI4MWM5
15
+ YWYxODg5MzIwZTg3NjkzODQ0Y2M2M2JmYjJmMDE2ZTE1ODUyYTk=
data/README.md CHANGED
@@ -1,68 +1,18 @@
1
- Skybox
2
- ======
3
-
4
- _(This project is currently in alpha)_
1
+ # Skybox
5
2
 
6
3
  ### Overview
7
4
 
8
- Skybox is an analytics front-end for the [Sky database](https://github.com/skydb/sky).
9
- It's built to allow someone to dynamically drill through and slice behavioral data in real-time.
10
- Skybox is also built to be extensible so you can extend the functionality of the server with simple Ruby.
5
+ Skybox is an analytics front-end for the [Sky database](http://skydb.io).
6
+ It's built to allow you to dynamically drill through and slice behavioral data in real-time.
11
7
 
12
8
 
13
9
  ### Install
14
10
 
15
11
  To install Skybox, simply install the gem and run the server:
16
12
 
17
- $ gem install skybox
18
- $ skybox server
19
-
20
- Skybox assumes you're running Sky locally.
21
- To point to a different server simply use the `--sky=[HOST]:[PORT]` argument:
22
-
23
- $ skybox --sky 10.0.1.1 # Assumes default port 8585
24
- $ skybox --sky 10.0.1.1:5000
25
-
26
- You can also specify the port you'd like Skybox to run on by using the `--port` or `-p` argument:
27
-
28
- $ skybox -p 8080
29
-
30
- Once Skybox is running, it will tell you where to open your browser so you can view the application.
31
-
32
-
33
- ### Extending
34
-
35
- Skybox is a Sinatra-based application and is built to be extended.
36
- To add additional functionality, simply subclass the `Skybox::App`:
37
-
38
- class MyApp < Skybox::App
39
- get '/my_page' do
40
- ...
41
- end
42
- end
43
-
44
- You'll need to override the `site.yml` which contains the navigation structure for the site.
45
- You can run `skybox generate site` in your project to write a default `site.yml` file to your project.
46
-
47
-
48
- ### Testing
49
-
50
- You can use cURL to test queries against the server like this:
51
-
52
- ```sh
53
- curl -H "Content-Type: application/json" -X POST -d '{"table":"gharchive","query":{"selections":[{"fields":[{"aggregationType":"count"}]}]}}' http://localhost:10000/query
54
- ```
55
-
56
- Or you can see the generated code from a query by using the `/query/code` endpoint:
57
-
58
13
  ```sh
59
- curl -H "Content-Type: application/json" -X POST -d '{"table":"gharchive","query":{"selections":[{"fields":[{"aggregationType":"count"}]}]}}' http://localhost:10000/query/code
14
+ $ gem install skybox
15
+ $ skybox
60
16
  ```
61
17
 
62
-
63
- ### Contributing
64
-
65
- Have a cool feature you want to see added?
66
- A bug that needs fixing?
67
- Want to keep up to date on what's happening with Skybox?
68
- Send an e-mail to the Sky mailing list: [sky@librelist.com](mailto:sky@librelist.com)!
18
+ Once Skybox is running, open your browser to `http://localhost:10000` to select your Sky table and begin analyzing.
data/bin/skybox CHANGED
@@ -7,7 +7,9 @@ require 'skydb'
7
7
  require 'commander/import'
8
8
 
9
9
  ################################################################################
10
+ #
10
11
  # Initialization
12
+ #
11
13
  ################################################################################
12
14
 
13
15
  # CLI Setup
@@ -21,16 +23,26 @@ Signal.trap('TERM') { Process.kill('KILL', 0) }
21
23
 
22
24
 
23
25
  ################################################################################
26
+ #
24
27
  # Commands
28
+ #
25
29
  ################################################################################
26
30
 
27
31
  command :server do |c|
28
32
  c.syntax = 'skybox server'
29
33
  c.description = 'Runs the Skybox server.'
30
- c.option '--port PORT', Integer, 'HTTP server port'
34
+ c.option '--sky-host HOST', String, 'The host that Sky is running on.'
35
+ c.option '--sky-port PORT', Integer, 'The port that Sky is running on.'
36
+ c.option '--port PORT', Integer, 'HTTP server port.'
37
+
31
38
  c.action do |args, options|
32
39
  options.default :port => 10000
33
- Skybox::App.run!(:port => options.port)
40
+
41
+ client = SkyDB::Client.new(:host => options.sky_host, :port => options.sky_port)
42
+ Skybox::App.run!(
43
+ :port => options.port,
44
+ :client => client
45
+ )
34
46
  end
35
47
  end
36
48
 
@@ -3,7 +3,7 @@ require 'json'
3
3
  require 'unindentable'
4
4
 
5
5
  class Skybox
6
- # The Skybox App class represents the Sinatra that can be run.
6
+ # The Skybox App class represents the Sinatra app that can be run.
7
7
  class App < Sinatra::Base
8
8
  ############################################################################
9
9
  #
@@ -25,118 +25,47 @@ class Skybox
25
25
  ############################################################################
26
26
 
27
27
  ####################################
28
- # System-level
28
+ # API
29
29
  ####################################
30
30
 
31
- # Retrieves a list of all actions.
32
- get '/tables' do
33
- @tables = SkyDB.get_tables()
34
- content_type 'application/json'
35
- JSON.dump(@tables)
36
- end
37
-
38
- # Retrieves a list of all actions.
39
- get '/actions' do
40
- SkyDB.table_name = params['table']
41
- @actions = SkyDB.get_actions()
42
- content_type 'application/json'
43
- JSON.dump(@actions)
44
- end
45
-
46
- # Retrieves a list of all properties.
47
- get '/properties' do
48
- SkyDB.table_name = params['table']
49
- @properties = SkyDB.get_properties()
50
- content_type 'application/json'
51
- JSON.dump(@properties)
31
+ # Retrieves a list of all properties on a table.
32
+ get '/api/:table_name/properties' do
33
+ table = SkyDB::Table.new(:name => params[:table_name], :client => settings.client)
34
+ properties = table.get_properties()
35
+ content_type :json
36
+ return "#{properties.to_json}\n"
52
37
  end
53
38
 
39
+ # Executes a query for a given table on the Sky server.
40
+ post '/api/:table_name/query' do
41
+ # Read the query from the POST body.
42
+ q = JSON.parse(request.env["rack.input"].read, :max_nesting => 200)
43
+ halt 422 if q.nil?
54
44
 
55
- ####################################
56
- # Analytics
57
- ####################################
58
-
59
- # Converts a JSON document to a Sky query, executes it and returns the
60
- # results.
61
- post '/query' do
62
- params = JSON.parse(request.env["rack.input"].read)
63
-
64
- # Set the table name.
65
- SkyDB.table_name = params['table']
66
-
67
- # Parse the query and return an error if it doesn't parse correctly.
68
- query = SkyDB.query.from_hash(params['query'])
69
- halt 422, params.inspect if query.nil?
70
-
71
- # Convert the result to JSON and return it.
45
+ # Execute the query on the Sky server and return the results.
46
+ warn(params)
47
+ results = settings.client.query(SkyDB::Table.new(:name => params[:table_name]), q)
72
48
  content_type :json
73
- results = query.execute
74
49
  return "#{results.to_json}\n"
75
50
  end
76
51
 
77
- # Generates the Lua code used by a given query.
78
- post '/query/code' do
79
- params = JSON.parse(request.env["rack.input"].read)
80
-
81
- # Parse the query and return an error if it doesn't parse correctly.
82
- query = SkyDB.query.from_hash(params['query'])
83
- halt 422 if query.nil?
84
-
85
- # Convert the result to JSON and return it.
86
- content_type :text
87
- return "#{query.codegen()}\n"
88
- end
89
-
90
-
91
52
 
92
53
  ####################################
93
54
  # Views
94
55
  ####################################
95
56
 
96
57
  get '/' do
97
- @tables = SkyDB.get_tables()
58
+ @tables = settings.client.get_tables()
98
59
  erb :index
99
60
  end
100
61
 
101
- get '/:table' do
102
- redirect "/#{params[:table]}/explore"
62
+ get '/:table_name' do
63
+ redirect "/#{params[:table_name]}/explore"
103
64
  end
104
65
 
105
- get '/:table/:action' do
106
- @table = params[:table]
107
- @action = params[:action]
66
+ get '/:table_name/explore' do
67
+ @table_name = params[:table_name]
108
68
  erb :explore
109
69
  end
110
-
111
-
112
- ####################################
113
- # Action Views
114
- ####################################
115
-
116
- get '/:table/admin/actions' do
117
- SkyDB.table_name = params[:table]
118
- @table = params[:table]
119
- @actions = SkyDB.get_actions()
120
- erb :'admin/actions/index'
121
- end
122
-
123
-
124
- ####################################
125
- # Property Views
126
- ####################################
127
-
128
- get '/:table/admin/properties' do
129
- SkyDB.table_name = params[:table]
130
- @table = params[:table]
131
- @properties = SkyDB.get_properties()
132
- erb :'admin/properties/index'
133
- end
134
-
135
- get '/:table/admin/properties/:id/edit' do
136
- SkyDB.table_name = params[:table]
137
- @table = params[:table]
138
- @property = SkyDB.get_property(params[:id])
139
- erb :'admin/properties/edit'
140
- end
141
70
  end
142
71
  end
@@ -107,8 +107,8 @@ d3.flow = function() {
107
107
 
108
108
  // Sort links.
109
109
  nodes.forEach(function(node) {
110
- node.sourceLinks = node.sourceLinks.sort(function(a,b) { return a.target.index-b.target.index})
111
- node.targetLinks = node.targetLinks.sort(function(a,b) { return a.source.index-b.source.index})
110
+ node.outboundLinks = node.outboundLinks.sort(function(a,b) { return a.target.index-b.target.index})
111
+ node.inboundLinks = node.inboundLinks.sort(function(a,b) { return a.source.index-b.source.index})
112
112
  });
113
113
 
114
114
  // Update everything!
@@ -123,10 +123,10 @@ d3.flow = function() {
123
123
  // Update node values from links.
124
124
  nodes.forEach(function(node) {
125
125
  // The value for the node is the sum of it's source or target links values (which ever is larger).
126
- node.sourceLinks = links.filter(function(link) {
126
+ node.outboundLinks = links.filter(function(link) {
127
127
  return link.source == node;
128
128
  });
129
- node.targetLinks = links.filter(function(link) {
129
+ node.inboundLinks = links.filter(function(link) {
130
130
  return link.target == node;
131
131
  });
132
132
  });
@@ -168,11 +168,13 @@ d3.flow = function() {
168
168
  //------------------------------------
169
169
 
170
170
  flow.nodes.layout = function(nodes) {
171
+ var yoffset = 0;
171
172
  nodes.forEach(function(node) {
172
173
  node.x = xScale(node.depth);
173
- node.y = yScale(node.offsetValue) + (verticalGap * node.index);
174
+ node.y = yScale(node.offsetValue) + (verticalGap * node.index) + yoffset;
174
175
  node.width = Math.max(0.1, maxNodeWidth);
175
176
  node.height = Math.max(minNodeHeight, heightScale(node.value));
177
+ if(node.height <= 20) yoffset += (35 - node.height);
176
178
  });
177
179
  }
178
180
 
@@ -187,7 +189,7 @@ d3.flow = function() {
187
189
  flow.nodes.title.position = function(selection) {
188
190
  selection
189
191
  .attr("x", function(d) { return d.x + titleMargin.left })
190
- .attr("y", function(d) { return d.y + titleMargin.top })
192
+ .attr("y", function(d) { return d.y + titleMargin.top + (d.height > 20 ? 0 : d.height) })
191
193
  .attr("width", function(d) { return d.width - titleMargin.left - titleMargin.right })
192
194
  .attr("height", function(d) { return d.height - titleMargin.top - titleMargin.bottom })
193
195
  }
@@ -211,7 +213,7 @@ d3.flow = function() {
211
213
  * Retrieves the immediate source of the node.
212
214
  */
213
215
  flow.nodes.source = function(node) {
214
- return (node.targetLinks.length > 0 ? node.targetLinks[0].source : null);
216
+ return (node.inboundLinks.length > 0 ? node.inboundLinks[0].source : null);
215
217
  }
216
218
 
217
219
  /**
@@ -219,8 +221,8 @@ d3.flow = function() {
219
221
  */
220
222
  flow.nodes.sources = function(node) {
221
223
  var sources = [];
222
- while(node.targetLinks.length > 0) {
223
- node = node.targetLinks[0].source;
224
+ while(node.inboundLinks.length > 0) {
225
+ node = node.inboundLinks[0].source;
224
226
  sources.unshift(node);
225
227
  }
226
228
  return sources;
@@ -234,10 +236,10 @@ d3.flow = function() {
234
236
  flow.links.layout = function(nodes, links) {
235
237
  nodes.forEach(function(node) {
236
238
  var sy = node.y;
237
- var totalTargetHeight = d3.sum(node.sourceLinks, function(l) { return l.target.height; });
238
- node.sourceLinks.forEach(function(link) {
239
+ var totalTargetHeight = d3.sum(node.outboundLinks, function(l) { return l.target.height; });
240
+ node.outboundLinks.forEach(function(link) {
239
241
  link.sy = sy;
240
- sy += link.source.height * (link.target.height/totalTargetHeight);
242
+ sy += link.target.height;
241
243
  });
242
244
  });
243
245
 
@@ -267,7 +269,7 @@ d3.flow = function() {
267
269
 
268
270
  // Set the Y scale and then modify the domain to adjust for spacing.
269
271
  var visibleNodes = nodes.filter(function(d) {
270
- return (d.depth > minDepth && d.depth <= maxDepth) || (d.depth == minDepth && d.sourceLinks.length > 0);
272
+ return (d.depth >= minDepth && d.depth <= maxDepth);
271
273
  });
272
274
  var minValue = d3.min(visibleNodes, function(d) { return d.offsetValue; });
273
275
  var maxValue = d3.max(visibleNodes, function(d) { return d.offsetValue + d.value; });
@@ -2,24 +2,26 @@
2
2
  var flow = d3.flow();
3
3
  var darkColors = ["#000", "#1f77b4"];
4
4
  var colors = d3.scale.category20();
5
- var root = {id:"enter", depth:0};
6
- var nodes = [root], links = [];
5
+ var nodes = [], links = [];
7
6
  var chart = null, svg = null, g = {};
8
7
 
9
8
  var highlightDepth = -1;
10
9
  var easingType = "quad-in";
11
10
 
12
- // Start with a simple session start query.
11
+ // Initialize with a simple query.
13
12
  var query = {
14
- selections:[{
15
- fields: [{aggregationType:"count"}],
16
- groups: [{expression:"action_id"}],
17
- conditions: [{type:"on", action:"enter"}]
18
- }],
19
- sessionIdleTime:7200
13
+ sessionIdleTime:7200,
14
+ steps: [
15
+ {type:"condition", expression:"true", within:[0,0], steps:[
16
+ {type:"selection", name:"0", dimensions:["action"], fields:[{name:"count", expression:"count()"}]}
17
+ ]}
18
+ ]
20
19
  };
21
20
 
22
21
 
22
+ if(!skybox) skybox = {};
23
+ if(!skybox.explore) skybox.explore = function(){};
24
+
23
25
  //----------------------------------------------------------------------------
24
26
  //
25
27
  // Initialization
@@ -27,7 +29,7 @@ var query = {
27
29
  //----------------------------------------------------------------------------
28
30
 
29
31
  // Initializes the view.
30
- function init() {
32
+ skybox.explore.init = function() {
31
33
  // Setup the SVG container for the visualization.
32
34
  chart = $("#chart")[0];
33
35
  svg = d3.select(chart).append("svg");
@@ -41,8 +43,8 @@ function init() {
41
43
  $(document).on("click", document_onClick);
42
44
 
43
45
  // Update!
44
- update();
45
- load(root);
46
+ skybox.explore.update();
47
+ skybox.explore.load();
46
48
  }
47
49
 
48
50
 
@@ -57,7 +59,7 @@ function init() {
57
59
  //--------------------------------------
58
60
 
59
61
  // Updates the view.
60
- function update(options) {
62
+ skybox.explore.update = function(options) {
61
63
  // Update the dimensions of the visualization.
62
64
  flow.width($(chart).width());
63
65
  flow.height(window.innerHeight - $(chart).offset().top - 40);
@@ -102,16 +104,16 @@ function update(options) {
102
104
  var enter = node.enter(), exit = node.exit();
103
105
 
104
106
  // Update selection.
105
- node.selectAll("rect")
107
+ node.selectAll("rect").data(nodes, function(d) { return d.key })
106
108
  .transition().ease(easingType)
107
109
  .call(flow.nodes.position)
108
110
  .style("fill", nodeFillColor)
109
111
  .style("fill-opacity", 1)
110
112
  .style("stroke-opacity", 1);
111
- node.selectAll(".title")
113
+ node.selectAll(".title").data(nodes, function(d) { return d.key })
112
114
  .transition().ease(easingType)
113
115
  .call(flow.nodes.title.position)
114
- .attr("display", function(d) { return (d.height > 20 ? "block" : "none"); })
116
+ .style("fill", nodeTextColor)
115
117
  .style("fill-opacity", 1);
116
118
 
117
119
  // Enter selection.
@@ -132,74 +134,36 @@ function update(options) {
132
134
  .attr("dy", "1em")
133
135
  .style("fill", nodeTextColor)
134
136
  .style("fill-opacity", function(d) { return (d.depth == 0 ? 1 : 0); })
135
- .attr("display", function(d) { return (d.height > 20 ? "block" : "none"); })
136
137
  .call(flow.nodes.title.position)
137
- .text(nodeTitle)
138
+ .text(function(d) { return d.title; })
138
139
  .transition().ease(easingType).delay(nodeDelay)
139
140
  .style("fill-opacity", 1)
140
141
 
141
142
  // Exit selection.
142
143
  exit.remove();
143
144
  });
144
-
145
- // Update the query text.
146
- updateQueryText();
147
145
  }
148
146
 
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
-
147
+
180
148
  function nodeDelay(node) {
181
149
  return 500 + (node.index*100);
182
150
  }
183
151
 
184
152
  function nodeFillColor(node) {
185
153
  var color;
186
- switch(node.id) {
187
- case "exit": color = "#000"; break;
154
+ switch(node.type) {
188
155
  case "other": color = "lightgray"; break;
189
- default: color = colors(node.id);
156
+ default: color = colors(node.expressionValue);
190
157
  }
191
158
  return color;
192
159
  }
193
160
 
194
161
  function nodeTextColor(node) {
162
+ if(node.height <= 20) return "#000";
195
163
  var fillColor = nodeFillColor(node);
196
164
  return (darkColors.indexOf(fillColor) != -1 ? "#f2f2f2" : "#000");
197
165
  }
198
166
 
199
- function nodeTitle(node) {
200
- var action = skybox.actions.find(node.id);
201
- return Humanize.truncate((action ? action.name : ""), 16);
202
- }
203
167
 
204
168
 
205
169
  //--------------------------------------
@@ -209,25 +173,16 @@ function nodeTitle(node) {
209
173
  /**
210
174
  * Runs the current query against the server, sets the returned data and
211
175
  * updates the UI.
212
- *
213
- * @param {Object} source The node that caused the load to occur.
214
176
  */
215
- function load(source) {
177
+ skybox.explore.load = function() {
216
178
  $(".loading").show();
217
179
 
218
180
  // 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();
181
+ var xhr = $.ajax("/api/" + skybox.table() + "/query", {method:"POST", data:JSON.stringify(query), contentType:"application/json"})
182
+ .success(function(data) {
183
+ nodes = skybox.explore.normalize(query, data, {limit:6});
184
+ links = skybox.explore.links(nodes);
185
+ skybox.explore.update();
231
186
  })
232
187
  // Notify the user if the query fails for some reason.
233
188
  .fail(function() {
@@ -240,6 +195,113 @@ function load(source) {
240
195
  return xhr;
241
196
  }
242
197
 
198
+ /**
199
+ * Normalizes the results into a data format that we can display in D3.
200
+ *
201
+ * @param {Object} query The query that was performed.
202
+ * @param {Object} results The results of the query.
203
+ *
204
+ * @return {Object} A list of normalized nodes.
205
+ */
206
+ skybox.explore.normalize = function(query, results, options) {
207
+ if(!options) options = {};
208
+ var nodes = [];
209
+
210
+ if(query && results) {
211
+ selections = skybox.query.selections.hash(query);
212
+ for(var selectionName in results) {
213
+ var depth = parseInt(selectionName);
214
+ var selection = selections[depth];
215
+ var dimension = selection.dimensions[0];
216
+ var field = selection.fields[0];
217
+ var items = results[selectionName][dimension];
218
+ for(var key in items) {
219
+ var item = items[key];
220
+ var node = {
221
+ id: selectionName + "." + key,
222
+ expressionValue: key,
223
+ title: key,
224
+ depth: depth,
225
+ value: item[field.name]
226
+ };
227
+ nodes.push(node);
228
+ }
229
+ }
230
+ }
231
+ nodes = nodes.sort(function(a,b) { return b.value-a.value;});
232
+
233
+ // Limit nodes.
234
+ if(options.limit > 0) {
235
+ nodes = skybox.explore.limit(nodes, options.limit);
236
+ }
237
+
238
+ return nodes;
239
+ }
240
+
241
+ /**
242
+ * Limits the number of nodes that can exist at any given depth.
243
+ *
244
+ * @param {Object} nodes A sorted list of normalized nodes.
245
+ *
246
+ * @return {Object} A list of limited nodes.
247
+ */
248
+ skybox.explore.limit = function(nodes, count) {
249
+ // Split up by depth.
250
+ var dnodes = {};
251
+ for(var i=0; i<nodes.length; i++) {
252
+ var node = nodes[i];
253
+ if(!dnodes[node.depth]) dnodes[node.depth] = [];
254
+ dnodes[node.depth].push(node);
255
+ }
256
+
257
+ // Limit each level.
258
+ for(var depth in dnodes) {
259
+ if(dnodes[depth].length > count) {
260
+ var others = dnodes[depth].splice(count-1, dnodes[depth].length-count+1);
261
+ var other = {
262
+ id: depth.toString() + ".__other__",
263
+ type:"other",
264
+ title: "Other",
265
+ depth: parseInt(depth),
266
+ value: d3.sum(others, function(d) { return d.value; })
267
+ };
268
+ dnodes[depth].push(other);
269
+ }
270
+ }
271
+
272
+ // Recombine.
273
+ nodes = [];
274
+ for(var depth in dnodes) {
275
+ nodes = nodes.concat(dnodes[depth])
276
+ }
277
+
278
+ return nodes;
279
+ }
280
+
281
+ /**
282
+ * Generates a list of links for a set of nodes.
283
+ *
284
+ * @param {Object} node The nodes.
285
+ *
286
+ * @return {Object} A list of links for d3.flow.js.
287
+ */
288
+ skybox.explore.links = function(nodes) {
289
+ var lnodes = {};
290
+ for(var i=0; i<nodes.length; i++) {
291
+ lnodes[nodes[i].depth] = nodes[i];
292
+ }
293
+
294
+ var links = [];
295
+ for(var i=0; i<nodes.length; i++) {
296
+ var node = nodes[i];
297
+ var source = lnodes[node.depth-1];
298
+ if(source) {
299
+ links.push({source:source, target:node, value:node.value});
300
+ }
301
+ }
302
+
303
+ return links;
304
+ }
243
305
 
244
306
  //----------------------------------------------------------------------------
245
307
  //
@@ -255,33 +317,31 @@ function load(source) {
255
317
  * Appends an 'After' condition to the query for a node and re-queries.
256
318
  */
257
319
  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)
320
+ if(node.type == "other") return;
321
+
322
+ selections = skybox.query.selections.hash(query);
323
+ selection = selections[node.depth.toString()];
324
+ condition = skybox.query.selections.parent(query, selection);
325
+ condition.expression = "action == '" + node.expressionValue + "'";
326
+ condition.steps = [
327
+ selection,
328
+ {type:"condition", expression:"true", within:[1,1], steps:[
329
+ {type:"selection", name:(node.depth+1).toString(), dimensions:["action"], fields:[{name:"count", expression:"count()"}]}
330
+ ]}
331
+ ];
332
+
333
+ skybox.explore.load()
273
334
  }
274
335
 
275
336
  /**
276
337
  * Shows a tooltip on mouse over.
277
338
  */
278
339
  function node_onMouseOver(node) {
279
- var action = skybox.actions.find(node.id);
280
340
  $(this).tooltip({
281
341
  html: true, container:"body",
282
342
  placement: (node.depth == 0 ? "right" : "left"),
283
- title:
284
- Humanize.truncate(action.name, 30) + "<br/>" +
343
+ title:
344
+ node.title + "<br/>" +
285
345
  "Count: " + Humanize.intcomma(node.value)
286
346
  });
287
347
  $(this).tooltip("show");
@@ -296,7 +356,7 @@ function node_onMouseOver(node) {
296
356
  * Updates the view whenever the window is resized.
297
357
  */
298
358
  function window_onResize() {
299
- update();
359
+ skybox.explore.update();
300
360
  }
301
361
 
302
362
  /**
@@ -308,23 +368,5 @@ function document_onClick() {
308
368
  $("*").popover("hide");
309
369
  }
310
370
  }
311
-
312
-
313
- //----------------------------------------------------------------------------
314
- //
315
- // Public Interface
316
- //
317
- //----------------------------------------------------------------------------
318
-
319
- skybox.explore = {
320
- init:init,
321
- update:update,
322
- };
323
-
324
371
  })();
325
372
 
326
-
327
- // Initialize the Explore view once the page has loaded.
328
- skybox.ready(function() {
329
- skybox.explore.init();
330
- });