skybox 0.2.3.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- });