+
+
diff -r 000000000000 -r 95ff566506f4 hexagram/hexagram.js
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/hexagram.js Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,2776 @@
+// hexagram.js
+// Run the hexagram visualizer client.
+
+// Globals
+// This is a mapping from coordinates [x][y] in the global hex grid to signature
+// name
+var signature_grid = [];
+
+// This holds a global list of layer pickers in layer order. It is also the
+// authority on what layers are currently selected.
+var layer_pickers = [];
+
+// This holds a list of layer objects by name.
+// Layer objects have:
+// A downloading function "downloader"
+// A data object (from hex name to float) "data"
+// A magnitude "magnitude"
+// A boolean "selection" that specifies whether this is a user selection or not.
+// (This may be absent, which is the same as false.)
+// Various optional metadata fields
+var layers = {};
+
+// This is a list of layer names maintained in sorted order.
+var layer_names_sorted = [];
+
+// This holds an array of layer names that the user has added to the "shortlist"
+// They can be quickly selected for display.
+var shortlist = [];
+
+// This holds an object form shortlisted layer names to jQuery shortlist UI
+// elements, so we can efficiently tell if e.g. one is selected.
+var shortlist_ui = {};
+
+// This holds colormaps (objects from layer values to category objects with a
+// name and color). They are stored under the name of the layer they apply to.
+var colormaps = {}
+
+// This holds an array of the available score matrix filenames
+var available_matrices = [];
+
+// This holds the Google Map that we use for visualization
+var googlemap = null;
+
+// This is the global Google Maps info window. We only want one hex to have its
+// info open at a time.
+var info_window = null;
+
+// This holds the signature name of the hex that the info window is currently
+// about.
+var selected_signature = undefined;
+
+// Which tool is the user currently using (string name or undefined for no tool)
+// TODO: This is a horrible hack, replace it with a unified tool system at once.
+var selected_tool = undefined;
+
+// This holds the grid of hexagon polygons on that Google Map.
+var polygon_grid = [];
+
+// This holds an object of polygons by signature name
+var polygons = {};
+
+// How big is a hexagon in google maps units? This gets filled in once we have
+// the hex assignment data. (This is really the side length.)
+var hex_size;
+
+// This holds a handle for the currently enqueued view redrawing timeout.
+var redraw_handle;
+
+// This holds all the currently active tool event listeners.
+// They are indexed by handle, and are objects with a "handler" and an "event".
+var tool_listeners = {};
+
+// This holds the next tool listener handle to give out
+var tool_listener_next_id = 0;
+
+// This holds the next selection number to use. Start at 1 since the user sees
+// these.
+var selection_next_id = 1;
+
+// This is a pool of statistics Web Workers.
+var rpc_workers = [];
+
+// This holds which RPC worker we ought to give work to next.
+// TODO: Better scheduling, and wrap all this into an RPC object.
+var next_free_worker = 0;
+
+// This holds how namy RPC jobs are currently running
+var jobs_running = 0;
+
+// This is the object of pending callbacks by RPC id
+var rpc_callbacks = {};
+
+// This is the next unallocated RPC id
+var rpc_next_id = 0;
+
+// How many statistics Web Workers should we start?
+var NUM_RPC_WORKERS = 10;
+
+// What's the minimum number of pixels that hex_size must represent at the
+// current zoom level before we start drawing hex borders?
+var MIN_BORDER_SIZE = 10;
+
+// And how thick should the border be when drawn?
+var HEX_STROKE_WEIGHT = 2;
+
+// How many layers do we know how to draw at once?
+var MAX_DISPLAYED_LAYERS = 2;
+
+// How many layer search results should we display at once?
+var SEARCH_PAGE_SIZE = 10;
+
+// How big is our color key in pixels?
+var KEY_SIZE = 100;
+
+// This is an array of all Google Maps events that tools can use.
+var TOOL_EVENTS = [
+ "click",
+ "mousemove"
+];
+
+function print(text) {
+ // Print some logging text to the browser console
+
+ if(console && console.log) {
+ // We know the console exists, and we can log to it.
+ console.log(text);
+ }
+}
+
+function complain(text) {
+ // Display a temporary error message to the user.
+ $("#error-notification").text(text);
+ $(".error").show().delay(1000).fadeOut(400);
+
+ if(console && console.error) {
+ // Inform the browser console of this problem.as
+ console.error(text);
+ }
+}
+
+function make_hexagon(row, column, hex_side_length, grid_offset) {
+ // Make a new hexagon representing the hexagon at the given grid coordinates.
+ // hex_side_length is the side length of hexagons in Google Maps world
+ // coordinate units. grid_offset specifies a distance to shift the whole
+ // grid down and right from the top left corner of the map. This lets us
+ // keep the whole thing away from the edges of the "earth", where Google
+ // Maps likes to wrap.
+ // Returns the Google Maps polygon.
+
+ // How much horizontal space is needed per hex on average, stacked the
+ // way we stack them (wiggly)?
+ var hex_column_width = 3.0/2.0 * hex_side_length;
+
+ // How tall is a hexagon?
+ var hex_height = Math.sqrt(3) * hex_side_length;
+
+ // How far apart are hexagons on our grid, horizontally (world coordinate units)?
+ var hex_padding_horizontal = 0;
+
+ // And vertically (world coordinate units)?
+ var hex_padding_veritcal = 0;
+
+ // First, what are x and y in 0-256 world coordinates fo this grid position?
+ var x = column * (hex_column_width + hex_padding_horizontal);
+ var y = row * (hex_height + hex_padding_veritcal);
+ if(column % 2 == 1) {
+ // Odd columns go up
+ y -= hex_height / 2;
+ }
+
+ // Apply the grid offset to this hex
+ x += grid_offset;
+ y += grid_offset;
+
+ // That got X and Y for the top left corner of the bounding box. Shift to
+ // the center.
+ x += hex_side_length;
+ y += hex_height / 2;
+
+ // Offset the whole thing so no hexes end up off the map when they wiggle up
+ y += hex_height / 2;
+
+ // This holds an array of all the hexagon corners
+ var coords = [
+ get_LatLng(x - hex_side_length, y),
+ get_LatLng(x - hex_side_length / 2, y - hex_height / 2),
+ get_LatLng(x + hex_side_length / 2, y - hex_height / 2),
+ get_LatLng(x + hex_side_length, y),
+ get_LatLng(x + hex_side_length / 2, y + hex_height / 2),
+ get_LatLng(x - hex_side_length / 2, y + hex_height / 2),
+ ];
+
+ // We don't know whether the hex should start with a stroke or not without
+ // looking at the current zoom level.
+ // Get the current zoom level (low is out)
+ var zoom = googlemap.getZoom();
+
+ // API docs say: pixelCoordinate = worldCoordinate * 2 ^ zoomLevel
+ // So this holds the number of pixels that the global length hex_size
+ // corresponds to at this zoom level.
+ var hex_size_pixels = hex_size * Math.pow(2, zoom);
+
+ // Construct the Polygon
+ var hexagon = new google.maps.Polygon({
+ paths: coords,
+ strokeColor: "#000000",
+ strokeOpacity: 1.0,
+ // Only turn on the border if we're big enough
+ strokeWeight: hex_size_pixels < MIN_BORDER_SIZE ? 0 : HEX_STROKE_WEIGHT,
+ fillColor: "#FF0000",
+ fillOpacity: 1.0
+ });
+
+ // Attach the hexagon to the global map
+ hexagon.setMap(googlemap);
+
+ // Set up the click listener to move the global info window to this hexagon
+ // and display the hexagon's information
+ google.maps.event.addListener(hexagon, "click", function(event) {
+ if(selected_tool == undefined) {
+ // The user isn't trying to use a tool currently, so we can use
+ // their clicks for the infowindow.
+
+ // Remove the window from where it currently is
+ info_window.close();
+
+ // Place the window in the center of this hexagon.
+ info_window.setPosition(get_LatLng(x, y));
+
+ // Record that this signature is selected now
+ selected_signature = hexagon.signature;
+
+ // Calculate the window's contents and make it display them.
+ redraw_info_window();
+ }
+ });
+
+ // Subscribe the tool listeners to events on this hexagon
+ subscribe_tool_listeners(hexagon);
+
+ return hexagon;
+}
+
+function set_hexagon_signature(hexagon, text) {
+ // Given a polygon representing a hexagon, set the signature that the
+ // hexagon represents.
+ hexagon.signature = text;
+}
+
+function set_hexagon_color(hexagon, color) {
+ // Given a polygon, set the hexagon's current background
+ // color.
+
+ hexagon.setOptions({
+ fillColor: color
+ });
+}
+
+function set_hexagon_stroke_weight(hexagon, weight) {
+ // Given a polygon, set the weight of hexagon's border stroke, in number of
+ // screen pixels.
+
+ hexagon.setOptions({
+ strokeWeight: weight
+ });
+}
+
+function redraw_info_window() {
+ // Set the contents of the global info window to reflect the currently
+ // visible information about the global selected signature.
+
+ if(selected_signature == undefined) {
+ // No need to update anything
+ return;
+ }
+
+ // Go get the infocard that goes in the info_window and, when it's
+ // prepared, display it.
+ with_infocard(selected_signature, function(infocard) {
+ // The [0] is supposed to get the DOM element from the jQuery
+ // element.
+ info_window.setContent(infocard[0]);
+
+ // Open the window. It may already be open, or it may be closed but
+ // properly positioned and waiting for its initial contents before
+ // opening.
+ info_window.open(googlemap);
+ });
+}
+
+function with_infocard(signature, callback) {
+ // Given a signature, call the callback with a jQuery element representing
+ // an "info card" about that signature. It's the contents of the infowindow
+ // that we want to appear when the user clicks on the hex representing this
+ // signature, and it includes things like the signature name and its values
+ // under any displayed layers (with category names if applicable).
+ // We return by callback because preparing the infocard requires reading
+ // from the layers, which are retrieved by callback.
+ // TODO: Can we say that we will never have to download a layer here and
+ // just directly access them? Is that neater or less neat?
+
+ // Using jQuery to build this saves us from HTML injection by making jQuery
+ // do all the escaping work (we only ever set text).
+
+ function row(key, value) {
+ // Small helper function that returns a jQuery element that displays the
+ // given key being the given value.
+
+ // This holds the root element of the row
+ var root = $("").addClass("info-row");
+
+ // Add the key and value elements
+ root.append($("").addClass("info-key").text(key));
+ root.append($("").addClass("info-value").text(value));
+
+ return root;
+ }
+
+ // This holds a list of the string names of the currently selected layers,
+ // in order.
+ // Just use everything on the shortlist.
+ var current_layers = shortlist;
+
+ // Obtain the layer objects (mapping from signatures/hex labels to colors)
+ with_layers(current_layers, function(retrieved_layers) {
+
+ // This holds the root element of the card.
+ var infocard = $("").addClass("infocard");
+
+ infocard.append(row("Name", signature).addClass("info-name"));
+
+ for(var i = 0; i < current_layers.length; i++) {
+ // This holds the layer's value for this signature
+ var layer_value = retrieved_layers[i].data[signature];
+
+ if(have_colormap(current_layers[i])) {
+ // This is a color map
+
+ // This holds the category object for this category number, or
+ // undefined if there isn't one.
+ var category = colormaps[current_layers[i]][layer_value];
+
+ if(category != undefined) {
+ // There's a specific entry for this category, with a
+ // human-specified name and color.
+ // Use the name as the layer value
+ layer_value = category.name;
+ }
+ }
+
+ if(layer_value == undefined) {
+ // Let the user know that there's nothing there in this layer.
+ layer_value = "";
+ }
+
+ // Make a listing for this layer's value
+ infocard.append(row(current_layers[i], layer_value));
+ }
+
+ // Return the infocard by callback
+ callback(infocard);
+ });
+
+}
+
+function add_layer_url(layer_name, layer_url, attributes) {
+ // Add a layer with the given name, to be downloaded from the given URL, to
+ // the list of available layers.
+ // Attributes is an object of attributes to copy into the layer.
+
+ // Store the layer. Just keep the URL, since with_layer knows what to do
+ // with it.
+ layers[layer_name] = {
+ url: layer_url,
+ data: undefined,
+ magnitude: undefined
+ };
+
+ for(var name in attributes) {
+ // Copy over each specified attribute
+ layers[layer_name][name] = attributes[name];
+ }
+
+ // Add it to the sorted layer list.
+ layer_names_sorted.push(layer_name);
+
+ // Don't sort because our caller does that when they're done adding layers.
+
+}
+
+function add_layer_data(layer_name, data, attributes) {
+ // Add a layer with the given name, with the given data to the list of
+ // available layers.
+ // Attributes is an object of attributes to copy into the layer.
+
+ // Store the layer. Just put in the data. with_layer knows what to do if the
+ // magnitude isn't filled in.
+ layers[layer_name] = {
+ url: undefined,
+ data: data,
+ magnitude: undefined
+ };
+
+ for(var name in attributes) {
+ // Copy over each specified attribute
+ layers[layer_name][name] = attributes[name];
+ }
+
+ // Add it to the sorted layer list and sort
+ layer_names_sorted.push(layer_name);
+
+ // Don't sort because our caller does that when they're done adding layers.
+}
+
+function with_layer(layer_name, callback) {
+ // Run the callback, passing it the layer (object from hex label/signature
+ // to float) with the given name.
+ // This is how you get layers, and allows for layers to be downloaded
+ // dynamically.
+ // have_layer must return true for the given name.
+
+ // First get what we have stored for the layer
+ var layer = layers[layer_name];
+
+ if(layer.data == undefined) {
+ // We need to download the layer.
+ print("Downloading \"" + layer.url + "\"");
+
+ // Go get it (as text!)
+ $.get(layer.url, function(layer_tsv_data) {
+
+ // This is the TSV as parsed by our TSV-parsing plugin
+ var layer_parsed = $.tsv.parseRows(layer_tsv_data);
+
+ // This is the layer we'll be passing out. Maps from
+ // signatures to floats on -1 to 1.
+ var layer_data = {};
+
+ for(var j = 0; j < layer_parsed.length; j++) {
+ // This is the label of the hex
+ var label = layer_parsed[j][0];
+
+ if(label == "") {
+ // Skip blank lines
+ continue;
+ }
+
+ // This is the heat level (-1 to 1)
+ var heat = parseFloat(layer_parsed[j][1]);
+
+ // Store in the layer
+ layer_data[label] = heat;
+ }
+
+ // Save the layer data locally
+ layers[layer_name].data = layer_data;
+
+ // Now the layer has been properly downloaded, but it may not have
+ // metadata. Recurse with the same callback to get metadata.
+ with_layer(layer_name, callback);
+ }, "text");
+ } else if(layer.magnitude == undefined) {
+ // We've downloaded it already, or generated it locally, but we don't
+ // know the magnitude. Compute that and check if it's a colormap.
+
+ // Grab the data, which we know is defined.
+ var layer_data = layers[layer_name].data;
+
+ // Store the maximum magnitude in the layer
+ // -1 is a good starting value since this always comes out positive
+ var magnitude = -1;
+
+ // We also want to know if all layer entries are non-negative
+ // integers (and it is thus valid as a colormap).
+ // If so, we want to display it as a colormap, so we will add an
+ // empty entry to the colormaps object (meaning we should
+ // auto-generate the colors on demand).
+ // This stores whether the layer is all integrs
+ all_nonnegative_integers = true;
+
+ for(var signature_name in layer_data) {
+ // Take the new max if it's bigger (and thus not something silly
+ // like NaN).
+ // This holds the potential new max magnitude.
+ var new_magnitude = Math.abs(layer_data[signature_name]);
+ if(new_magnitude > magnitude) {
+ magnitude = new_magnitude;
+ }
+
+ if(layer_data[signature_name] % 1 !== 0 ||
+ layer_data[signature_name] < 0 ) {
+
+ // If we have an illegal value for a colormap, record that
+ // fact
+ // See http://stackoverflow.com/a/3886106
+
+ all_nonnegative_integers = false;
+ }
+ }
+
+ // Save the layer magnitude for later.
+ layer.magnitude = magnitude;
+
+ if(!have_colormap(layer_name) && all_nonnegative_integers) {
+ // Add an empty colormap for this layer, so that
+ // auto-generated discrete colors will be used.
+ // TODO: Provide some way to override this if you really do want
+ // to see integers as a heatmap?
+ // The only overlap with the -1 to 1 restricted actual layers
+ // is if you have a data set with only 0s and 1s. Is it a
+ // heatmap layer or a colormap layer?
+ colormaps[layer_name] = {};
+ print("Inferring that " + layer_name +
+ " is really a colormap");
+ }
+
+ // Now layer metadata has been filled in. Call the callback.
+ callback(layer);
+ } else {
+ // It's already downloaded, and already has metadata.
+ // Pass it to our callback
+ callback(layer);
+ }
+}
+
+function with_layers(layer_list, callback) {
+ // Given an array of layer names, call the callback with an array of the
+ // corresponding layer objects (objects from signatures to floats).
+ // Conceptually it's like calling with_layer several times in a loop, only
+ // because the whole thing is continuation-based we have to phrase it in
+ // terms of recursion.
+
+ // See http://marijnhaverbeke.nl/cps/
+ // "So, we've created code that does exactly the same as the earlier
+ // version, but is twice as confusing."
+
+ if(layer_list.length == 0) {
+ // Base case: run the callback with an empty list
+ callback([]);
+ } else {
+ // Recursive case: handle the last thing in the list
+ with_layers(layer_list.slice(0, layer_list.length - 1),
+ function(rest) {
+
+ // We've recursively gotten all but the last layer
+ // Go get the last one, and pass the complete array to our callback.
+
+ with_layer(layer_list[layer_list.length - 1],
+ function(last) {
+
+ // Mutate the array. Shouldn't matter because it won't matter
+ // for us if callback does it.
+ rest.push(last);
+
+ // Send the complete array to the callback.
+ callback(rest);
+
+ });
+
+ });
+
+ }
+}
+
+function have_layer(layer_name) {
+ // Returns true if a layer exists with the given name, false otherwise.
+ return layers.hasOwnProperty(layer_name);
+}
+
+function make_shortlist_ui(layer_name) {
+ // Return a jQuery element representing the layer with the given name in the
+ // shortlist UI.
+
+ // This holds the root element for this shortlist UI entry
+ var root = $("").addClass("shortlist-entry");
+ root.data("layer", layer_name);
+
+ // If this is a selection, give the layer a special class
+ // TODO: Justify not having to use with_layer because this is always known
+ // client-side
+ if(layers[layer_name].selection) {
+ root.addClass("selection");
+ }
+
+ // We have some configuration stuff and then the div from the dropdown
+ // This holds all the config stuff
+ var controls = $("").addClass("shortlist-controls");
+
+ // Add a remove link
+ var remove_link = $("").addClass("remove").attr("href", "#").text("X");
+
+ controls.append(remove_link);
+
+ // Add a checkbox for whether this is enabled or not
+ var checkbox = $("").attr("type", "checkbox").addClass("layer-on");
+
+ controls.append(checkbox);
+
+ root.append(controls);
+
+ var contents = $("").addClass("shortlist-contents");
+
+ // Add the layer name
+ contents.append($("").text(layer_name));
+
+ // Add all of the metadata. This is a div to hold it
+ var metadata_holder = $("").addClass("metadata-holder");
+
+ // Fill it in
+ fill_layer_metadata(metadata_holder, layer_name);
+
+ contents.append(metadata_holder);
+
+ // Add a div to hold the filtering stuff so it wraps together.
+ var filter_holder = $("").addClass("filter-holder");
+
+ // Add an image label for the filter control.
+ // TODO: put this in a label
+ var filter_image = $("").attr("src", "filter.svg")
+ filter_image.addClass("control-icon");
+ filter_image.attr("title", "Filter on Layer");
+ filter_image.addClass("filter");
+
+ // Add a control for filtering
+ var filter_control = $("").attr("type", "checkbox");
+ filter_control.addClass("filter-on");
+
+ filter_holder.append(filter_image);
+ filter_holder.append(filter_control);
+
+ // Add a text input to specify a filtering threshold for continuous layers
+ var filter_threshold = $("").addClass("filter-threshold");
+ // Initialize to a reasonable value.
+ filter_threshold.val(0);
+ filter_holder.append(filter_threshold);
+
+ // Add a select input to pick from a discrete list of values to filter on
+ var filter_value = $("").addClass("filter-value");
+ filter_holder.append(filter_value);
+
+ contents.append(filter_holder);
+ if(layers[layer_name].selection) {
+ // We can do statistics on this layer.
+
+ // Add a div to hold the statistics stuff so it wraps together.
+ var statistics_holder = $("").addClass("statistics-holder");
+
+ // Add an icon
+ var statistics_image = $("").attr("src", "statistics.svg");
+ statistics_image.addClass("control-icon");
+ statistics_image.attr("title", "Statistics Group");
+ statistics_holder.append(statistics_image);
+
+ // Label the "A" radio button.
+ var a_label = $("").addClass("radio-label").text("A");
+ statistics_holder.append(a_label);
+
+ // Add a radio button for being the "A" group
+ var statistics_a_control = $("").attr("type", "radio");
+ statistics_a_control.attr("name", "statistics-a");
+ statistics_a_control.addClass("statistics-a");
+ // Put the layer name in so it's easy to tell which layer is A.
+ statistics_a_control.data("layer-name", layer_name);
+ statistics_holder.append(statistics_a_control);
+
+ // And a link to un-select it if it's selected
+ var statistics_a_clear = $("").attr("href", "#").text("X");
+ statistics_a_clear.addClass("radio-clear");
+ statistics_holder.append(statistics_a_clear);
+
+ // Label the "B" radio button.
+ var b_label = $("").addClass("radio-label").text("B");
+ statistics_holder.append(b_label);
+
+ // Add a radio button for being the "B" group
+ var statistics_b_control = $("").attr("type", "radio");
+ statistics_b_control.attr("name", "statistics-b");
+ statistics_b_control.addClass("statistics-b");
+ // Put the layer name in so it's easy to tell which layer is A.
+ statistics_b_control.data("layer-name", layer_name);
+ statistics_holder.append(statistics_b_control);
+
+ // And a link to un-select it if it's selected
+ var statistics_b_clear = $("").attr("href", "#").text("X");
+ statistics_b_clear.addClass("radio-clear");
+ statistics_holder.append(statistics_b_clear);
+
+ contents.append(statistics_holder);
+
+ // Statistics UI logic
+
+ // Make the clear links work
+ statistics_a_clear.click(function() {
+ statistics_a_control.prop("checked", false);
+ });
+ statistics_b_clear.click(function() {
+ statistics_b_control.prop("checked", false);
+ });
+ }
+
+ // Add a div to contain layer settings
+ var settings = $("").addClass("settings");
+
+ // Add a slider for setting the min and max for drawing
+ var range_slider = $("").addClass("range range-slider");
+ settings.append($("").addClass("stacker").append(range_slider));
+
+ // And a box that tells us what we have selected in the slider.
+ var range_display = $("").addClass("range range-display");
+ range_display.append($("").addClass("low"));
+ range_display.append(" to ");
+ range_display.append($("").addClass("high"));
+ settings.append($("").addClass("stacker").append(range_display));
+
+ contents.append(settings);
+
+ root.append(contents);
+
+ // Handle enabling and disabling
+ checkbox.change(function() {
+ if($(this).is(":checked") && get_current_layers().length >
+ MAX_DISPLAYED_LAYERS) {
+
+ // Enabling this checkbox puts us over the edge, so un-check it
+ $(this).prop("checked", false);
+
+ // Skip the redraw
+ return;
+ }
+
+ refresh();
+ });
+
+ // Run the removal process
+ remove_link.click(function() {
+ // Remove this layer from the shortlist
+ shortlist.splice(shortlist.indexOf(layer_name), 1);
+
+ // Remove this from the DOM
+ root.remove();
+
+ // Make the UI match the list.
+ update_shortlist_ui();
+
+ if(checkbox.is(":checked") || filter_control.is(":checked")) {
+ // Re-draw the view since we were selected (as coloring or filter)
+ // before removal.
+ refresh();
+ }
+ });
+
+ // Functionality for turning filtering on and off
+ filter_control.change(function() {
+ if(filter_control.is(":checked")) {
+ // First, figure out what kind of filter settings we take based on
+ // what kind of layer we are.
+ with_layer(layer_name, function(layer) {
+ if(have_colormap(layer_name)) {
+ // A discrete layer.
+ // Show the value picker.
+ filter_value.show();
+
+ // Make sure we have all our options
+ if(filter_value.children().length == 0) {
+ // No options available. We have to add them.
+ // TODO: Is there a better way to do this than asking
+ // the DOM?
+
+ for(var i = 0; i < layer.magnitude + 1; i++) {
+ // Make an option for each value.
+ var option = $("").attr("value", i);
+
+ if(colormaps[layer_name].hasOwnProperty(i)) {
+ // We have a real name for this value
+ option.text(colormaps[layer_name][i].name);
+ } else {
+ // No name. Use the number.
+ option.text(i);
+ }
+
+ filter_value.append(option);
+
+ }
+
+ // Select the last option, so that 1 on 0/1 layers will
+ // be selected by default.
+ filter_value.val(
+ filter_value.children().last().attr("value"));
+
+ }
+ } else {
+ // Not a discrete layer, so we take a threshold.
+ filter_threshold.show();
+ }
+
+ // Now that the right controls are there, assume they have
+ refresh();
+ });
+ } else {
+ // Hide the filtering settings
+ filter_value.hide();
+ filter_threshold.hide();
+
+ // Draw view since we're no longer filtering on this layer.
+ refresh();
+ }
+ });
+
+ // Respond to changes to filter configuration
+ filter_value.change(refresh);
+
+ // TODO: Add a longer delay before refreshing here so the user can type more
+ // interactively.
+ filter_threshold.keyup(refresh);
+
+ // Configure the range slider
+
+ // First we need a function to update the range display, which we will run
+ // on change and while sliding (to catch both user-initiated and
+ //programmatic changes).
+ var update_range_display = function(event, ui) {
+ range_display.find(".low").text(ui.values[0].toFixed(3));
+ range_display.find(".high").text(ui.values[1].toFixed(3));
+ }
+
+ range_slider.slider({
+ range: true,
+ min: -1,
+ max: 1,
+ values: [-1, 1],
+ step: 1E-9, // Ought to be fine enough
+ slide: update_range_display,
+ change: update_range_display,
+ stop: function(event, ui) {
+ // The user has finished sliding
+ // Draw the view. We will be asked for our values
+ refresh();
+ }
+ });
+
+ // When we have time, go figure out whether the slider should be here, and
+ // what its end values should be.
+ reset_slider(layer_name, root)
+
+ return root;
+}
+
+function update_shortlist_ui() {
+ // Go through the shortlist and make sure each layer there has an entry in
+ // the shortlist UI, and that each UI element has an entry in the shortlist.
+ // Also make sure the metadata for all existing layers is up to date.
+
+ // Clear the existing UI lookup table
+ shortlist_ui = {};
+
+ for(var i = 0; i < shortlist.length; i++) {
+ // For each shortlist entry, put a false in the lookup table
+ shortlist_ui[shortlist[i]] = false;
+ }
+
+
+ $("#shortlist").children().each(function(index, element) {
+ if(shortlist_ui[$(element).data("layer")] === false) {
+ // There's a space for this element: it's still in the shortlist
+
+ // Fill it in
+ shortlist_ui[$(element).data("layer")] = $(element);
+
+ // Update the metadata in the element. It make have changed due to
+ // statistics info coming back.
+ fill_layer_metadata($(element).find(".metadata-holder"),
+ $(element).data("layer"));
+ } else {
+ // It wasn't in the shortlist, so get rid of it.
+ $(element).remove();
+ }
+ });
+
+ for(var layer_name in shortlist_ui) {
+ // For each entry in the lookup table
+ if(shortlist_ui[layer_name] === false) {
+ // If it's still false, make a UI element for it.
+ shortlist_ui[layer_name] = make_shortlist_ui(layer_name);
+ $("#shortlist").prepend(shortlist_ui[layer_name]);
+
+ // Check it's box if possible
+ shortlist_ui[layer_name].find(".layer-on").click();
+ }
+ }
+
+ // Make things re-orderable
+ // Be sure to re-draw the view if the order changes, after the user puts
+ // things down.
+ $("#shortlist").sortable({
+ update: refresh,
+ // Sort by the part with the lines icon, so we can still select text.
+ handle: ".shortlist-controls"
+ });
+
+}
+
+function layer_sort_order(a, b) {
+ // A sort function defined on layer names.
+ // Return <0 if a belongs before b, >0 if a belongs after
+ // b, and 0 if their order doesn't matter.
+
+ // Sort by selection status, then p_value, then clumpiness, then (for binary
+ // layers that are not selections) the frequency of the less common value,
+ // then alphabetically by name if all else fails.
+
+ // Note that we can consult the layer metadata "n" and "positives" fields to
+ // calculate the frequency of the least common value in binary layers,
+ // without downloading them.
+
+ if(layers[a].selection && !layers[b].selection) {
+ // a is a selection and b isn't, so put a first.
+ return -1;
+ } else if(layers[b].selection && !layers[a].selection) {
+ // b is a selection and a isn't, so put b first.
+ return 1;
+ }
+
+ if(layers[a].p_value < layers[b].p_value) {
+ // a has a lower p value, so put it first.
+ return -1;
+ } else if(layers[b].p_value < layers[a].p_value) {
+ // b has a lower p value. Put it first instead.
+ return 1;
+ } else if(isNaN(layers[b].p_value) && !isNaN(layers[a].p_value)) {
+ // a has a p value and b doesn't, so put a first
+ return -1;
+ } else if(!isNaN(layers[b].p_value) && isNaN(layers[a].p_value)) {
+ // b has a p value and a doesn't, so put b first.
+ return 1;
+ }
+
+ if(layers[a].clumpiness < layers[b].clumpiness) {
+ // a has a lower clumpiness score, so put it first.
+ return -1;
+ } else if(layers[b].clumpiness < layers[a].clumpiness) {
+ // b has a lower clumpiness score. Put it first instead.
+ return 1;
+ } else if(isNaN(layers[b].clumpiness) && !isNaN(layers[a].clumpiness)) {
+ // a has a clumpiness score and b doesn't, so put a first
+ return -1;
+ } else if(!isNaN(layers[b].clumpiness) && isNaN(layers[a].clumpiness)) {
+ // b has a clumpiness score and a doesn't, so put b first.
+ return 1;
+ }
+
+
+
+ if(!layers[a].selection && !isNaN(layers[a].positives) && layers[a].n > 0 &&
+ !layers[b].selection && !isNaN(layers[b].positives) &&
+ layers[b].n > 0) {
+
+ // We have checked to see each layer is supposed to be bianry layer
+ // without downloading. TODO: This is kind of a hack. Redesign the
+ // whole system with a proper concept of layer type.
+
+ // We've also verified they both have some data in them. Otherwise we
+ // might divide by 0 trying to calculate frequency.
+
+ // Two binary layers (not selections).
+ // Compute the frequency of the least common value for each
+
+ // This is the frequency of the least common value in a (will be <=1/2)
+ var minor_frequency_a = layers[a].positives / layers[a].n;
+ if(minor_frequency_a > 0.5) {
+ minor_frequency_a = 1 - minor_frequency_a;
+ }
+
+ // And this is the same frequency for the b layer
+ var minor_frequency_b = layers[b].positives / layers[b].n;
+ if(minor_frequency_b > 0.5) {
+ minor_frequency_b = 1 - minor_frequency_b;
+ }
+
+ if(minor_frequency_a > minor_frequency_b) {
+ // a is more evenly split, so put it first
+ return -1;
+ } else if(minor_frequency_a < minor_frequency_b) {
+ // b is more evenly split, so put it first
+ return 1;
+ }
+
+ } else if (!layers[a].selection && !isNaN(layers[a].positives) &&
+ layers[a].n > 0) {
+
+ // a is a binary layer we can nicely sort by minor value frequency, but
+ // b isn't. Put a first so that we can avoid intransitive sort cycles.
+
+ // Example: X and Z are binary layers, Y is a non-binary layer, Y comes
+ // after X and before Z by name ordering, but Z comes before X by minor
+ // frequency ordering. This sort is impossible.
+
+ // The solution is to put both X and Z in front of Y, because they're
+ // more interesting.
+
+ return -1;
+
+ } else if (!layers[b].selection && !isNaN(layers[b].positives) &&
+ layers[b].n > 0) {
+
+ // b is a binary layer that we can evaluate based on minor value
+ // frequency, but a isn't. Put b first.
+
+ return 1;
+
+ }
+
+ // We couldn't find a difference in selection status, p-value, or clumpiness
+ // score, or the binary layer minor value frequency, or whether eqach layer
+ // *had* a binary layer minor value frequency, so use lexicographic ordering
+ // on the name.
+ return a.localeCompare(b);
+
+}
+
+function sort_layers(layer_array) {
+ // Given an array of layer names, sort the array in place as we want layers
+ // to appear to the user.
+ // We should sort by p value, with NaNs at the end. But selections should be
+ // first.
+
+ layer_array.sort(layer_sort_order);
+}
+
+function fill_layer_metadata(container, layer_name) {
+ // Empty the given jQuery container element, and fill it with layer metadata
+ // for the layer with the given name.
+
+ // Empty the container.
+ container.html("");
+
+ for(attribute in layers[layer_name]) {
+ // Go through everything we know about this layer
+ if(attribute == "data" || attribute == "url" ||
+ attribute == "magnitude" || attribute == "selection") {
+
+ // Skip built-in things
+ // TODO: Ought to maybe have all metadata in its own object?
+ continue;
+ }
+
+ // This holds the metadata value we're displaying
+ var value = layers[layer_name][attribute];
+
+ if(typeof value == "number" && isNaN(value)) {
+ // If it's a numerical NaN (but not a string), just leave it out.
+ continue;
+ }
+
+ // If we're still here, this is real metadata.
+ // Format it for display.
+ var value_formatted;
+ if(typeof value == "number") {
+ if(value % 1 == 0) {
+ // It's an int!
+ // Display the default way
+ value_formatted = value;
+ } else {
+ // It's a float!
+ // Format the number for easy viewing
+ value_formatted = value.toExponential(2);
+ }
+ } else {
+ // Just put the thing in as a string
+ value_formatted = value;
+ }
+
+ // Make a spot for it in the container and put it in
+ var metadata = $("
").addClass("layer-metadata");
+ metadata.text(attribute + " = " + value_formatted);
+
+ container.append(metadata);
+
+ }
+}
+
+function make_browse_ui(layer_name) {
+ // Returns a jQuery element to represent the layer with the given name in
+ // the browse panel.
+
+ // This holds a jQuery element that's the root of the structure we're
+ // building.
+ var root = $("").addClass("layer-entry");
+ root.data("layer-name", layer_name);
+
+ // Put in the layer name in a div that makes it wrap.
+ root.append($("").addClass("layer-name").text(layer_name));
+
+ // Put in a layer metadata container div
+ var metadata_container = $("").addClass("layer-metadata-container");
+
+ fill_layer_metadata(metadata_container, layer_name);
+
+ root.append(metadata_container);
+
+ return root;
+}
+
+function update_browse_ui() {
+ // Make the layer browse UI reflect the current list of layers in sorted
+ // order.
+
+ // Re-sort the sorted list that we maintain
+ sort_layers(layer_names_sorted);
+
+ // Close the select if it was open, forcing the data to refresh when it
+ // opens again.
+ $("#search").select2("close");
+}
+
+function get_slider_range(layer_name) {
+ // Given the name of a layer, get the slider range from its shortlist UI
+ // entry.
+ // Assumes the layer has a shortlist UI entry.
+ return shortlist_ui[layer_name].find(".range-slider").slider("values");
+}
+
+function reset_slider(layer_name, shortlist_entry) {
+ // Given a layer name and a shortlist UI entry jQuery element, reset the
+ // slider in the entry to its default values, after downloading the layer.
+ // The default value may be invisible because we decided the layer should be
+ // a colormap.
+
+ // We need to set its boundaries to the min and max of the data set
+ with_layer(layer_name, function(layer) {
+ if(have_colormap(layer_name)) {
+ // This is a colormap, so don't use the range slider at all.
+ // We couldn't know this before because the colormap may need to be
+ // auto-detected upon download.
+ shortlist_entry.find(".range").hide();
+ return;
+ } else {
+ // We need the range slider
+ shortlist_entry.find(".range").show();
+
+ // TODO: actually find max and min
+ // For now just use + and - magnitude
+ // This has the advantage of letting us have 0=black by default
+ var magnitude = layer.magnitude;
+
+ // This holds the limit to use, which should be 1 if the magnitude
+ // is <1. This is sort of heuristic, but it's a good guess that
+ // nobody wants to look at a layer with values -0.2 to 0.7 on a
+ // scale of -10 to 10, say, but they might want it on -1 to 1.
+ var range = Math.max(magnitude, 1.0)
+
+ // Set the min and max.
+ shortlist_entry.find(".range-slider").slider("option", "min",
+ -range);
+ shortlist_entry.find(".range-slider").slider("option", "max",
+ range);
+
+ // Set slider to autoscale for the magnitude.
+ shortlist_entry.find(".range-slider").slider("values", [-magnitude,
+ magnitude]);
+
+ print("Scaled to magnitude " + magnitude);
+
+ // Redraw the view in case this changed anything
+ refresh();
+ }
+
+ });
+}
+
+function get_current_layers() {
+ // Returns an array of the string names of the layers that are currently
+ // supposed to be displayed, according to the shortlist UI.
+ // Not responsible for enforcing maximum selected layers limit.
+
+ // This holds a list of the string names of the currently selected layers,
+ // in order.
+ var current_layers = [];
+
+ $("#shortlist").children().each(function(index, element) {
+ // This holds the checkbox that determines if we use this layer
+ var checkbox = $(element).find(".layer-on");
+ if(checkbox.is(":checked")) {
+ // Put the layer in if its checkbox is checked.
+ current_layers.push($(element).data("layer"));
+ }
+ });
+
+ // Return things in reverse order relative to the UI.
+ // Thus, layer-added layers will be "secondary", and e.g. selecting
+ // something with only tissue up behaves as you might expect, highlighting
+ // those things.
+ current_layers.reverse();
+
+ return current_layers;
+}
+
+function get_current_filters() {
+ // Returns an array of filter objects, according to the shortlist UI.
+ // Filter objects have a layer name and a boolean-valued filter function
+ // that returns true or false, given a value from that layer.
+ var current_filters = [];
+
+ $("#shortlist").children().each(function(index, element) {
+ // Go through all the shortlist entries.
+ // This function is also the scope used for filtering function config
+ // variables.
+
+ // This holds the checkbox that determines if we use this layer
+ var checkbox = $(element).find(".filter-on");
+ if(checkbox.is(":checked")) {
+ // Put the layer in if its checkbox is checked.
+
+ // Get the layer name
+ var layer_name = $(element).data("layer");
+
+ // This will hold our filter function. Start with a no-op filter.
+ var filter_function = function(value) {
+ return true;
+ }
+
+ // Get the filter parameters
+ // This holds the input that specifies a filter threshold
+ var filter_threshold = $(element).find(".filter-threshold");
+ // And this the element that specifies a filter match value for
+ // discrete layers
+ var filter_value = $(element).find(".filter-value");
+
+ // We want to figure out which of these to use without going and
+ // downloading the layer.
+ // So, we check to see which was left visible by the filter config
+ // setup code.
+ if(filter_threshold.is(":visible")) {
+ // Use a threshold. This holds the threshold.
+ var threshold = parseInt(filter_threshold.val());
+
+ filter_function = function(value) {
+ return value > threshold;
+ }
+ }
+
+ if(filter_value.is(":visible")) {
+ // Use a discrete value match instead. This hodls the value we
+ // want to match.
+ var desired = filter_value.val();
+
+ filter_function = function(value) {
+ return value == desired;
+ }
+ }
+
+ // Add a filter on this layer, with the function we've prepared.
+ current_filters.push({
+ layer_name: layer_name,
+ filter_function: filter_function
+ });
+ }
+ });
+
+ return current_filters;
+}
+
+function with_filtered_signatures(filters, callback) {
+ // Takes an array of filters, as produced by get_current_filters. Signatures
+ // pass a filter if the filter's layer has a value >0 for that signature.
+ // Computes an array of all signatures passing all filters, and passes that
+ // to the given callback.
+
+ // TODO: Re-organize this to do filters one at a time, recursively, like a
+ // reasonable second-order filter.
+
+ // Prepare a list of all the layers
+ var layer_names = [];
+
+ for(var i = 0; i < filters.length; i++) {
+ layer_names.push(filters[i].layer_name);
+ }
+
+ with_layers(layer_names, function(filter_layers) {
+ // filter_layers is guaranteed to be in the same order as filters.
+
+ // This is an array of signatures that pass all the filters.
+ var passing_signatures = [];
+
+ for(var signature in polygons) {
+ // For each signature
+
+ // This holds whether we pass all the filters
+ var pass = true;
+
+ for(var i = 0; i < filter_layers.length; i++) {
+ // For each filtering layer
+ if(!filters[i].filter_function(
+ filter_layers[i].data[signature])) {
+
+ // If the signature fails the filter function for the layer,
+ // skip the signature.
+ pass = false;
+ break;
+ }
+ }
+
+ if(pass) {
+ // Record that the signature passes all filters
+ passing_signatures.push(signature);
+ }
+ }
+
+ // Now we have our list of all passing signatures, so hand it off to the
+ // callback.
+ callback(passing_signatures);
+ });
+}
+
+function select_list(to_select) {
+ // Given an array of signature names, add a new selection layer containing
+ // just those hexes. Only looks at hexes that are not filtered out by the
+ // currently selected filters.
+
+ // Make the requested signature list into an object for quick membership
+ // checking. This holds true if a signature was requested, undefined
+ // otherwise.
+ var wanted = {};
+
+ for(var i = 0; i < to_select.length; i++) {
+ wanted[to_select[i]] = true;
+ }
+
+ // This is the data object for the layer: from signature names to 1/0
+ var data = {};
+
+ // How many signatures will we have any mention of in this layer
+ var signatures_available = 0;
+
+ // Start it out with 0 for each signature. Otherwise we wil have missing
+ // data for signatures not passing the filters.
+ for(var signature in polygons) {
+ data[signature] = 0;
+ signatures_available += 1;
+ }
+
+ // This holds the filters we're going to use to restrict our selection
+ var filters = get_current_filters();
+
+ // Go get the list of signatures passing the filters and come back.
+ with_filtered_signatures(filters, function(signatures) {
+ // How many signatures get selected?
+ var signatures_selected = 0;
+
+ for(var i = 0; i < signatures.length; i++) {
+ if(wanted[signatures[i]]) {
+ // This signature is both allowed by the filters and requested.
+ data[signatures[i]] = 1;
+ signatures_selected++;
+ }
+ }
+
+ // Make up a name for the layer
+ var layer_name = "Selection " + selection_next_id;
+ selection_next_id++;
+
+ // Add the layer. Say it is a selection
+ add_layer_data(layer_name, data, {
+ selection: true,
+ selected: signatures_selected, // Display how many hexes are in
+ n: signatures_available // And how many have a value at all
+ });
+
+ // Update the browse UI with the new layer.
+ update_browse_ui();
+
+ // Immediately shortlist it
+ shortlist.push(layer_name);
+ update_shortlist_ui();
+ });
+
+}
+
+function select_rectangle(start, end) {
+ // Given two Google Maps LatLng objects (denoting arbitrary rectangle
+ // corners), add a new selection layer containing all the hexagons
+ // completely within that rectangle.
+ // Only looks at hexes that are not filtered out by the currently selected
+ // filters.
+
+ // Sort out the corners to get the rectangle limits in each dimension
+ var min_lat = Math.min(start.lat(), end.lat());
+ var max_lat = Math.max(start.lat(), end.lat());
+ var min_lng = Math.min(start.lng(), end.lng());
+ var max_lng = Math.max(start.lng(), end.lng());
+
+ // This holds an array of all signature names in our selection box.
+ var in_box = [];
+
+ // Start it out with 0 for each signature. Otherwise we wil have missing
+ // data for signatures not passing the filters.
+ for(var signature in polygons) {
+ // Get the path for its hex
+ var path = polygons[signature].getPath();
+
+ // This holds if any points of the path are outside the selection
+ // box
+ var any_outside = false;
+
+ path.forEach(function(point, index) {
+ // Check all the points. Runs synchronously.
+
+ if(point.lat() < min_lat || point.lat() > max_lat ||
+ point.lng() < min_lng || point.lng() > max_lng) {
+
+ // This point is outside the rectangle
+ any_outside = true;
+
+ }
+ });
+
+ // Select the hex if all its corners are inside the selection
+ // rectangle.
+ if(!any_outside) {
+ in_box.push(signature);
+ }
+ }
+
+ // Now we have an array of the signatures that ought to be in the selection
+ // (if they pass filters). Hand it off to select_list.
+
+ select_list(in_box);
+
+}
+
+function recalculate_statistics(passed_filters) {
+ // Interrogate the UI to determine signatures that are "in" and "out", and
+ // run an appropriate statisical test for each layer between the "in" and
+ // "out" signatures, and update all the "p_value" fields for all the layers
+ // with the p values. Takes in a list of signatures that passed the filters,
+ // and ignores any signatures not on that list.
+
+ // Build an efficient index of passing signatures
+ var passed = {};
+ for(var i = 0; i < passed_filters.length; i++) {
+ passed[passed_filters[i]] = true;
+ }
+
+ // Figure out what the in-list should be (statistics group A)
+ var layer_a_name = $(".statistics-a:checked").data("layer-name");
+ var layer_b_name = $(".statistics-b:checked").data("layer-name");
+
+ print("Running statistics between " + layer_a_name + " and " +
+ layer_b_name);
+
+ if(!layer_a_name) {
+ complain("Can't run statistics without an \"A\" group.");
+
+ // Get rid of the throbber
+ // TODO: Move this UI code out of the backend code.
+ $(".recalculate-throbber").hide();
+ $("#recalculate-statistics").show();
+
+ return;
+ }
+
+ // We know the layers have data since they're selections, so we can just go
+ // look at them.
+
+ // This holds the "in" list: hexes from the "A" group.
+ var in_list = [];
+
+ for(var signature in layers[layer_a_name].data) {
+ if(passed[signature] && layers[layer_a_name].data[signature]) {
+ // Add all the signatures in the "A" layer to the in list.
+ in_list.push(signature);
+ }
+ }
+
+ if(in_list.length == 0) {
+ complain("Can't run statistics with an empty \"A\" group.");
+
+ // Get rid of the throbber
+ // TODO: Move this UI code out of the backend code.
+ $(".recalculate-throbber").hide();
+ $("#recalculate-statistics").show();
+
+ return;
+ }
+
+ // This holds the "out" list: hexes in the "B" group, or, if that's not
+ // defined, all hexes. It's a little odd to run A vs. a set that includes
+ // some members of A, but Prof. Stuart wants that and it's not too insane
+ // for a Binomial test (which is the only currently implemented test
+ // anyway).
+ var out_list = [];
+
+ if(layer_b_name) {
+ // We have a layer B, so take everything that's on in it.
+ for(var signature in layers[layer_b_name].data) {
+ if(passed[signature] && layers[layer_b_name].data[signature]) {
+ // Add all the signatures in the "B" layer to the out list.
+ out_list.push(signature);
+ }
+ }
+ } else {
+ // The out list is all hexes
+ for(var signature in polygons) {
+ if(passed[signature]) {
+ // Put it on the out list.
+ out_list.push(signature);
+ }
+ }
+ }
+
+ // So now we have our in_list and our out_list
+
+ for(var layer_name in layers) {
+ // Do the stats on each layer between those lists. This only processes
+ // layers that don't have URLs. Layers with URLs are assumed to be part
+ // of the available matrices.
+ recalculate_statistics_for_layer(layer_name, in_list, out_list,
+ passed_filters);
+ }
+
+ // Now do all the layers with URLs. They are in the available score
+ // matrices.
+ for(var i = 0; i < available_matrices.length; i++) {
+ recalculate_statistics_for_matrix(available_matrices[i], in_list,
+ out_list, passed_filters);
+ }
+
+ print("Statistics jobs launched.");
+
+}
+
+function recalculate_statistics_for_layer(layer_name, in_list, out_list, all) {
+ // Re-calculate the stats for the layer with the given name, between the
+ // given in and out arrays of signatures. Store the re-calculated statistics
+ // in the layer. all is a list of "all" signatures, from which we can
+ // calculate pseudocounts.
+
+ // All we do is send the layer data or URL (whichever is more convenient) to
+ // the workers. They independently identify the data type and run the
+ // appropriate test, returning a p value or NaN by callback.
+
+ // This holds a callback for setting the layer's p_value to the result of
+ // the statistics.
+ var callback = function(result) {
+ layers[layer_name].p_value = result;
+ if(jobs_running == 0) {
+ // All statistics are done!
+ // TODO: Unify this code with similar callback below.
+ // Re-sort everything and draw all the new p values.
+ update_browse_ui();
+ update_shortlist_ui();
+
+ // Get rid of the throbber
+ $(".recalculate-throbber").hide();
+ $("#recalculate-statistics").show();
+ }
+ };
+
+ if(layers[layer_name].data != undefined) {
+ // Already have this downloaded. A local copy to the web worker is
+ // simplest, and a URL may not exist anyway.
+
+ rpc_call("statistics_for_layer", [layers[layer_name].data, in_list,
+ out_list, all], callback);
+ } else if(layers[layer_name].url != undefined) {
+ // We have a URL, so the layer must be in a matrix, too.
+ // Skip it here.
+ } else {
+ // Layer has no data and no way to get data. Should never happen.
+ complain("Layer " + layer_name + " has no data and no url.");
+ }
+}
+
+function recalculate_statistics_for_matrix(matrix_url, in_list, out_list, all) {
+ // Given the URL of one of the visualizer generator's input score matrices,
+ // download the matrix, calculate statistics for each layer in the matrix
+ // between the given in and out lists, and update the layer p values. all is
+ // a list of "all" signatures, from which we can calculate pseudocounts.
+
+ rpc_call("statistics_for_matrix", [matrix_url, in_list, out_list, all],
+ function(result) {
+
+ // The return value is p values by layer name
+ for(var layer_name in result) {
+ // Copy over p values
+ layers[layer_name].p_value = result[layer_name];
+ }
+
+ if(jobs_running == 0) {
+ // All statistics are done!
+ // TODO: Unify this code with similar callback above.
+ // Re-sort everything and draw all the new p values.
+ update_browse_ui();
+ update_shortlist_ui();
+
+ // Get rid of the throbber
+ $(".recalculate-throbber").hide();
+ $("#recalculate-statistics").show();
+ }
+ });
+
+}
+
+function rpc_initialize() {
+ // Set up the RPC system. Must be called before rpc_call is used.
+
+ for(var i = 0; i < NUM_RPC_WORKERS; i++) {
+ // Start the statistics RPC (remote procedure call) Web Worker
+ var worker = new Worker("statistics.js");
+
+ // Send all its messages to our reply processor
+ worker.onmessage = rpc_reply;
+
+ // Add it to the list of workers
+ rpc_workers.push(worker);
+ }
+}
+
+function rpc_call(function_name, function_args, callback) {
+ // Given a function name and an array of arguments, send a message to a Web
+ // Worker thread to ask it to run the given job. When it responds with the
+ // return value, pass it to the given callback.
+
+ // Allocate a new call id
+ var call_id = rpc_next_id;
+ rpc_next_id++;
+
+ // Store the callback
+ rpc_callbacks[call_id] = callback;
+
+ // Launch the call. Pass the function name, function args, and id to send
+ // back with the return value.
+ rpc_workers[next_free_worker].postMessage({
+ name: function_name,
+ args: function_args,
+ id: call_id
+ });
+
+ // Next time, use the next worker on the list, wrapping if we run out.
+ // This ensures no one worker gets all the work.
+ next_free_worker = (next_free_worker + 1) % rpc_workers.length;
+
+ // Update the UI with the number of jobs in flight. Decrement jobs_running
+ // so the callback knows if everything is done or not.
+ jobs_running++;
+ $("#jobs-running").text(jobs_running);
+
+ // And the number of jobs total
+ $("#jobs-ever").text(rpc_next_id);
+}
+
+function rpc_reply(message) {
+ // Handle a Web Worker message, which may be an RPC response or a log entry.
+
+ if(message.data.log != undefined) {
+ // This is really a log entry
+ print(message.data.log);
+ return;
+ }
+
+ // This is really a job completion message (success or error).
+
+ // Update the UI with the number of jobs in flight.
+ jobs_running--;
+ $("#jobs-running").text(jobs_running);
+
+ if(message.data.error) {
+ // The RPC call generated an error.
+ // Inform the page.
+ print("RPC error: " + message.data.error);
+
+ // Get rid of the callback
+ delete rpc_callbacks[message.data.id];
+
+ return;
+ }
+
+ // Pass the return value to the registered callback.
+ rpc_callbacks[message.data.id](message.data.return_value);
+
+ // Get rid of the callback
+ delete rpc_callbacks[message.data.id];
+}
+
+function initialize_view() {
+ // Initialize the global Google Map.
+
+ // Configure a Google map
+ var mapOptions = {
+ // Look at the center of the map
+ center: get_LatLng(128, 128),
+ // Zoom all the way out
+ zoom: 0,
+ mapTypeId: "blank",
+ // Don't show a map type picker.
+ mapTypeControlOptions: {
+ mapTypeIds: []
+ },
+ // Or a street view man that lets you walk around various Earth places.
+ streetViewControl: false
+ };
+
+ // Create the actual map
+ googlemap = new google.maps.Map(document.getElementById("visualization"),
+ mapOptions);
+
+ // Attach the blank map type to the map
+ googlemap.mapTypes.set("blank", new BlankMapType());
+
+ // Make the global info window
+ info_window = new google.maps.InfoWindow({
+ content: "No Signature Selected",
+ position: get_LatLng(0, 0)
+ });
+
+ // Add an event to close the info window when the user clicks outside of any
+ // hexagon
+ google.maps.event.addListener(googlemap, "click", function(event) {
+ info_window.close();
+
+ // Also make sure that the selected signature is no longer selected,
+ // so we don't pop the info_window up again.
+ selected_signature = undefined;
+
+ // Also un-focus the search box
+ $("#search").blur();
+ });
+
+
+ // And an event to clear the selected hex when the info_window closes.
+ google.maps.event.addListener(info_window, "closeclick", function(event) {
+ selected_signature = undefined;
+ });
+
+ // We also have an event listener that checks when the zoom level changes,
+ // and turns off hex borders if we zoom out far enough, and turns them on
+ // again if we come back.
+ google.maps.event.addListener(googlemap, "zoom_changed", function(event) {
+ // Get the current zoom level (low is out)
+ var zoom = googlemap.getZoom();
+
+ // API docs say: pixelCoordinate = worldCoordinate * 2 ^ zoomLevel
+ // So this holds the number of pixels that the global length hex_size
+ // corresponds to at this zoom level.
+ var hex_size_pixels = hex_size * Math.pow(2, zoom);
+
+ if(hex_size_pixels < MIN_BORDER_SIZE) {
+ // We're too small for borders
+ for(var signature in polygons) {
+ set_hexagon_stroke_weight(polygons[signature], 0);
+ }
+ } else {
+ // We can fit borders on the hexes
+ for(var signature in polygons) {
+ set_hexagon_stroke_weight(polygons[signature],
+ HEX_STROKE_WEIGHT);
+ }
+ }
+
+ });
+
+ // Subscribe all the tool listeners to the map
+ subscribe_tool_listeners(googlemap);
+
+}
+
+function add_tool(tool_name, tool_menu_option, callback) {
+ // Given a programmatic unique name for a tool, some text for the tool's
+ // button, and a callback for when the user clicks that button, add a tool
+ // to the tool menu.
+
+ // This hodls a button to activate the tool.
+ var tool_button = $("").attr("href", "#").addClass("stacker");
+ tool_button.text(tool_menu_option);
+ tool_button.click(function() {
+ // New tool. Remove all current tool listeners
+ clear_tool_listeners();
+
+ // Say that the select tool is selected
+ selected_tool = tool_name;
+ callback();
+
+ // End of tool workflow must set current_tool to undefined.
+ });
+
+ $("#toolbar").append(tool_button);
+}
+
+function add_tool_listener(name, handler, cleanup) {
+ // Add a global event listener over the Google map and everything on it.
+ // name specifies the event to listen to, and handler is the function to be
+ // set up as an event handler. It should take a single argument: the Google
+ // Maps event. A handle is returned that can be used to remove the event
+ // listen with remove_tool_listener.
+ // Only events in the TOOL_EVENTS array are allowed to be passed for name.
+ // TODO: Bundle this event thing into its own object.
+ // If "cleanup" is specified, it must be a 0-argument function to call when
+ // this listener is removed.
+
+ // Get a handle
+ var handle = tool_listener_next_id;
+ tool_listener_next_id++;
+
+ // Add the listener for the given event under that handle.
+ // TODO: do we also need to index this for O(1) event handling?
+ tool_listeners[handle] = {
+ handler: handler,
+ event: name,
+ cleanup: cleanup
+ };
+ return handle;
+}
+
+function remove_tool_listener(handle) {
+ // Given a handle returned by add_tool_listener, remove the listener so it
+ // will no longer fire on its event. May be called only once on a given
+ // handle. Runs any cleanup code associated with the handle being removed.
+
+ if(tool_listeners[handle].cleanup) {
+ // Run cleanup code if applicable
+ tool_listeners[handle].cleanup();
+ }
+
+ // Remove the property from the object
+ delete tool_listeners[handle];
+}
+
+function clear_tool_listeners() {
+ // We're starting to use another tool. Remove all current tool listeners.
+ // Run any associated cleanup code for each listener.
+
+ for(var handle in tool_listeners) {
+ remove_tool_listener(handle);
+ }
+}
+
+function subscribe_tool_listeners(maps_object) {
+ // Put the given Google Maps object into the tool events system, so that
+ // events on it will fire global tool events. This can happen before or
+ // after the tool events themselves are enabled.
+
+ for(var i = 0; i < TOOL_EVENTS.length; i++) {
+ // For each event name we care about,
+ // use an inline function to generate an event name specific handler,
+ // and attach that to the Maps object.
+ google.maps.event.addListener(maps_object, TOOL_EVENTS[i],
+ function(event_name) {
+ return function(event) {
+ // We are handling an event_name event
+
+ for(var handle in tool_listeners) {
+ if(tool_listeners[handle].event == event_name) {
+ // The handler wants this event
+ // Fire it with the Google Maps event args
+ tool_listeners[handle].handler(event);
+ }
+ }
+ };
+ }(TOOL_EVENTS[i]));
+ }
+
+}
+
+function have_colormap(colormap_name) {
+ // Returns true if the given string is the name of a colormap, or false if
+ // it is only a layer.
+
+ return !(colormaps[colormap_name] == undefined);
+}
+
+function get_range_position(score, low, high) {
+ // Given a score float, and the lower and upper bounds of an interval (which
+ // may be equal, but not backwards), return a number in the range -1 to 1
+ // that expresses the position of the score in the [low, high] interval.
+ // Positions out of bounds are clamped to -1 or 1 as appropriate.
+
+ // This holds the length of the input interval
+ var interval_length = high - low;
+
+ if(interval_length > 0) {
+ // First rescale 0 to 1
+ score = (score - low) / interval_length
+
+ // Clamp
+ score = Math.min(Math.max(score, 0), 1);
+
+ // Now re-scale to -1 to 1
+ score = 2 * score - 1;
+ } else {
+ // The interval is just a point
+ // Just use 1 if we're above the point, and 0 if below.
+ score = (score > low)? 1 : -1
+ }
+
+ return score;
+}
+
+function refresh() {
+ // Schedule the view to be redrawn after the current event finishes.
+
+ // Get rid of the previous redraw request, if there was one. We only want
+ // one.
+ window.clearTimeout(redraw_handle);
+
+ // Make a new one to happen as soon as this event finishes
+ redraw_handle = window.setTimeout(redraw_view, 0);
+}
+
+function redraw_view() {
+ // Make the view display the correct hexagons in the colors of the current
+ // layer(s), as read from the values of the layer pickers in the global
+ // layer pickers array.
+ // All pickers must have selected layers that are in the object of
+ // layers.
+ // Instead of calling this, you probably want to call refresh().
+
+ // This holds a list of the string names of the currently selected layers,
+ // in order.
+ var current_layers = get_current_layers();
+
+ // This holds arrays of the lower and upper limit we want to use for
+ // each layer, by layer number. The lower limit corresponds to u or
+ // v = -1, and the upper to u or v = 1. The entries we make for
+ // colormaps are ignored.
+ // Don't do this inside the callback since the UI may have changed by then.
+ var layer_limits = []
+ for(var i = 0; i < current_layers.length; i++) {
+ layer_limits.push(get_slider_range(current_layers[i]));
+ }
+
+ // This holds all the current filters
+ var filters = get_current_filters();
+
+ // Obtain the layer objects (mapping from signatures/hex labels to colors)
+ with_layers(current_layers, function(retrieved_layers) {
+ print("Redrawing view with " + retrieved_layers.length + " layers.");
+
+ // Turn all the hexes the filtered-out color, pre-emptively
+ for(var signature in polygons) {
+ set_hexagon_color(polygons[signature], "black");
+ }
+
+ // Go get the list of filter-passing hexes.
+ with_filtered_signatures(filters, function(signatures) {
+ for(var i = 0; i < signatures.length; i++) {
+ // For each hex passign the filter
+ // This hodls its signature label
+ var label = signatures[i];
+
+ // This holds the color we are calculating for this hexagon.
+ // Start with the missing data color.
+ var computed_color = "grey";
+
+ if(retrieved_layers.length >= 1) {
+ // Two layers. We find a point in u, v cartesian space, map
+ // it to polar, and use that to compute an HSV color.
+ // However, we map value to the radius instead of
+ // saturation.
+
+ // Get the heat along u and v axes. This puts us in a square
+ // of side length 2. Fun fact: undefined / number = NaN, but
+ // !(NaN == NaN)
+ var u = retrieved_layers[0].data[label];
+
+ if(!have_colormap(current_layers[0])) {
+ // Take into account the slider values and re-scale the
+ // layer value to express its position between them.
+ u = get_range_position(u, layer_limits[0][0],
+ layer_limits[0][1]);
+ }
+
+ if(retrieved_layers.length >= 2) {
+ // There's a second layer, so use the v axis.
+ var v = retrieved_layers[1].data[label];
+
+ if(!have_colormap(current_layers[1])) {
+ // Take into account the slider values and re-scale
+ // the layer value to express its position between
+ // them.
+ v = get_range_position(v, layer_limits[1][0],
+ layer_limits[1][1]);
+ }
+
+ } else {
+ // No second layer, so v axis is unused. Don't make it
+ // undefined (it's not missing data), but set it to 0.
+ var v = 0;
+ }
+
+ // Either of u or v may be undefined (or both) if the layer
+ // did not contain an entry for this signature. But that's
+ // OK. Compute the color that we should use to express this
+ // combination of layer values. It's OK to pass undefined
+ // names here for layers.
+ computed_color = get_color(current_layers[0], u,
+ current_layers[1], v);
+ }
+
+ // Set the color by the composed layers.
+ set_hexagon_color(polygons[label], computed_color);
+ }
+ });
+
+ // Draw the color key.
+ if(retrieved_layers.length == 0) {
+ // No color key to draw
+ $(".key").hide();
+ } else {
+ // We do actually want the color key
+ $(".key").show();
+
+ // This holds the canvas that the key gets drawn in
+ var canvas = $("#color-key")[0];
+
+ // This holds the 2d rendering context
+ var context = canvas.getContext("2d");
+
+ for(var i = 0; i < KEY_SIZE; i++) {
+ // We'll use i for the v coordinate (-1 to 1) (left to right)
+ var v = 0;
+ if(retrieved_layers.length >= 2) {
+ v = i / (KEY_SIZE / 2) - 1;
+
+ if(have_colormap(current_layers[1])) {
+ // This is a color map, so do bands instead.
+ v = Math.floor(i / KEY_SIZE *
+ (retrieved_layers[1].magnitude + 1));
+ }
+
+ }
+
+ for(var j = 0; j < KEY_SIZE; j++) {
+ // And j spacifies the u coordinate (bottom to top)
+ var u = 0;
+ if(retrieved_layers.length >= 1) {
+ u = 1 - j / (KEY_SIZE / 2);
+
+ if(have_colormap(current_layers[0])) {
+ // This is a color map, so do bands instead.
+ // Make sure to flip sign, and have a -1 for the
+ // 0-based indexing.
+ u = Math.floor((KEY_SIZE - j - 1) / KEY_SIZE *
+ (retrieved_layers[0].magnitude + 1));
+ }
+ }
+
+ // Set the pixel color to the right thing for this u, v
+ // It's OK to pass undefined names here for layers.
+ context.fillStyle = get_color(current_layers[0], u,
+ current_layers[1], v);
+
+ // Fill the pixel
+ context.fillRect(i, j, 1, 1);
+ }
+ }
+
+ }
+
+ if(have_colormap(current_layers[0])) {
+ // We have a layer with horizontal bands
+ // Add labels to the key if we have names to use.
+ // TODO: Vertical text for vertical bands?
+
+ // Get the colormap
+ var colormap = colormaps[current_layers[0]]
+
+ if(colormap.length > 0) {
+ // Actually have any categories (not auto-generated)
+ print("Drawing key text for " + colormap.length +
+ " categories.");
+
+ // How many pixels do we get per label, vertically
+ var pixels_per_label = KEY_SIZE / colormap.length;
+
+ // Configure for text drawing
+ context.font = pixels_per_label + "px Arial";
+ context.textBaseline = "top";
+
+ for(var i = 0; i < colormap.length; i++) {
+
+ // This holds the pixel position where our text goes
+ var y_position = KEY_SIZE - (i + 1) * pixels_per_label;
+
+ // Get the background color here as a 1x1 ImageData
+ var image = context.getImageData(0, y_position, 1, 1);
+
+ // Get the components r, g, b, a in an array
+ var components = image.data;
+
+ // Make a Color so we can operate on it
+ var background_color = Color({
+ r: components[0],
+ g: components[1],
+ b: components[2]
+ });
+
+ if(background_color.light()) {
+ // This color is light, so write in black.
+ context.fillStyle = "black";
+ } else {
+ // It must be dark, so write in white.
+ context.fillStyle = "white";
+ }
+
+ // Draw the name on the canvas
+ context.fillText(colormap[i].name, 0, y_position);
+ }
+ }
+ }
+
+ // We should also set up axis labels on the color key.
+ // We need to know about colormaps to do this
+
+ // Hide all the labels
+ $(".label").hide();
+
+ if(current_layers.length > 0) {
+ // Show the y axis label
+ $("#y-axis").text(current_layers[0]).show();
+
+ if(!have_colormap(current_layers[0])) {
+ // Show the low to high markers for continuous values
+ $("#low-both").show();
+ $("#high-y").show();
+ }
+ }
+
+ if(current_layers.length > 1) {
+ // Show the x axis label
+ $("#x-axis").text(current_layers[1]).show();
+
+ if(!have_colormap(current_layers[1])) {
+ // Show the low to high markers for continuous values
+ $("#low-both").show();
+ $("#high-x").show();
+ }
+ }
+
+
+ });
+
+ // Make sure to also redraw the info window, which may be open.
+ redraw_info_window();
+}
+
+function get_color(u_name, u, v_name, v) {
+ // Given u and v, which represent the heat in each of the two currently
+ // displayed layers, as well as u_name and v_name, which are the
+ // corresponding layer names, return the computed CSS color.
+ // Either u or v may be undefined (or both), in which case the no-data color
+ // is returned. If a layer name is undefined, that layer dimension is
+ // ignored.
+
+ if(have_colormap(v_name) && !have_colormap(u_name)) {
+ // We have a colormap as our second layer, and a layer as our first.
+ // Swap everything around so colormap is our first layer instead.
+ // Now we don't need to think about drawing a layer first with a
+ // colormap second.
+ // This is a temporary swapping variable.
+ var temp = v_name;
+ v_name = u_name;
+ u_name = temp;
+
+ temp = v;
+ v = u;
+ u = temp;
+ }
+
+ if(isNaN(u) || isNaN(v) || u == undefined || v == undefined) {
+ // At least one of our layers has no data for this hex.
+ return "grey";
+ }
+
+ if(have_colormap(u_name) && have_colormap(v_name) &&
+ !colormaps[u_name].hasOwnProperty(u) &&
+ !colormaps[v_name].hasOwnProperty(v) &&
+ layers[u_name].magnitude <= 1 && layers[v_name].magnitude <= 1) {
+
+ // Special case: two binary or unary auto-generated colormaps.
+ // Use dark grey/red/blue/purple color scheme
+
+ if(u == 1) {
+ if(v == 1) {
+ // Both are on
+ return "#FF00FF";
+ } else {
+ // Only the first is on
+ return "#FF0000";
+ }
+ } else {
+ if(v == 1) {
+ // Only the second is on
+ return "#0000FF";
+ } else {
+ // Neither is on
+ return "#545454";
+ }
+ }
+
+ }
+
+ if(have_colormap(u_name) && !colormaps[u_name].hasOwnProperty(u) &&
+ layers[u_name].magnitude <= 1 && v_name == undefined) {
+
+ // Special case: a single binary or unary auto-generated colormap.
+ // Use dark grey/red to make 1s stand out.
+
+ if(u == 1) {
+ // Red for on
+ return "#FF0000";
+ } else {
+ // Dark grey for off
+ return "#545454";
+ }
+ }
+
+
+ if(have_colormap(u_name)) {
+ // u is a colormap
+ if(colormaps[u_name].hasOwnProperty(u)) {
+ // And the colormap has an entry here. Use it as the base color.
+ var to_clone = colormaps[u_name][u].color;
+
+ var base_color = Color({
+ hue: to_clone.hue(),
+ saturation: to_clone.saturationv(),
+ value: to_clone.value()
+ });
+ } else {
+ // The colormap has no entry. Assume we're calculating all the
+ // entries. We do this by splitting the color circle evenly.
+
+ // This holds the number of colors, which is 1 more than the largest
+ // value used (since we start at color 0), which is the magnitude.
+ // It's OK to go ask for the magnitude of this layer since it must
+ // have already been downloaded.
+ var num_colors = layers[u_name].magnitude + 1;
+
+ // Calculate the hue for this number.
+ var hsv_hue = u / (num_colors + 1) * 360;
+
+ // The base color is a color at that hue, with max saturation and
+ // value
+ var base_color = Color({
+ hue: hsv_hue,
+ saturation: 100,
+ value: 100
+ })
+ }
+
+ // Now that the base color is set, consult v to see what shade to use.
+ if(v_name == undefined) {
+ // No v layer is actually in use. Use whatever is in the base
+ // color
+ // TODO: This code path is silly, clean it up.
+ var hsv_value = base_color.value();
+ } else if(have_colormap(v_name)) {
+ // Do discrete shades in v
+ // This holds the number of shades we need.
+ // It's OK to go ask for the magnitude of this layer since it must
+ // have already been downloaded.
+ var num_shades = layers[v_name].magnitude + 1;
+
+ // Calculate what shade we need from the nonnegative integer v
+ // We want 100 to be included (since that's full brightness), but we
+ // want to skip 0 (since no color can be seen at 0), so we add 1 to
+ // v.
+ var hsv_value = (v + 1) / num_shades * 100;
+ } else {
+ // Calculate what shade we need from v on -1 to 1
+ var hsv_value = 50 + v * 50;
+ }
+
+ // Set the color's value component.
+ base_color.value(hsv_value);
+
+ // Return the shaded color
+ return base_color.hexString();
+ }
+
+
+ // If we get here, we only have non-colormap layers.
+
+ // This is the polar angle (hue) in degrees, forced to be
+ // positive.
+ var hsv_hue = Math.atan2(v, u) * 180 / Math.PI;
+ if(hsv_hue < 0) {
+ hsv_hue += 360;
+ }
+
+ // Rotate it by 60 degrees, so that the first layer is
+ // yellow/blue
+ hsv_hue += 60;
+ if(hsv_hue > 360) {
+ hsv_hue -= 360;
+ }
+
+ // This is the polar radius (value). We inscribe our square
+ // of side length 2 in a circle of radius 1 by dividing by
+ // sqrt(2). So we get a value from 0 to 1
+ var hsv_value = (Math.sqrt(Math.pow(u, 2) +
+ Math.pow(v, 2)) / Math.sqrt(2));
+
+ // This is the HSV saturation component of the color on 0 to 1.
+ // Just fix to 1.
+ var hsv_saturation = 1.0;
+
+ // Now scale saturation and value to percent
+ hsv_saturation *= 100;
+ hsv_value *= 100;
+
+ // Now we have the color as HSV, but CSS doesn't support it.
+
+ // Make a Color object and get the RGB string
+ try {
+ return Color({
+ hue: hsv_hue,
+ saturation: hsv_saturation,
+ value: hsv_value,
+ }).hexString();
+ } catch(error) {
+ print("(" + u + "," + v + ") broke with color (" + hsv_hue +
+ "," + hsv_saturation + "," + hsv_value + ")");
+
+ // We'll return an error color
+ return "white";
+ }
+}
+
+// Define a flat projection
+// See https://developers.google.com/maps/documentation/javascript/maptypes#Projections
+function FlatProjection() {
+}
+
+
+FlatProjection.prototype.fromLatLngToPoint = function(latLng) {
+ // Given a LatLng from -90 to 90 and -180 to 180, transform to an x, y Point
+ // from 0 to 256 and 0 to 256
+ var point = new google.maps.Point((latLng.lng() + 180) * 256 / 360,
+ (latLng.lat() + 90) * 256 / 180);
+
+ return point;
+
+}
+
+
+FlatProjection.prototype.fromPointToLatLng = function(point, noWrap) {
+ // Given a an x, y Point from 0 to 256 and 0 to 256, transform to a LatLng from
+ // -90 to 90 and -180 to 180
+ var latLng = new google.maps.LatLng(point.y * 180 / 256 - 90,
+ point.x * 360 / 256 - 180, noWrap);
+
+ return latLng;
+}
+
+// Define a Google Maps MapType that's all blank
+// See https://developers.google.com/maps/documentation/javascript/examples/maptype-base
+function BlankMapType() {
+}
+
+BlankMapType.prototype.tileSize = new google.maps.Size(256,256);
+BlankMapType.prototype.maxZoom = 19;
+
+BlankMapType.prototype.getTile = function(coord, zoom, ownerDocument) {
+ // This is the element representing this tile in the map
+ // It should be an empty div
+ var div = ownerDocument.createElement("div");
+ div.style.width = this.tileSize.width + "px";
+ div.style.height = this.tileSize.height + "px";
+ div.style.backgroundColor = "#000000";
+
+ return div;
+}
+
+BlankMapType.prototype.name = "Blank";
+BlankMapType.prototype.alt = "Blank Map";
+
+BlankMapType.prototype.projection = new FlatProjection();
+
+
+
+function get_LatLng(x, y) {
+ // Given a point x, y in map space (0 to 256), get the corresponding LatLng
+ return FlatProjection.prototype.fromPointToLatLng(
+ new google.maps.Point(x, y));
+}
+
+$(function() {
+
+ // Set up the RPC system for background statistics
+ rpc_initialize();
+
+ // Set up the Google Map
+ initialize_view();
+
+ // Set up the layer search
+ $("#search").select2({
+ placeholder: "Add Attribute...",
+ query: function(query) {
+ // Given a select2 query object, call query.callback with an object
+ // with a "results" array.
+
+ // This is the array of result objects we will be sending back.
+ var results = [];
+
+ // Get where we should start in the layer list, from select2's
+ // infinite scrolling.
+ var start_position = 0;
+ if(query.context != undefined) {
+ start_position = query.context;
+ }
+
+ for(var i = start_position; i < layer_names_sorted.length; i++) {
+ // For each possible result
+ if(layer_names_sorted[i].toLowerCase().indexOf(
+ query.term.toLowerCase()) != -1) {
+
+ // Query search term is in this layer's name. Add a select2
+ // record to our results. Don't specify text: our custom
+ // formatter looks up by ID and makes UI elements
+ // dynamically.
+ results.push({
+ id: layer_names_sorted[i]
+ });
+
+ if(results.length >= SEARCH_PAGE_SIZE) {
+ // Page is full. Send it on.
+ break;
+ }
+
+ }
+ }
+
+ // Give the results back to select2 as the results parameter.
+ query.callback({
+ results: results,
+ // Say there's more if we broke out of the loop.
+ more: i < layer_names_sorted.length,
+ // If there are more results, start after where we left off.
+ context: i + 1
+ });
+ },
+ formatResult: function(result, container, query) {
+ // Given a select2 result record, the element that our results go
+ // in, and the query used to get the result, return a jQuery element
+ // that goes in the container to represent the result.
+
+ // Get the layer name, and make the browse UI for it.
+ return make_browse_ui(result.id);
+ },
+ // We want our dropdown to be big enough to browse.
+ dropdownCssClass: "results-dropdown"
+ });
+
+ // Handle result selection
+ $("#search").on("select2-selecting", function(event) {
+ // The select2 id of the thing clicked (the layer's name) is event.val
+ var layer_name = event.val;
+
+ // User chose this layer. Add it to the global shortlist.
+
+ // Only add to the shortlist if it isn't already there
+ // Was it already there?
+ var found = false;
+ for(var j = 0; j < shortlist.length; j++) {
+ if(shortlist[j] == layer_name) {
+ found = true;
+ break;
+ }
+ }
+
+ if(!found) {
+ // It's new. Add it to the shortlist
+ shortlist.push(layer_name);
+
+ // Update the UI to reflect this. This may redraw the view.
+ update_shortlist_ui();
+
+ }
+
+ // Don't actually change the selection.
+ // This keeps the dropdown open when we click.
+ event.preventDefault();
+ });
+
+ $("#recalculate-statistics").button().click(function() {
+ // Re-calculate the statistics between the currently filtered hexes and
+ // everything else.
+
+ // Put up the throbber instead of us.
+ $("#recalculate-statistics").hide();
+ $(".recalculate-throbber").show();
+
+ // This holds the currently enabled filters.
+ var filters = get_current_filters();
+
+ with_filtered_signatures(filters, function(signatures) {
+ // Find everything passing the filters and run the statistics.
+ recalculate_statistics(signatures);
+ });
+ });
+
+ // Download the signature assignments to hexagons and fill in the global
+ // hexagon assignment grid.
+ $.get("assignments.tab", function(tsv_data) {
+ // This is an array of rows, which are arrays of values:
+ // id, x, y
+ var parsed = $.tsv.parseRows(tsv_data);
+
+ // This holds the maximum observed x
+ var max_x = 0;
+ // And y
+ var max_y = 0;
+
+ // Fill in the global signature grid and ploygon grid arrays.
+ for(var i = 0; i < parsed.length; i++) {
+ // Get the label
+ var label = parsed[i][0];
+
+ if(label == "") {
+ // Blank line
+ continue;
+ }
+
+ // Get the x coord
+ var x = parseInt(parsed[i][1]);
+ // And the y coord
+ var y = parseInt(parsed[i][2]);
+
+
+ // Update maxes
+ max_x = Math.max(x, max_x);
+ max_y = Math.max(y, max_y);
+
+
+ // Make sure we have a row
+ if(signature_grid[y] == null) {
+ signature_grid[y] = [];
+ // Pre-emptively add a row to the polygon grid.
+ polygon_grid[y] = [];
+ }
+
+ // Store the label in the global signature grid.
+ signature_grid[y][x] = label;
+ }
+
+ // We need to fit this whole thing into a 256x256 grid.
+ // How big can we make each hexagon?
+ // TODO: Do the algrbra to make this exact. Right now we just make a
+ // grid that we know to be small enough.
+ // Divide the space into one column per column, and calculate
+ // side length from column width. Add an extra column for dangling
+ // corners.
+ var side_length_x = 256 / (max_x + 2) * (2.0 / 3.0);
+
+ print("Max hexagon side length horizontally is " + side_length_x);
+
+ // Divide the space into rows and calculate the side length
+ // from hex height. Remember to add an extra row for wggle.
+ var side_length_y = (256 / (max_y + 2)) / Math.sqrt(3);
+
+ print("Max hexagon side length vertically is " + side_length_y);
+
+ // How long is a hexagon side in world coords?
+ // Shrink it from the biggest we can have so that we don't wrap off the
+ // edges of the map.
+ var hexagon_side_length = Math.min(side_length_x, side_length_y) / 2.0;
+
+ // Store this in the global hex_size, so we can later calculate the hex
+ // size in pixels and make borders go away if we are too zoomed out.
+ hex_size = hexagon_side_length;
+
+ // How far in should we move the whole grid from the top left corner of
+ // the earth?
+ // Let's try leaving a 1/4 Earth gap at least, to stop wrapping in
+ // longitude that we can't turn off.
+ // Since we already shrunk the map to half max size, this would put it
+ // 1/4 of the 256 unit width and height away from the top left corner.
+ grid_offset = 256 / 4;
+
+ // Loop through again and draw the polygons, now that we know how big
+ // they have to be
+ for(var i = 0; i < parsed.length; i++) {
+ // TODO: don't re-parse this info
+ // Get the label
+ var label = parsed[i][0];
+
+ if(label == "") {
+ // Blank line
+ continue;
+ }
+
+ // Get the x coord
+ var x = parseInt(parsed[i][1]);
+ // And the y coord
+ var y = parseInt(parsed[i][2]);
+
+ // Make a hexagon on the Google map and store that.
+ var hexagon = make_hexagon(y, x, hexagon_side_length, grid_offset);
+ // Store by x, y in grid
+ polygon_grid[y][x] = hexagon;
+ // Store by label
+ polygons[label] = hexagon;
+
+ // Set the polygon's signature so we can look stuff up for it when
+ // it's clicked.
+ set_hexagon_signature(hexagon, label);
+
+ }
+
+ // Now that the ploygons exist, do the initial redraw to set all their
+ // colors corectly. In case someone has messed with the controls.
+ // TODO: can someone yet have messed with the controlls?
+ refresh();
+
+
+ }, "text");
+
+ // Download the DrL position data, and make it into a layer
+ $.get("drl.tab", function(tsv_data) {
+ // This is an array of rows, which are arrays of values:
+ // id, x, y
+ // Only this time X and Y are Cartesian coordinates.
+ var parsed = $.tsv.parseRows(tsv_data);
+
+ // Compute two layers: one for x position, and one for y position.
+ var layer_x = {};
+ var layer_y = {};
+
+ for(var i = 0; i < parsed.length; i++) {
+ // Pull out the parts of the TSV entry
+ var label = parsed[i][0];
+
+ if(label == "") {
+ // DrL ends its output with a blank line, which we skip
+ // here.
+ continue;
+ }
+
+ var x = parseFloat(parsed[i][1]);
+ // Invert the Y coordinate since we do that in the hex grid
+ var y = -parseFloat(parsed[i][2]);
+
+ // Add x and y to the appropriate layers
+ layer_x[label] = x;
+ layer_y[label] = y;
+ }
+
+ // Register the layers with no priorities. By default they are not
+ // selections.
+ add_layer_data("DrL X Position", layer_x);
+ add_layer_data("DrL Y Position", layer_y);
+
+ // Make sure the layer browser has the up-to-date layer list
+ update_browse_ui();
+
+ }, "text");
+
+ // Download the layer index
+ $.get("layers.tab", function(tsv_data) {
+ // Layer index is \t\t
+ var parsed = $.tsv.parseRows(tsv_data);
+
+ for(var i = 0; i < parsed.length; i++) {
+ // Pull out the parts of the TSV entry
+ // This is the name of the layer.
+ var layer_name = parsed[i][0];
+
+ if(layer_name == "") {
+ // Skip any blank lines
+ continue;
+ }
+
+ // This is the URL from which to download the TSV for the actual
+ // layer.
+ var layer_url = parsed[i][1];
+
+ // This is the layer's clumpiness score
+ var layer_clumpiness = parseFloat(parsed[i][2]);
+
+ // This is the number of hexes that the layer has any values for.
+ // We need to get it from the server so we don't have to download
+ // the layer to have it.
+ var layer_count = parseFloat(parsed[i][3]);
+
+ // This is the number of 1s in a binary layer, or NaN in other
+ // layers
+ var layer_positives = parseFloat(parsed[i][4]);
+
+ // Add this layer to our index of layers
+ add_layer_url(layer_name, layer_url, {
+ clumpiness: layer_clumpiness,
+ positives: layer_positives,
+ n: layer_count
+ });
+ }
+
+ // Now we have added layer downloaders for all the layers in the
+ // index. Update the UI
+ update_browse_ui();
+
+
+ }, "text");
+
+ // Download full score matrix index, which we later use for statistics. Note
+ // that stats won't work unless this finishes first. TODO: enforce this.
+ $.get("matrices.tab", function(tsv_data) {
+ // Matrix index is just
+ var parsed = $.tsv.parseRows(tsv_data);
+
+ for(var i = 0; i < parsed.length; i++) {
+ // Pull out the parts of the TSV entry
+ // This is the filename of the matrix.
+ var matrix_name = parsed[i][0];
+
+ if(matrix_name == "") {
+ // Not a real matrix
+ continue;
+ }
+
+ // Add it to the global list
+ available_matrices.push(matrix_name);
+ }
+ }, "text");
+
+ // Download color map information
+ $.get("colormaps.tab", function(tsv_data) {
+ // Colormap data is \t\t\t
+ // \t\t\t...
+ var parsed = $.tsv.parseRows(tsv_data);
+
+ for(var i = 0; i < parsed.length; i++) {
+ // Get the name of the layer
+ var layer_name = parsed[i][0];
+
+ // Skip blank lines
+ if(layer_name == "") {
+ continue;
+ }
+
+ // This holds all the categories (name and color) by integer index
+ var colormap = [];
+
+ print("Loading colormap for " + layer_name);
+
+ for(j = 1; j < parsed[i].length; j += 3) {
+ // Store each color assignment.
+ // Doesn't run if there aren't any assignments, leaving an empty
+ // colormap object that just forces automatic color selection.
+
+ // This holds the index of the category
+ var category_index = parseInt(parsed[i][j]);
+
+ // The colormap gets an object with the name and color that the
+ // index number refers to. Color is stored as a color object.
+ colormap[category_index] = {
+ name: parsed[i][j + 1],
+ color: Color(parsed[i][j + 2])
+ };
+
+ print( colormap[category_index].name + " -> " +
+ colormap[category_index].color.hexString());
+ }
+
+ // Store the finished color map in the global object
+ colormaps[layer_name] = colormap;
+
+
+ }
+
+ // We may need to redraw the view in response to having new color map
+ // info, if it came particularly late.
+ refresh();
+
+ }, "text");
+});
+
diff -r 000000000000 -r 95ff566506f4 hexagram/hexagram.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/hexagram.py Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,1065 @@
+#!/usr/bin/env python2.7
+"""
+hexagram.py: Given a matrix of similarities, produce a hexagram visualization.
+
+This script takes in the filename of a tab-separated value file containing a
+sparse similarity matrix (with string labels) and several matrices of
+layer/score data. It produces an HTML file (and several support files) that
+provide an interactive visualization of the items clustered on a hexagonal grid.
+
+This script depends on the DrL graph alyout package, binaries for which must be
+present in your PATH.
+
+Re-uses sample code and documentation from
+
+"""
+
+import argparse, sys, os, itertools, math, numpy, subprocess, shutil, tempfile
+import collections, scipy.stats, multiprocessing, traceback, numpy.ma
+import os.path
+import tsv
+
+def parse_args(args):
+ """
+ Takes in the command-line arguments list (args), and returns a nice argparse
+ result with fields for all the options.
+ Borrows heavily from the argparse documentation examples:
+
+ """
+
+ # The command line arguments start with the program name, which we don't
+ # want to treat as an argument for argparse. So we remove it.
+ args = args[1:]
+
+ # Construct the parser (which is stored in parser)
+ # Module docstring lives in __doc__
+ # See http://python-forum.com/pythonforum/viewtopic.php?f=3&t=36847
+ # And a formatter class so our examples in the docstring look good. Isn't it
+ # convenient how we already wrapped it to 80 characters?
+ # See http://docs.python.org/library/argparse.html#formatter-class
+ parser = argparse.ArgumentParser(description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+
+ # Now add all the options to it
+ # Options match the ctdHeatmap tool options as much as possible.
+ parser.add_argument("similarities", type=argparse.FileType("r"),
+ help="the TSV file with the similarities for signatures we're using")
+ parser.add_argument("--scores", type=str,
+ action="append", default=[],
+ help="a TSV to read scores for each signature from")
+ parser.add_argument("--colormaps", type=argparse.FileType("r"),
+ default=None,
+ help="a TSV defining coloring and value names for discrete scores")
+ parser.add_argument("--html", "-H", type=str,
+ default="index.html",
+ help="where to write HTML report")
+ parser.add_argument("--directory", "-d", type=str, default=".",
+ help="directory in which to create other output files")
+ parser.add_argument("--query", type=str, default=None,
+ help="Galaxy-escaped name of the query signature")
+ parser.add_argument("--window_size", type=int, default=20,
+ help="size of the window to use when looking for clusters")
+ parser.add_argument("--no-stats", dest="stats", action="store_false",
+ default=True,
+ help="disable cluster-finding statistics")
+
+ return parser.parse_args(args)
+
+def hexagon_center(x, y, scale=1.0):
+ """
+ Given a coordinate on a grid of hexagons (using wiggly rows in x), what is
+ the 2d Euclidian coordinate of its center?
+
+ x and y are integer column and row coordinates of the hexagon in the grid.
+
+ scale is a float specifying hexagon side length.
+
+ The origin in coordinate space is defined as the upper left corner of the
+ bounding box of the hexagon wityh indices x=0 and y=0.
+
+ Returns a tuple of floats.
+ """
+ # The grid looks like this:
+ #
+ # /-\ /-\ /-\ /-\
+ # /-\-/-\-/-\-/-\-/-\
+ # \-/-\-/-\-/-\-/-\-/
+ # /-\-/-\-/-\-/-\-/-\
+ # \-/-\-/-\-/-\-/-\-/
+ # /-\-/-\-/-\-/-\-/-\
+ # \-/ \-/ \-/ \-/ \-/
+ #
+ # Say a hexagon side has length 1
+ # It's 2 across corner to corner (x), and sqrt(3) across side to side (y)
+ # X coordinates are 1.5 per column
+ # Y coordinates (down from top) are sqrt(3) per row, -1/2 sqrt(3) if you're
+ # in an odd column.
+
+ center_y = math.sqrt(3) * y
+ if x % 2 == 1:
+ # Odd column: shift up
+ center_y -= 0.5 * math.sqrt(3)
+
+ return (1.5 * x * scale + scale, center_y * scale + math.sqrt(3.0) / 2.0 *
+ scale)
+
+def hexagon_pick(x, y, scale=1.0):
+ """
+ Given floats x and y specifying coordinates in the plane, determine which
+ hexagon grid cell that point is in.
+
+ scale is a float specifying hexagon side length.
+
+ See http://blog.ruslans.com/2011/02/hexagonal-grid-math.html
+ But we flip the direction of the wiggle. Odd rows are up (-y)
+ """
+
+ # How high is a hex?
+ hex_height = math.sqrt(3) * scale
+
+ # First we pick a rectangular tile, from the point of one side-traingle to
+ # the base of the other in width, and the whole hexagon height in height.
+
+ # How wide are these tiles? Corner to line-between-far-corners distance
+ tile_width = (3.0 / 2.0 * scale)
+
+ # Tile X index is floor(x / )
+ tile_x = int(math.floor(x / tile_width))
+
+ # We need this intermediate value for the Y index and for tile-internal
+ # picking
+ corrected_y = y + (tile_x % 2) * hex_height / 2.0
+
+ # Tile Y index is floor((y + (x index mod 2) * hex height/2) / hex height)
+ tile_y = int(math.floor(corrected_y / hex_height))
+
+ # Find coordinates within the tile
+ internal_x = x - tile_x * tile_width
+ internal_y = corrected_y - tile_y * hex_height
+
+ # Do tile-scale picking
+ # Are we in the one corner, the other corner, or the bulk of the tile?
+ if internal_x > scale * abs(0.5 - internal_y / hex_height):
+ # We're in the bulk of the tile
+ # This is the column (x) of the picked hexagon
+ hexagon_x = tile_x
+
+ # This is the row (y) of the picked hexagon
+ hexagon_y = tile_y
+ else:
+ # We're in a corner.
+ # In an even column, the lower left is part of the next row, and the
+ # upper left is part of the same row. In an odd column, the lower left
+ # is part of the same row, and the upper left is part of the previous
+ # row.
+ if internal_y > hex_height / 2.0:
+ # It's the lower left corner
+ # This is the offset in row (y) that being in this corner gives us
+ # The lower left corner is always 1 row below the upper left corner.
+ corner_y_offset = 1
+ else:
+ corner_y_offset = 0
+
+ # TODO: verify this for correctness. It seems to be right, but I want a
+ # unit test to be sure.
+ # This is the row (y) of the picked hexagon
+ hexagon_y = tile_y - tile_x % 2 + corner_y_offset
+
+ # This is the column (x) of the picked hexagon
+ hexagon_x = tile_x - 1
+
+ # Now we've picked the hexagon
+ return (hexagon_x, hexagon_y)
+
+def radial_search(center_x, center_y):
+ """
+ An iterator that yields coordinate tuples (x, y) in order of increasing
+ hex-grid distance from the specified center position.
+ """
+
+ # A hexagon has neighbors at the following relative coordinates:
+ # (-1, 0), (1, 0), (0, -1), (0, 1)
+ # and ((-1, 1) and (1, 1) if in an even column)
+ # or ((-1, -1) and (1, -1) if in an odd column)
+
+ # We're going to go outwards using breadth-first search, so we need a queue
+ # of hexes to visit and a set of already visited hexes.
+
+ # This holds a queue (really a deque) of hexes waiting to be visited.
+ # A list has O(n) pop/insert at left.
+ queue = collections.deque()
+ # This holds a set of the (x, y) coordinate tuples of already-seen hexes,
+ # so we don't enqueue them again.
+ seen = set()
+
+ # First place to visit is the center.
+ queue.append((center_x, center_y))
+
+ while len(queue) > 0:
+ # We should in theory never run out of items in the queue.
+ # Get the current x and y to visit.
+ x, y = queue.popleft()
+
+ # Yield the location we're visiting
+ yield (x, y)
+
+ # This holds a list of all relative neighbor positions as (x, y) tuples.
+ neighbor_offsets = [(-1, 0), (1, 0), (0, -1), (0, 1)]
+ if y % 2 == 0:
+ # An even-column hex also has these neighbors
+ neighbor_offsets += [(-1, 1), (1, 1)]
+ else:
+ # An odd-column hex also has these neighbors
+ neighbor_offsets += [(-1, -1), (1, -1)]
+
+ for x_offset, y_offset in neighbor_offsets:
+ # First calculate the absolute position of the neighbor in x
+ neighbor_x = x + x_offset
+ # And in y
+ neighbor_y = y + y_offset
+
+ if (neighbor_x, neighbor_y) not in seen:
+ # This is a hex that has never been in the queue. Add it.
+ queue.append((neighbor_x, neighbor_y))
+
+ # Record that it has ever been enqueued
+ seen.add((neighbor_x, neighbor_y))
+
+
+
+
+def assign_hexagon(hexagons, node_x, node_y, node, scale=1.0):
+ """
+ This function assigns the given node to a hexagon in hexagons. hexagons is a
+ defaultdict from tuples of hexagon (x, y) integer indices to assigned nodes,
+ or None if a hexagon is free. node_x and node_y are the x and y coordinates
+ of the node, adapted so that the seed node lands in the 0, 0 hexagon, and
+ re-scaled to reduce hexagon conflicts. node is the node to be assigned.
+ scale, if specified, is the hexagon side length in node space units.
+
+ This function assigns nodes to their closest hexagon, reprobing outwards if
+ already occupied.
+
+ When the function completes, node is stored in hexagons under some (x, y)
+ tuple.
+
+ Returns the distance this hexagon is from its ideal location.
+ """
+
+ # These hold the hexagon that the point falls in, which may be taken.
+ best_x, best_y = hexagon_pick(node_x, node_y, scale=scale)
+
+ for x, y in radial_search(best_x, best_y):
+ # These hexes are enumerated in order of increasign distance from the
+ # best one, starting with the best hex itself.
+
+ if hexagons[(x, y)] is None:
+ # This is the closest free hex. Break out of the loop, leaving x and
+ # y pointing here.
+ break
+
+ # Assign the node to the hexagon
+ hexagons[(x, y)] = node
+
+ return math.sqrt((x - best_x) ** 2 + (y - best_y) ** 2)
+
+
+
+def assign_hexagon_local_radial(hexagons, node_x, node_y, node, scale=1.0):
+ """
+ This function assigns the given node to a hexagon in hexagons. hexagons is a
+ defaultdict from tuples of hexagon (x, y) integer indices to assigned nodes,
+ or None if a hexagon is free. node_x and node_y are the x and y coordinates
+ of the node, adapted so that the seed node lands in the 0, 0 hexagon, and
+ re-scaled to reduce hexagon conflicts. node is the node to be assigned.
+ scale, if specified, is the hexagon side length in node space units.
+
+ This function assigns nodes to their closest hexagon. If thast hexagon is
+ full, it re-probes in the direction that the node is from the closest
+ hexagon's center.
+
+ When the function completes, node is stored in hexagons under some (x, y)
+ tuple.
+
+ Returns the distance this hexagon is from its ideal location.
+ """
+
+ # These hold the hexagon that the point falls in, which may be taken.
+ best_x, best_y = hexagon_pick(node_x, node_y, scale=scale)
+
+ # These hold the center of that hexagon in float space
+ center_x, center_y = hexagon_center(best_x, best_y, scale=scale)
+
+ # This holds the distance from this point to the center of that hexagon
+ node_distance = math.sqrt((node_x - center_x) ** 2 + (node_y - center_y) **
+ 2)
+
+ # These hold the normalized direction of this point, relative to the center
+ # of its best hexagon
+ direction_x = (node_x - center_x) / node_distance
+ direction_y = (node_y - center_y) / node_distance
+
+ # Do a search in that direction, starting at the best hex.
+
+ # These are the hexagon indices we're considering
+ x, y = best_x, best_y
+
+ # These are the Cartesian coordinates we're probing. Must be in the x, y hex
+ # as a loop invariant.
+ test_x, test_y = center_x, center_y
+
+ while hexagons[(x, y)] is not None:
+ # Re-probe outwards from the best hex in scale/2-sized steps
+ # TODO: is that the right step size? Scale-sized steps seemed slightly
+ # large.
+ test_x += direction_x * scale
+ test_y += direction_y * scale
+
+ # Re-pick x and y for the hex containing our test point
+ x, y = hexagon_pick(test_x, test_y, scale=scale)
+
+ # We've finally reached the edge of the cluster.
+ # Drop our hexagon
+ hexagons[(x, y)] = node
+
+ return math.sqrt((x - best_x) ** 2 + (y - best_y) ** 2)
+
+def assign_hexagon_radial(hexagons, node_x, node_y, node, scale=1.0):
+ """
+ This function assigns the given node to a hexagon in hexagons. hexagons is a
+ defaultdict from tuples of hexagon (x, y) integer indices to assigned nodes,
+ or None if a hexagon is free. node_x and node_y are the x and y coordinates
+ of the node, adapted so that the seed node lands in the 0, 0 hexagon, and
+ re-scaled to reduce hexagon conflicts. node is the node to be assigned.
+ scale, if specified, is the hexagon side length in node space units.
+
+ This function assigns nodes to hexagons based on radial distance from 0, 0.
+ This makes hexagon assignment much more dense, but can lose spatial
+ structure.
+
+ When the function completes, node is stored in hexagons under some (x, y)
+ tuple.
+
+ Returns the distance this hexagon is from its ideal location. Unfortunately,
+ this doesn't really make sense for this assignment scheme, so it is always
+ 0.
+ """
+
+ # Compute node's distance from the origin
+ node_distance = math.sqrt(node_x ** 2 + node_y ** 2)
+
+ # Compute normalized direction from the origin for this node
+ direction_x = node_x / node_distance
+ direction_y = node_y / node_distance
+
+ # These are the coordinates we are testing
+ test_x = 0
+ test_y = 0
+
+ # These are the hexagon indices that correspond to that point
+ x, y = hexagon_pick(test_x, test_y, scale=scale)
+
+ while hexagons[(x, y)] is not None:
+ # Re-probe outwards from the origin in scale-sized steps
+ # TODO: is that the right step size?
+ test_x += direction_x * scale
+ test_y += direction_y * scale
+
+ # Re-pick
+ x, y = hexagon_pick(test_x, test_y, scale=scale)
+
+ # We've finally reached the edge of the cluster.
+ # Drop our hexagon
+ # TODO: this has to be N^2 if we line them all up in a line
+ hexagons[(x, y)] = node
+
+ return 0
+
+def hexagons_in_window(hexagons, x, y, width, height):
+ """
+ Given a dict from (x, y) position to signature names, return the list of all
+ signatures in the window starting at hexagon x, y and extending width in the
+ x direction and height in the y direction on the hexagon grid.
+ """
+
+ # This holds the list of hexagons we've found
+ found = []
+
+ for i in xrange(x, x + width):
+ for j in xrange(y, y + height):
+ if hexagons.has_key((i, j)):
+ # This position in the window has a hex.
+ found.append(hexagons[(i, j)])
+
+ return found
+
+class ClusterFinder(object):
+ """
+ A class that can be invoked to find the p value of the best cluster in its
+ layer. Instances are pickleable.
+ """
+
+ def __init__(self, hexagons, layer, window_size=5):
+ """
+ Keep the given hexagons dict (from (x, y) to signature name) and the
+ given layer (a dict from signature name to a value), and the given
+ window size, in a ClusterFinder object.
+ """
+
+ # TODO: This should probably all operate on numpy arrays that we can
+ # slice efficiently.
+
+ # Store the layer
+ self.hexagons = hexagons
+ # Store the hexagon assignments
+ self.layer = layer
+
+ # Store the window size
+ self.window_size = window_size
+
+ @staticmethod
+ def continuous_p(in_values, out_values):
+ """
+ Get the p value for in_values and out_values being distinct continuous
+ distributions.
+
+ in_values and out_values are both Numpy arrays. Returns the p value, or
+ raises a ValueError if the statistical test cannot be run for some
+ reason.
+
+ Uses the Mann-Whitney U test.
+ """
+
+ # Do a Mann-Whitney U test to see how different the data
+ # sets are.
+ u_statistic, p_value = scipy.stats.mannwhitneyu(in_values,
+ out_values)
+
+ return p_value
+
+ @staticmethod
+ def dichotomous_p(in_values, out_values):
+ """
+ Given two one-dimensional Numpy arrays of 0s and 1s, compute a p value
+ for the in_values having a different probability of being 1 than the
+ frequency of 1s in the out_values.
+
+ This test uses the scipy.stats.binom_test function, which does not claim
+ to use the normal approximation. Therefore, this test should be valid
+ for arbitrarily small frequencies of either 0s or 1s in in_values.
+
+ TODO: What if out_values is shorter than in_values?
+ """
+
+ if len(out_values) == 0:
+ raise ValueError("Background group is empty!")
+
+ # This holds the observed frequency of 1s in out_values
+ frequency = numpy.sum(out_values) / len(out_values)
+
+ # This holds the number of 1s in in_values
+ successes = numpy.sum(in_values)
+
+ # This holds the number of "trials" we got that many successes in
+ trials = len(in_values)
+
+ # Return how significantly the frequency inside differs from that
+ # outside.
+ return scipy.stats.binom_test(successes, trials, frequency)
+
+ @staticmethod
+ def categorical_p(in_values, out_values):
+ """
+ Given two one-dimensional Numpy arrays of integers (which may be stored
+ as floats), which represent items being assigned to different
+ categories, return a p value for the distribution of categories observed
+ in in_values differing from that observed in out_values.
+
+ The normal way to do this is with a chi-squared goodness of fit test.
+ However, that test has invalid assumptions when there are fewer than 5
+ expected and 5 observed observations in every category.
+ See http://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.chis
+ quare.html
+
+ However, we will use it anyway, because the tests that don't break down
+ are prohibitively slow.
+ """
+
+ # Convert our inputs to integer arrays
+ in_values = in_values.astype(int)
+ out_values = out_values.astype(int)
+
+ # How many categories are there (count 0 to the maximum value)
+ num_categories = max(numpy.max(in_values), numpy.max(out_values)) + 1
+
+ # Count the number of in_values and out_values in each category
+ in_counts = numpy.array([len(in_values[in_values == i]) for i in
+ xrange(num_categories)])
+ out_counts = numpy.array([len(out_values[out_values == i]) for i in
+ xrange(num_categories)])
+
+ # Get the p value for the window being from the estimated distribution
+ # None of the distribution parameters count as "estimated from data"
+ # because they aren't estimated from the data under test.
+ _, p_value = scipy.stats.chisquare(in_counts, out_counts)
+
+ return p_value
+
+ def __call__(self):
+ """
+ Find the best p value for any window of size window_size. Return it.
+ """
+
+ # Calculate the bounding box where we want to look for windows.
+ # TODO: This would just be all of a numpy array
+ min_x = min(coords[0] for coords in self.hexagons.iterkeys())
+ min_y = min(coords[1] for coords in self.hexagons.iterkeys())
+ max_x = max(coords[0] for coords in self.hexagons.iterkeys())
+ max_y = max(coords[1] for coords in self.hexagons.iterkeys())
+
+ # This holds a Numpy array of all the data by x, y
+ layer_data = numpy.empty((max_x - min_x + 1, max_y - min_y + 1))
+
+ # Fill it with NaN so we can mask those out later
+ layer_data[:] = numpy.NAN
+
+ for (hex_x, hex_y), name in self.hexagons.iteritems():
+ # Copy the layer values into the Numpy array
+ if self.layer.has_key(name):
+ layer_data[hex_x - min_x, hex_y - min_y] = self.layer[name]
+
+ # This holds a masked version of the layer data
+ layer_data_masked = numpy.ma.masked_invalid(layer_data, copy=False)
+
+ # This holds the smallest p value we have found for this layer
+ best_p = float("+inf")
+
+ # This holds the statistical test to use (a function from two Numpy
+ # arrays to a p value)
+ # The most specific test is the dichotomous test (0 or 1)
+ statistical_test = self.dichotomous_p
+
+ if numpy.sum(~layer_data_masked.mask) == 0:
+ # There is actually no data in this layer at all.
+ # nditer complains if we try to iterate over an empty thing.
+ # So quit early and say we couldn't find anything.
+ return best_p
+
+ for value in numpy.nditer(layer_data_masked[~layer_data_masked.mask]):
+ # Check all the values in the layer.
+ # If this value is out of the domain of the current statistical
+ # test, upgrade to a more general test.
+
+ if statistical_test == self.dichotomous_p and (value > 1 or
+ value < 0):
+
+ # We can't use a dichotomous test on things outside 0 to 1
+ # But we haven't yet detected any non-integers
+ # Use categorical
+ statistical_test = self.categorical_p
+
+ if value % 1 != 0:
+ # This is not an integer value
+ # So, we must use a continuous statistical test
+ statistical_test = self.continuous_p
+
+ # This is the least specific test, so we can stop now
+ break
+
+
+ for i in xrange(min_x, max_x - self.window_size):
+ for j in xrange(min_y, max_y - self.window_size):
+
+ # Get the layer values for hexes in the window, as a Numpy
+ # masked array.
+ in_region = layer_data_masked[i:i + self.window_size,
+ j:j + self.window_size]
+
+ # And as a 1d Numpy array
+ in_values = numpy.reshape(in_region[~in_region.mask], -1).data
+
+ # And out of the window (all the other hexes) as a masked array
+ out_region = numpy.ma.copy(layer_data_masked)
+ # We get this by masking out everything in the region
+ out_region.mask[i:i + self.window_size,
+ j:j + self.window_size] = True
+
+ # And as a 1d Numpy array
+ out_values = numpy.reshape(out_region[~out_region.mask],
+ -1).data
+
+
+ if len(in_values) == 0 or len(out_values) == 0:
+ # Can't do any stats on this window
+ continue
+
+ if len(in_values) < 0.5 * self.window_size ** 2:
+ # The window is less than half full. Skip it.
+ # TODO: Make this threshold configurable.
+ continue
+
+ try:
+
+ # Get the p value for this window under the selected
+ # statistical test
+ p_value = statistical_test(in_values, out_values)
+
+ # If this is the best p value so far, record it
+ best_p = min(best_p, p_value)
+ except ValueError:
+ # Probably an all-zero layer, or something else the test
+ # can't handle.
+ # But let's try all the other windows to be safe.
+ # Maybe one will work.
+ pass
+
+
+
+ # We have now found the best p for any window for this layer.
+ print "Best p found: {}".format(best_p)
+ sys.stdout.flush()
+
+ return best_p
+
+def run_functor(functor):
+ """
+ Given a no-argument functor (like a ClusterFinder), run it and return its
+ result. We can use this with multiprocessing.map and map it over a list of
+ job functors to do them.
+
+ Handles getting more than multiprocessing's pitiful exception output
+ """
+
+ try:
+ return functor()
+ except:
+ # Put all exception text into an exception and raise that
+ raise Exception(traceback.format_exc())
+
+def main(args):
+ """
+ Parses command line arguments, and makes visualization.
+ "args" specifies the program arguments, with args[0] being the executable
+ name. The return value should be used as the program's exit code.
+ """
+
+ options = parse_args(args) # This holds the nicely-parsed options object
+
+ # Test our picking
+ x, y = hexagon_center(0, 0)
+ if hexagon_pick(x, y) != (0, 0):
+ raise Exception("Picking is broken!")
+
+ # First bit of stdout becomes annotation in Galaxy
+
+ # Make sure our output directory exists.
+ if not os.path.exists(options.directory):
+ # makedirs is the right thing to use here: recursive
+ os.makedirs(options.directory)
+
+ # Work in a temporary directory
+ drl_directory = tempfile.mkdtemp()
+
+ # This is the base name for all the files that DrL uses to do the layout
+ # We're going to put it in a temporary directory.
+ drl_basename = os.path.join(drl_directory, "layout")
+
+ # We can just pass our similarity matrix to DrL's truncate
+ # But we want to run it through our tsv parser to strip comments and ensure
+ # it's valid
+
+ # This holds a reader for the similarity matrix
+ sim_reader = tsv.TsvReader(options.similarities)
+
+ # This holds a writer for the sim file
+ sim_writer = tsv.TsvWriter(open(drl_basename + ".sim", "w"))
+
+ print "Regularizing similarity matrix..."
+ sys.stdout.flush()
+
+ for parts in sim_reader:
+ sim_writer.list_line(parts)
+
+ sim_reader.close()
+ sim_writer.close()
+
+ # Now our input for DrL is prepared!
+
+ # Do DrL truncate.
+ # TODO: pass a truncation level
+ print "DrL: Truncating..."
+ sys.stdout.flush()
+ subprocess.check_call(["truncate", drl_basename])
+
+ # Run the DrL layout engine.
+ print "DrL: Doing layout..."
+ sys.stdout.flush()
+ subprocess.check_call(["layout", drl_basename])
+
+ # Put the string names back
+ print "DrL: Restoring names..."
+ sys.stdout.flush()
+ subprocess.check_call(["recoord", drl_basename])
+
+ # Now DrL has saved its coordinates as \t\t rows in
+ # .coord
+
+ # We want to read that.
+ # This holds a reader for the DrL output
+ coord_reader = tsv.TsvReader(open(drl_basename + ".coord", "r"))
+
+ # This holds a dict from signature name string to (x, y) float tuple
+ nodes = {}
+
+ print "Reading DrL output..."
+ sys.stdout.flush()
+ for parts in coord_reader:
+ nodes[parts[0]] = (float(parts[1]), float(parts[2]))
+
+ coord_reader.close()
+
+ # Save the DrL coordinates in our bundle, to be displayed client-side for
+ # debugging.
+ coord_writer = tsv.TsvWriter(open(
+ os.path.join(options.directory, "drl.tab"), "w"))
+
+ for signature_name, (x, y) in nodes.iteritems():
+ # Write a tsv with names instead of numbers, like what DrL recoord would
+ # have written. This is what the Javascript on the client side wants.
+ coord_writer.line(signature_name, x, y)
+
+ coord_writer.close()
+
+ # Do the hexagon layout
+ # We do the squiggly rows setup, so express everything as integer x, y
+
+ # This is a defaultdict from (x, y) integer tuple to id that goes there, or
+ # None if it's free.
+ hexagons = collections.defaultdict(lambda: None)
+
+ # This holds the side length that we use
+ side_length = 1.0
+
+ # This holds what will be a layer of how badly placed each hexagon is
+ # A dict from node name to layer value
+ placement_badnesses = {}
+
+ for node, (node_x, node_y) in nodes.iteritems():
+ # Assign each node to a hexagon
+ # This holds the resulting placement badness for that hexagon (i.e.
+ # distance from ideal location)
+ badness = assign_hexagon(hexagons, node_x, node_y, node,
+ scale=side_length)
+
+ # Put the badness in the layer
+ placement_badnesses[node] = float(badness)
+
+ # Normalize the placement badness layer
+ # This holds the max placement badness
+ max_placement_badness = max(placement_badnesses.itervalues())
+ print "Max placement badness: {}".format(max_placement_badness)
+
+ if max_placement_badness != 0:
+ # Normalize by the max if possible.
+ placement_badnesses = {node: value / max_placement_badness for node,
+ value in placement_badnesses.iteritems()}
+
+ # The hexagons have been assigned. Make hexagons be a dict instead of a
+ # defaultdict, so it pickles.
+ # TODO: I should change it so I don't need to do this.
+ hexagons = dict(hexagons)
+
+ # Now dump the hexagon assignments as an id, x, y tsv. This will be read by
+ # the JavaScript on the static page and be used to produce the
+ # visualization.
+ hexagon_writer = tsv.TsvWriter(open(os.path.join(options.directory,
+ "assignments.tab"), "w"))
+
+ # First find the x and y offsets needed to make all hexagon positions
+ # positive
+ min_x = min(coords[0] for coords in hexagons.iterkeys())
+ min_y = min(coords[1] for coords in hexagons.iterkeys())
+
+ for coords, name in hexagons.iteritems():
+ # Write this hexagon assignment, converted to all-positive coordinates.
+ hexagon_writer.line(name, coords[0] - min_x, coords[1] - min_y)
+ hexagon_writer.close()
+
+ # Now that we have hex assignments, compute layers.
+
+ # In addition to making per-layer files, we're going to copy all the score
+ # matrices to our output directoy. That way, the client can download layers
+ # in big chunks when it wants all layer data for statistics. We need to
+ # write a list of matrices that the client can read, which is written by
+ # this TSV writer.
+ matrix_index_writer = tsv.TsvWriter(open(os.path.join(options.directory,
+ "matrices.tab"), "w"))
+
+ # Read in all the layer data at once
+ # TODO: Don't read in all the layer data at once
+
+ # This holds a dict from layer name to a dict from signature name to
+ # score.
+ layers = {}
+
+ # This holds the names of all layers
+ layer_names = []
+
+ for matrix_number, score_filename in enumerate(options.scores):
+ # First, copy the whole matrix into our output. This holds its filename.
+ output_filename = "matrix_{}.tab".format(matrix_number)
+ shutil.copy2(score_filename, os.path.join(options.directory,
+ output_filename))
+
+ # Record were we put it
+ matrix_index_writer.line(output_filename)
+
+ # This holds a reader for the scores TSV
+ scores_reader = tsv.TsvReader(open(score_filename, "r"))
+
+ # This holds an iterator over lines in that file
+ # TODO: Write a proper header/data API
+ scores_iterator = scores_reader.__iter__()
+
+ try:
+ # This holds the names of the columns (except the first, which is
+ # labels). They also happen to be layer names
+ file_layer_names = scores_iterator.next()[1:]
+
+ # Add all the layers in this file to the complete list of layers.
+ layer_names += file_layer_names
+
+ # Ensure that we have a dict for every layer mentioned in the file
+ # (even the ones that have no data below). Doing it this way means
+ # all score matrices need disjoint columns, or the last one takes
+ # precedence.
+ for name in file_layer_names:
+ layers[name] = {}
+
+ for parts in scores_iterator:
+ # This is the signature that this line is about
+ signature_name = parts[0]
+
+ # These are the scores for all the layers for this signature
+ layer_scores = parts[1:]
+
+ for (layer_name, score) in itertools.izip(file_layer_names,
+ layer_scores):
+
+ # Store all the layer scores in the appropriate
+ # dictionaries.
+ try:
+ layers[layer_name][signature_name] = float(score)
+ except ValueError:
+ # This is not a float.
+ # Don't set that entry for this layer.
+ # TODO: possibly ought to complain to the user? But then
+ # things like "N/A" won't be handled properly.
+ continue
+
+ except StopIteration:
+ # We don't have any real data here. Couldn't read the header line.
+ # Skip to the next file
+ pass
+
+ # We're done with this score file now
+ scores_reader.close()
+
+ # We're done with all the input score matrices, so our index is done too.
+ matrix_index_writer.close()
+
+ # We have now loaded all layer data into memory as Python objects. What
+ # could possibly go wrong?
+
+ # Stick our placement badness layer on the end
+ layer_names.append("Placement Badness")
+ layers["Placement Badness"] = placement_badnesses
+
+ # Now we need to write layer files.
+
+ # Generate some filenames for layers that we can look up by layer name.
+ # We do this because layer names may not be valid filenames.
+ layer_files = {name: os.path.join(options.directory,
+ "layer_{}.tab".format(number)) for (name, number) in itertools.izip(
+ layer_names, itertools.count())}
+
+ for layer_name, layer in layers.iteritems():
+ # Write out all the individual layer files
+ # This holds the writer for this layer file
+ scores_writer = tsv.TsvWriter(open(layer_files[layer_name], "w"))
+ for signature_name, score in layer.iteritems():
+ # Write the score for this signature in this layer
+ scores_writer.line(signature_name, score)
+ scores_writer.close()
+
+ # We need something to sort layers by. We have "priority" (lower is
+ # better)
+
+ if len(layer_names) > 0 and options.stats:
+ # We want to do this fancy parallel stats thing.
+ # We skip it when there are no layers, so we don't try to join a
+ # never-used pool, which seems to hang.
+
+ print "Running statistics..."
+
+ # This holds an iterator that makes ClusterFinders for all out layers
+ cluster_finders = [ClusterFinder(hexagons, layers[layer_name],
+ window_size=options.window_size) for layer_name in layer_names]
+
+ print "{} jobs to do.".format(len(cluster_finders))
+
+ # This holds a multiprocessing pool for parallelization
+ pool = multiprocessing.Pool()
+
+ # This holds all the best p values in the same order
+ best_p_values = pool.map(run_functor, cluster_finders)
+
+ # Close down the pool so multiprocessing won't die sillily at the end
+ pool.close()
+ pool.join()
+
+ # This holds a dict from layer name to priority (best p value)
+ # We hope the order of the dict items has not changed
+ layer_priorities = {layer_name: best_p_value for layer_name,
+ best_p_value in itertools.izip(layer_names, best_p_values)}
+ else:
+ # We aren't doing any stats.
+
+ print "Skipping statistics."
+
+ # Make up priorities.
+ layer_priorities = {name: float("+inf") for name in layer_names}
+
+ # Count how many layer entries are greater than 0 for each binary layer, and
+ # store that number in this dict by layer name. Things with the default
+ # empty string instead of a number aren't binary layers, but they can use
+ # the empty string as their TSV field value, so we can safely pull any layer
+ # out of this by name.
+ layer_positives = collections.defaultdict(str)
+
+ for layer_name in layer_names:
+ # Assume it's a binary layer until proven otherwise
+ layer_positives[layer_name] = 0
+ for value in layers[layer_name].itervalues():
+ if value == 1:
+ # Count up all the 1s in the layer
+ layer_positives[layer_name] += 1
+ elif value != 0:
+ # It has something that isn't 1 or 0, so it can't be a binary
+ # layer. Throw it out and try the next layer.
+ layer_positives[layer_name] = ""
+ break
+
+ # Write an index of all the layers we have, in the form:
+ # \t\t\t\t
+ # This is the writer to use.
+ index_writer = tsv.TsvWriter(open(os.path.join(options.directory,
+ "layers.tab"), "w"))
+
+ for layer_name, layer_file in layer_files.iteritems():
+ # Write the index entry for this layer
+ index_writer.line(layer_name, os.path.basename(layer_file),
+ layer_priorities[layer_name], len(layers[layer_name]),
+ layer_positives[layer_name])
+
+ index_writer.close()
+
+ # Copy over the user-specified colormaps file, or make an empty TSV if it's
+ # not specified.
+
+
+
+ # This holds a writer for the sim file. Creating it creates the file.
+ colormaps_writer = tsv.TsvWriter(open(os.path.join(options.directory,
+ "colormaps.tab"), "w"))
+
+ if options.colormaps is not None:
+ # The user specified colormap data, so copy it over
+ # This holds a reader for the colormaps file
+ colormaps_reader = tsv.TsvReader(options.colormaps)
+
+ print "Regularizing colormaps file..."
+ sys.stdout.flush()
+
+ for parts in colormaps_reader:
+ colormaps_writer.list_line(parts)
+
+ colormaps_reader.close()
+
+ # Close the colormaps file we wrote. It may have gotten data, or it may
+ # still be empty.
+ colormaps_writer.close()
+
+ # Now copy any static files from where they live next to this Python file
+ # into the web page bundle.
+ # This holds the directory where this script lives, which also contains
+ # static files.
+ tool_root = os.path.dirname(os.path.realpath(__file__))
+
+ # Copy over all the static files we need for the web page
+ # This holds a list of them
+ static_files = [
+ # Static images
+ "drag.svg",
+ "filter.svg",
+ "statistics.svg",
+ "right.svg",
+ "throbber.svg",
+
+ # jQuery itself is pulled from a CDN.
+ # We can't take everything offline since Google Maps needs to be sourced
+ # from Google, so we might as well use CDN jQuery.
+
+ # Select2 scripts and resources:
+ "select2.css",
+ "select2.js",
+ "select2.png",
+ "select2-spinner.gif",
+ "select2x2.png",
+
+ # The jQuery.tsv plugin
+ "jquery.tsv.js",
+ # The color library
+ "color-0.4.1.js",
+ # The jStat statistics library
+ "jstat-1.0.0.js",
+ # The Google Maps MapLabel library
+ "maplabel-compiled.js",
+ # The main CSS file
+ "hexagram.css",
+ # The main JavaScript file that runs the page
+ "hexagram.js",
+ # Web Worker for statistics
+ "statistics.js",
+ # File with all the tool code
+ "tools.js"
+ ]
+
+ # We'd just use a directory of static files, but Galaxy needs single-level
+ # output.
+ for filename in static_files:
+ shutil.copy2(os.path.join(tool_root, filename), options.directory)
+
+ # Copy the HTML file to our output file. It automatically knows to read
+ # assignments.tab, and does its own TSV parsing
+ shutil.copy2(os.path.join(tool_root, "hexagram.html"), options.html)
+
+ # Delete our temporary directory.
+ shutil.rmtree(drl_directory)
+
+ print "Visualization generation complete!"
+
+ return 0
+
+if __name__ == "__main__" :
+ try:
+ # Get the return code to return
+ # Don't just exit with it because sys.exit works by exceptions.
+ return_code = main(sys.argv)
+ except:
+ traceback.print_exc()
+ # Return a definite number and not some unspecified error code.
+ return_code = 1
+
+ sys.exit(return_code)
diff -r 000000000000 -r 95ff566506f4 hexagram/hexagram.xml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/hexagram.xml Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,55 @@
+
+ Interactive hex grid clustering visualization
+
+ drl-graph-layout
+
+
+ hexagram.py "$similarity"
+ #for $i, $s in enumerate( $scores )
+ --scores "${s.score_matrix.file_name}"
+ #end for
+ #if $query:
+ --query "$query"
+ #end if
+ #if $colormaps
+ --colormaps "$colormaps"
+ #end if
+ --html "$output"
+ --directory "$output.files_path"
+ #if $nostats
+ --no-stats
+ #end if
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff -r 000000000000 -r 95ff566506f4 hexagram/jquery.tsv.js
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/jquery.tsv.js Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,357 @@
+/**
+ * jQuery-tsv (jQuery Plugin)
+ *
+ * Inspired by jQuery-csv by Evan Plaice.
+ *
+ * Copyright 2012 by Bob Kerns
+ *
+ * This software is licensed as free software under the terms of the MIT License:
+ * http://www.opensource.org/licenses/mit-license.php
+ */
+
+(function ($) {
+ // Make sure we have a copy, not original, of $.tsv.options.
+ function copyOptions(options) {
+ return $.extend({__copy: true}, options);
+ }
+ // Default the options.
+ function tsvOptions(options) {
+ if (options) {
+ if (options.__defaults_applied) {
+ return options;
+ }
+ return $.extend(copyOptions($.tsv.options), options);
+ }
+ return copyOptions($.tsv.options);
+ }
+
+ function tsvColumn(options, index) {
+ var opts = tsvOptions(options);
+ return String(opts.columns ? opts.columns[index] : index);
+ }
+
+ function tsvColumns(options, top) {
+ if (options.columns) {
+ return options.columns;
+ } else {
+ var cols = Object.keys(top || {}).sort();
+ options.columns = cols;
+ return cols;
+ }
+ }
+
+ $.tsv = {
+ version: "0.957",
+ /**
+ * The default set of options. It is not recommended to change these, as the impact will be global
+ */
+ options: {
+ /**
+ * If supplied, a function to format a value on output.
+ * The returned value is used in the output instead of the supplied value.
+ * If not supplied, it is simply converted to a string.
+ *
+ * @param value the value to be formatted.
+ * @param the options
+ * @param colnum the column number
+ * @param colname the column name, if known, or the column number as a string.
+ * @param rownum the row number
+ * @returns the value, formatted
+ */
+ formatValue: null,
+ /**
+ * If supplied, a function to parse or canonicalize a value on input.
+ * The returned value is used in place of the input.
+ *
+ * @param value the value to be formatted.
+ * @param the options
+ * @param colnum the column number
+ * @param colname the column name, if known, or the column number as a string.
+ * @param rownum the row number
+ * @returns the value, parsed
+ */
+ parseValue: null,
+ /**
+ * The character sequence to use to separate lines.
+ */
+ lineSeparator: "\n",
+ /** A RegExp to recognize line separators */
+ lineSplitter: /\r?\n/,
+ /** The character sequence to use to separate values. */
+ valueSeparator: "\t",
+ /** A RegExp to recognize value separators. */
+ valueSplitter: /\t/,
+ /**
+ * If supplied, a function of one argument to convert a row to an object.
+ *
+ * @param row an array of values, e.g. ["1", "2", "3.14"]
+ * @param options { columns: ["id", "count", "price"] }
+ * @returns e.g. {id: "1", count: "2", price: "3.14"}
+ */
+ arrayToObject: null,
+ /**
+ * If supplied, a function of one argument to convert an object to a row. Typically, this will implement a variant
+ * of the contract for $.tsv.objectToArray.
+ *
+ * @param object an object to be converted to a row, e.g. {id: "1", count: "2", price: "3.14"}
+ * @param options { columns: ["id", "count", "price"] }
+ * @returns an array of values, e.g. ["1", "2", "3.14"]. Typically these would be ordered by options.column
+ */
+ objectToArray: null,
+ /**
+ * If true, when converting from an array of objects to a TSV string, include the column names as the
+ * first line. For most purposes, you won't want to override this, but if you're working with tables in sections,
+ * for example, you'd want to suppress this for the latter segments.
+ *
+ * But you are strongly encouraged to use column names whenever possible, especially if you work with objects.
+ */
+ includeHeader: true,
+ /**
+ * The starting row number, not counting the header, if any (which is always numbered -1).
+ * This can be useful for computing subranges of a table, or appending to a table.
+ */
+ startRownum: 0,
+ // An internal flag, to avoid multiple defaulting steps.
+ // values are true, if it is this default, or 'copy'.
+ ___defaults_applied: true,
+ extend: $.extend
+ },
+
+ /**
+ * Parse one value. This can be overridden in the options.
+ * @param value the string to parse
+ * @param options optional: { parseValue: }
+ * @param colnum the column number
+ * @param colname the column name, if known, or the column number as a string.
+ * @param rownum the row number
+ * @returns the string
+ */
+ parseValue: function parseValue(value, options, colnum, colname, rownum) {
+ var opts = tsvOptions(options);
+ if (opts.parseValue) {
+ // We have an override; use that instead.
+ return options.parseValue(value, opts, colnum, colname, rownum);
+ }
+ return value;
+ },
+
+ /**
+ * Format one value. This can be overridden in the options.
+ * @param value the value to format
+ * @param options optional: { formatValue: }
+ * @param colnum the column number
+ * @param colname the column name, if known, or the column number as a string.
+ * @param rownum the row number
+ */
+ formatValue: function formatValue(value, options, rownum, colnum, colname, rownum) {
+ var opts = tsvOptions(options);
+ if (opts.formatValue) {
+ // We have an override; use that instead.
+ return options.formatValue(value, opts, colnum, colname, rownum);
+ }
+ return String(value);
+ },
+
+ /**
+ * $.tsv.toArray(line, options) parses one line of TSV input into an array of values.
+ * @param line A line with values separated by single tab characters, e.g. "11\t12\t13"
+ * @param options optional: { valueSplitter: /\t/, parseValue: }
+ * @param rownum optional: the row number (defaults to 0);
+ * @returns an array of values, e.g. ["11" "12", "13"]
+ */
+ toArray: function toArray(line, options, rownum) {
+ var opts = tsvOptions(options);
+ var valueSplitter = opts.valueSplitter;
+ rownum = rownum || 0;
+ var colnum = 0;
+ function doValue(val) {
+ var c = colnum++;
+ return $.tsv.parseValue(val, opts, c, tsvColumn(opts, c), rownum);
+ }
+ return line.split(valueSplitter).map(doValue);
+ },
+
+ /**
+ * $.tsv.fromArray(row, options) returns one line of TSV input from an array of values.
+ * @param array an array of values, e.g. ["11" "12", "13"]
+ * @param options optional: { valueSeparator: "\t", formatValue: }
+ * @param rownum optional: the row number (defaults to 0);
+ * @returns A line with values separated by single tab characters, e.g. "11\t12\t13"
+ */
+ fromArray: function fromArray(array, options, rownum) {
+ var opts = tsvOptions(options);
+ var valueSeparator = opts.valueSeparator;
+ var colnum = 0;
+ function doValue(val) {
+ var c = colnum++;
+ return $.tsv.formatValue(val, opts, c, tsvColumn(c), rownum);
+ }
+ return array.map(doValue).join(valueSeparator);
+ },
+
+ /**
+ * $.tsv.toArrays(tsv, options) returns an array of arrays, one per line, each containing values from one row.
+ * @param tsv a tab-separated-values input, e.g. "11\t\12\t13\n21\t22\t23"
+ * @param options optional: { valueSplitter: /\t/, lineSplitter: /\r?\n/, parseValue: }
+ * @returns an array of arrays, e.g. [["11", "12", "13"], ["21", "22", "23"]]
+ */
+ toArrays: function toArrays(tsv, options) {
+ var opts = tsvOptions(options);
+ var lines = tsv.split(opts.lineSplitter);
+ var rownum = opts.startRownum || 0;
+ return lines.map(function doLine(line) {
+ return $.tsv.toArray(line, opts, rownum++);
+ });
+ },
+
+ /**
+ * $.tsv.fromArrays(array, options) returns a TSV string representing the array of row arrays.
+ * @param array an array of arrays of values. To produce valid TSV, all the arrays should be of the same length.
+ * @param options optional: { valueSeparator: "\t", lineSeparator: "\n", columns: ["c1", "c2", "c3"], formatValue: }
+ * @returns An tsv string, e.g. "c1\tc2\tc3\n11\t\12\t13\n21\t22\t23"
+ */
+ fromArrays: function fromArrays(array, options) {
+ var opts = tsvOptions(options);
+ var first = array.length ? array[0] : [];
+ var cols = tsvColumns(opts, first);
+ var rownum = opts.startRownum || 0;
+ var header = opts.includeHeader ? $.tsv.fromArray(cols, opts, -1) : undefined;
+ function doRow(row) {
+ return $.tsv.fromArray(row, opts, rownum++);
+ }
+ var rtemp = array.map(doRow);
+ if (header) {
+ rtemp.unshift(header);
+ }
+ return rtemp.join(opts.lineSeparator);
+ },
+
+ /**
+ * $.tsv.arrayToObject(row, options) returns an object whose fields are named in options.columns, and
+ * whose values come from the corresponding position in row (an array of values in the same order).
+ *
+ * If the columns are not supplied, "0", "1", etc. will be used.
+ * @param row the values, e.g. ["v1", "v2"]
+ * @param options optional: { columns: ["name1", "name2"], rowToObject: }
+ * @param rownum optional: the row number
+ * @returns an object derived from the elements of the row.
+ */
+ arrayToObject: function arrayToObject(row, options, rownum) {
+ var opts = tsvOptions(options);
+ rownum = rownum || 0;
+ var columns = tsvColumns(opts, row);
+ if (opts.arrayToObject) {
+ // We have an override; use that instead.
+ return opts.arrayToObject(row, opts, rownum);
+ }
+ var dict = {};
+ for (var j = 0; j < columns.length; j++) {
+ dict[columns[j]] = row[j];
+ }
+ return dict;
+ },
+
+ /**
+ * $.tsv.arraysToObjects(array, options) returns an array of objects, derived from the array.
+ * The array must either have the first row be column names, or columns: ["name1", "name2", ...] must be supplied
+ * in the options.
+ * @param array an array of arrays of values. [ ["name1", "name2" ...],? ["val1", "val2" ...] ...]
+ * @param options optional: { columns: ["name1", "name2", ...] }
+ * @returns An array of objects, [ { name1: val1, name2: val2 ... } ... ]
+ */
+ arraysToObjects: function arraysToObjects(array, options) {
+ var opts = tsvOptions(options);
+ if (! opts.columns) {
+ opts.columns = array.shift();
+ }
+ var rownum = opts.startRownum || 0;
+ return array.map(function convert(row) {
+ return $.tsv.arrayToObject(row, opts, rownum++);
+ });
+ },
+
+ /**
+ * $.tsv.toObjects(tsv, options) returns an array of objects from a tsv string.
+ * The string must either have the first row be column names, or columns: ["name1", "name2", ...] must be supplied
+ * in the options.
+ *
+ * @param A TSV string, e.g. "val1\tval2..." or "name1\tname2...\n\val1\val2..."
+ * @param options optional: { columns ["name1", "name2" ...] }
+ * @returns an array of objects, e.g. [ {name1: val1, name2: val2 ...} ...]
+ */
+ toObjects: function toObjects(tsv, options) {
+ var opts = tsvOptions(options);
+ return $.tsv.arraysToObjects($.tsv.toArrays(tsv, opts), opts);
+ },
+
+ /**
+ * $.tsv.objectToArray(obj, options) Convert one object to an array representation for storing as a TSV line.
+ *
+ * @param obj an object to convert to an array representations, e.g. { name1: "val1", name2: "val2" ... }
+ * @param options optional: { columns: ["name1", "name2"], objectToArray: }
+ * @param rownum optional: the row number
+ * @result an array, e.g. ["val1", "val2"]
+ */
+ objectToArray: function objectToArray(obj, options, rownum) {
+ var opts = tsvOptions(options);
+ var columns = tsvColumns(opts, obj);
+ rownum = rownum || 0;
+ if (opts.objectToArray) {
+ // We have an override; use that instead.
+ return opts.objectToArray(obj, opts, rownum);
+ }
+ var row = [];
+ for (var j = 0; j < columns.length; j++) {
+ row.push(obj[columns[j]]);
+ }
+ return row;
+ },
+
+ /**
+ * $.tsv.objectsToArrays(array, options) converts an array of objects into an array of row arrays.
+ *
+ * @param array An array of objects, e.g. [ { name1: "val1", name2: "val2", ...} ...]
+ * @param options { columns: ["name1", "name2"...], includeHeaders: true, objectToArray: }
+ */
+ objectsToArrays: function objectsToArrays(array, options) {
+ var opts = tsvOptions(options);
+ var rownum = options.startRownum;
+ var result = array.map(function convert(obj) {
+ return $.tsv.objectToArray(obj, opts, rownum++);
+ });
+ return result;
+ },
+
+ fromObject: function fromObject(array, options) {
+ var opts = tsvOptions(options);
+ return $.tsv.fromArray($.tsv.objectToArray(array, opts), opts);
+ },
+
+ /**
+ * $.tsv.fromObjects(array, options) converts an array of objects into a tsv string.
+ *
+ * @param array An array of objects, e.g. [ { name1: "val1", name2: "val2", ...} ...]
+ * @param options { columns: ["name1", "name2"...], includeHeaders: true, objectToArray: }
+ */
+ fromObjects: function fromObjects(array, options) {
+ var opts = tsvOptions(options);
+ var first = array.length ? array[0] : {};
+ // Calculate the columns while we still have the original objects. This is being called for side-effect!
+ tsvColumns(opts, first);
+ return $.tsv.fromArrays($.tsv.objectsToArrays(array, opts), opts);
+ },
+
+ extend: $.extend
+ };
+ // Compatibility with initial release.
+ $.tsv.parseRow = $.tsv.toArray;
+ $.tsv.parseRows = $.tsv.toArrays;
+ $.tsv.parseObject = $.tsv.toObject;
+ $.tsv.parseObjects = $.tsv.toObjects;
+ $.tsv.formatValue = $.tsv.formatValue;
+ $.tsv.formatRow = $.tsv.fromArray;
+ $.tsv.formatRows = $.tsv.fromArrays;
+ $.tsv.formatObject = $.tsv.fromObject;
+ $.tsv.formatObjects = $.tsv.fromObjects;
+
+})(jQuery);
diff -r 000000000000 -r 95ff566506f4 hexagram/jstat-1.0.0.js
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/jstat-1.0.0.js Tue Jun 11 18:26:25 2013 -0400
@@ -0,0 +1,2576 @@
+function jstat(){}
+j = jstat;
+/* Simple JavaScript Inheritance
+ * By John Resig http://ejohn.org/
+ * MIT Licensed.
+ */
+// Inspired by base2 and Prototype
+(function(){
+ var initializing = false, fnTest = /xyz/.test(function(){
+ xyz;
+ }) ? /\b_super\b/ : /.*/;
+ // The base Class implementation (does nothing)
+ this.Class = function(){};
+
+ // Create a new Class that inherits from this class
+ Class.extend = function(prop) {
+ var _super = this.prototype;
+
+ // Instantiate a base class (but only create the instance,
+ // don't run the init constructor)
+ initializing = true;
+ var prototype = new this();
+ initializing = false;
+
+ // Copy the properties over onto the new prototype
+ for (var name in prop) {
+ // Check if we're overwriting an existing function
+ prototype[name] = typeof prop[name] == "function" &&
+ typeof _super[name] == "function" && fnTest.test(prop[name]) ?
+ (function(name, fn){
+ return function() {
+ var tmp = this._super;
+
+ // Add a new ._super() method that is the same method
+ // but on the super-class
+ this._super = _super[name];
+
+ // The method only need to be bound temporarily, so we
+ // remove it when we're done executing
+ var ret = fn.apply(this, arguments);
+ this._super = tmp;
+
+ return ret;
+ };
+ })(name, prop[name]) :
+ prop[name];
+ }
+
+ // The dummy class constructor
+ function Class() {
+ // All construction is actually done in the init method
+ if ( !initializing && this.init )
+ this.init.apply(this, arguments);
+ }
+
+ // Populate our constructed prototype object
+ Class.prototype = prototype;
+
+ // Enforce the constructor to be what we expect
+ Class.constructor = Class;
+
+ // And make this class extendable
+ Class.extend = arguments.callee;
+
+ return Class;
+ };
+})();
+
+/******************************************************************************/
+/* Constants */
+/******************************************************************************/
+jstat.ONE_SQRT_2PI = 0.3989422804014327;
+jstat.LN_SQRT_2PI = 0.9189385332046727417803297;
+jstat.LN_SQRT_PId2 = 0.225791352644727432363097614947;
+jstat.DBL_MIN = 2.22507e-308;
+jstat.DBL_EPSILON = 2.220446049250313e-16;
+jstat.SQRT_32 = 5.656854249492380195206754896838;
+jstat.TWO_PI = 6.283185307179586;
+jstat.DBL_MIN_EXP = -999;
+jstat.SQRT_2dPI = 0.79788456080287;
+jstat.LN_SQRT_PI = 0.5723649429247;
+/******************************************************************************/
+/* jstat Functions */
+/******************************************************************************/
+jstat.seq = function(min, max, length) {
+ var r = new Range(min, max, length);
+ return r.getPoints();
+}
+
+jstat.dnorm = function(x, mean, sd, log) {
+ if(mean == null) mean = 0;
+ if(sd == null) sd = 1;
+ if(log == null) log = false;
+ var n = new NormalDistribution(mean, sd);
+ if(!isNaN(x)) {
+ // is a number
+ return n._pdf(x, log);
+ } else if(x.length) {
+ var res = [];
+ for(var i = 0; i < x.length; i++) {
+ res.push(n._pdf(x[i], log));
+ }
+ return res;
+ } else {
+ throw "Illegal argument: x";
+ }
+}
+
+jstat.pnorm = function(q, mean, sd, lower_tail, log) {
+ if(mean == null) mean = 0;
+ if(sd == null) sd = 1;
+ if(lower_tail == null) lower_tail = true;
+ if(log == null) log = false;
+
+ var n = new NormalDistribution(mean, sd);
+ if(!isNaN(q)) {
+ // is a number
+ return n._cdf(q, lower_tail, log);
+ } else if(q.length) {
+ var res = [];
+ for(var i = 0; i < q.length; i++) {
+ res.push(n._cdf(q[i], lower_tail, log));
+ }
+ return res;
+ } else {
+ throw "Illegal argument: x";
+ }
+}
+
+jstat.dlnorm = function(x, meanlog, sdlog, log) {
+ if(meanlog == null) meanlog = 0;
+ if(sdlog == null) sdlog = 1;
+ if(log == null) log = false;
+ var n = new LogNormalDistribution(meanlog, sdlog);
+ if(!isNaN(x)) {
+ // is a number
+ return n._pdf(x, log);
+ } else if(x.length) {
+ var res = [];
+ for(var i = 0; i < x.length; i++) {
+ res.push(n._pdf(x[i], log));
+ }
+ return res;
+ } else {
+ throw "Illegal argument: x";
+ }
+}
+
+jstat.plnorm = function(q, meanlog, sdlog, lower_tail, log) {
+ if(meanlog == null) meanlog = 0;
+ if(sdlog == null) sdlog = 1;
+ if(lower_tail == null) lower_tail = true;
+ if(log == null) log = false;
+
+ var n = new LogNormalDistribution(meanlog, sdlog);
+ if(!isNaN(q)) {
+ // is a number
+ return n._cdf(q, lower_tail, log);
+ }
+ else if(q.length) {
+ var res = [];
+ for(var i = 0; i < q.length; i++) {
+ res.push(n._cdf(q[i], lower_tail, log));
+ }
+ return res;
+ } else {
+ throw "Illegal argument: x";
+ }
+}
+
+jstat.dbeta = function(x, alpha, beta, ncp, log) {
+ if(ncp == null) ncp = 0;
+ if(log == null) log = false;
+ var b = new BetaDistribution(alpha, beta);
+ if(!isNaN(x)) {
+ // is a number
+ return b._pdf(x, log);
+ }
+ else if(x.length) {
+ var res = [];
+ for(var i = 0; i < x.length; i++) {
+ res.push(b._pdf(x[i], log));
+ }
+ return res;
+ } else {
+ throw "Illegal argument: x";
+ }
+}
+
+jstat.pbeta = function(q, alpha, beta, ncp, lower_tail, log) {
+ if(ncp == null) ncp = 0;
+ if(log == null) log = false;
+ if(lower_tail == null) lower_tail = true;
+
+ var b = new BetaDistribution(alpha, beta);
+ if(!isNaN(q)) {
+ // is a number
+ return b._cdf(q, lower_tail, log);
+ } else if(q.length) {
+ var res = [];
+ for(var i = 0; i < q.length; i++) {
+ res.push(b._cdf(q[i], lower_tail, log));
+ }
+ return res;
+ }
+ else {
+ throw "Illegal argument: x";
+ }
+}
+
+jstat.dgamma = function(x, shape, rate, scale, log) {
+ if(rate == null) rate = 1;
+ if(scale == null) scale = 1/rate;
+ if(log == null) log = false;
+
+ var g = new GammaDistribution(shape, scale);
+ if(!isNaN(x)) {
+ // is a number
+ return g._pdf(x, log);
+ } else if(x.length) {
+ var res = [];
+ for(var i = 0; i < x.length; i++) {
+ res.push(g._pdf(x[i], log));
+ }
+ return res;
+ } else {
+ throw "Illegal argument: x";
+ }
+}
+
+jstat.pgamma = function(q, shape, rate, scale, lower_tail, log) {
+ if(rate == null) rate = 1;
+ if(scale == null) scale = 1/rate;
+ if(lower_tail == null) lower_tail = true;
+ if(log == null) log = false;
+
+ var g = new GammaDistribution(shape, scale);
+ if(!isNaN(q)) {
+ // is a number
+ return g._cdf(q, lower_tail, log);
+ } else if(q.length) {
+ var res = [];
+ for(var i = 0; i < q.length; i++) {
+ res.push(g._cdf(q[i], lower_tail, log));
+ }
+ return res;
+ } else {
+ throw "Illegal argument: x";
+ }
+
+}
+
+jstat.dt = function(x, df, ncp, log) {
+ if(log == null) log = false;
+
+ var t = new StudentTDistribution(df, ncp);
+ if(!isNaN(x)) {
+ // is a number
+ return t._pdf(x, log);
+ } else if(x.length) {
+ var res = [];
+ for(var i = 0; i < x.length; i++) {
+ res.push(t._pdf(x[i], log));
+ }
+ return res;
+ } else {
+ throw "Illegal argument: x";
+ }
+
+}
+
+jstat.pt = function(q, df, ncp, lower_tail, log) {
+ if(lower_tail == null) lower_tail = true;
+ if(log == null) log = false;
+
+ var t = new StudentTDistribution(df, ncp);
+ if(!isNaN(q)) {
+ // is a number
+ return t._cdf(q, lower_tail, log);
+ } else if(q.length) {
+ var res = [];
+ for(var i = 0; i < q.length; i++) {
+ res.push(t._cdf(q[i], lower_tail, log));
+ }
+ return res;
+ } else {
+ throw "Illegal argument: x";
+ }
+
+}
+
+jstat.plot = function(x, y, options) {
+ if(x == null) {
+ throw "x is undefined in jstat.plot";
+ }
+ if(y == null) {
+ throw "y is undefined in jstat.plot";
+ }
+ if(x.length != y.length) {
+ throw "x and y lengths differ in jstat.plot";
+ }
+
+ var flotOpt = {
+ series: {
+ lines: {
+
+ },
+ points: {
+
+ }
+ }
+ };
+
+ // combine x & y
+ var series = [];
+ if(x.length == undefined) {
+ // single point
+ series.push([x, y]);
+ flotOpt.series.points.show = true;
+ } else {
+ // array
+ for(var i = 0; i < x.length; i++) {
+ series.push([x[i], y[i]]);
+ }
+ }
+
+ var title = 'jstat graph';
+
+ // configure Flot options
+ if(options != null) {
+ // options = JSON.parse(String(options));
+ if(options.type != null) {
+ if(options.type == 'l') {
+ flotOpt.series.lines.show = true;
+ } else if (options.type == 'p') {
+ flotOpt.series.lines.show = false;
+ flotOpt.series.points.show = true;
+ }
+ }
+ if(options.hover != null) {
+ flotOpt.grid = {
+ hoverable: options.hover
+ }
+ }
+
+ if(options.main != null) {
+ title = options.main;
+ }
+ }
+ var now = new Date();
+ var hash = now.getMilliseconds() * now.getMinutes() + now.getSeconds();
+ $('body').append('