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.
- checksums.yaml +15 -0
- data/README.md +6 -56
- data/bin/skybox +14 -2
- data/lib/skybox/app.rb +21 -92
- data/lib/skybox/static/js/d3.flow.js +15 -13
- data/lib/skybox/static/js/skybox.explore.js +153 -111
- data/lib/skybox/static/js/skybox.js +2 -145
- data/lib/skybox/static/js/skybox.query.js +96 -0
- data/lib/skybox/version.rb +1 -1
- data/lib/skybox/views/explore.erb +8 -5
- data/lib/skybox/views/index.erb +1 -1
- data/lib/skybox/views/layout.erb +5 -18
- metadata +22 -53
- data/lib/skybox/views/admin/actions/index.erb +0 -22
- data/lib/skybox/views/admin/properties/index.erb +0 -26
checksums.yaml
ADDED
@@ -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](
|
9
|
-
It's built to allow
|
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
|
-
|
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 '--
|
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
|
-
|
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
|
|
data/lib/skybox/app.rb
CHANGED
@@ -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
|
-
#
|
28
|
+
# API
|
29
29
|
####################################
|
30
30
|
|
31
|
-
# Retrieves a list of all
|
32
|
-
get '/
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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
|
-
|
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 =
|
58
|
+
@tables = settings.client.get_tables()
|
98
59
|
erb :index
|
99
60
|
end
|
100
61
|
|
101
|
-
get '/:
|
102
|
-
redirect "/#{params[:
|
62
|
+
get '/:table_name' do
|
63
|
+
redirect "/#{params[:table_name]}/explore"
|
103
64
|
end
|
104
65
|
|
105
|
-
get '/:
|
106
|
-
@
|
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.
|
111
|
-
node.
|
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.
|
126
|
+
node.outboundLinks = links.filter(function(link) {
|
127
127
|
return link.source == node;
|
128
128
|
});
|
129
|
-
node.
|
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.
|
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.
|
223
|
-
node = node.
|
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.
|
238
|
-
node.
|
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.
|
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
|
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
|
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
|
-
//
|
11
|
+
// Initialize with a simple query.
|
13
12
|
var query = {
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
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(
|
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
|
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
|
-
.
|
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(
|
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.
|
187
|
-
case "exit": color = "#000"; break;
|
154
|
+
switch(node.type) {
|
188
155
|
case "other": color = "lightgray"; break;
|
189
|
-
default: color = colors(node.
|
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
|
177
|
+
skybox.explore.load = function() {
|
216
178
|
$(".loading").show();
|
217
179
|
|
218
180
|
// Execute the query.
|
219
|
-
var xhr = $.
|
220
|
-
|
221
|
-
skybox.
|
222
|
-
|
223
|
-
|
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.
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
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
|
-
|
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
|
-
});
|