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.
- 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
|
-
});
|