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