<commit_before>/* -*- mode: espresso; espresso-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set softtabstop=2 shiftwidth=2 tabstop=2 expandtab: */

"use strict";

var SkeletonConnectivity = function() {
  this.widgetID = this.registerInstance();
  this.registerSource();
  this.init();
  // Default table layout to be side by side. Have it seperate from init() as
  // long as it is part of the top button row.
  this.tablesSideBySide = true;
};

SkeletonConnectivity.prototype = {};
$.extend(SkeletonConnectivity.prototype, new InstanceRegistry());
$.extend(SkeletonConnectivity.prototype, new SkeletonSource());

/**
 * Initializes the connectivity widget by setting all fields to their default
 * value.
 */
SkeletonConnectivity.prototype.init = function() {
  // An ordered list of neurons/skeletons for display
  this.ordered_skeleton_ids = [];
  // An (per se unordered) object mapping skeletonIDs to skeleton names
  this.skeletons = {};
  // Incoming an outgoing connections of current neurons
  this.incoming = {};
  this.outgoing = {};
  // Default upstream and downstream tables to be not collapsed
  this.upstreamCollapsed = false;
  this.downstreamCollapsed = false;
  // Thresholds for current skeleton set
  this.upThresholds = {};
  this.downThresholds = {};
  // Indicates whether single nodes should be hidden
  this.hideSingleNodePartners = false;
  // ID of the user who is currently reviewing or null for 'union'
  this.reviewFilter = null;
  // An object mapping skeleton IDs to their selection state
  this.skeletonSelection = {};
  // An object for remembering the selection state of the select-all controls
  this.selectAllSelection = {
    'up': false,
    'down': false,
  };
};

/** Appends only to the top list, that is, the set of seed skeletons
 *  for which all pre- and postsynaptic partners are listed. */
SkeletonConnectivity.prototype.append = function(models) {
  var skeletons = this.skeletons,
      added = 0,
      removed = 0,
      widgetID = this.widgetID;
  var new_skeletons = Object.keys(models).reduce(function(o, skid) {
    var model = models[skid];
    if (skid in skeletons) {
      if (model.selected) {
        // Update name
        skeletons[skid] = model.baseName;
        $('#a-connectivity-table-' + widgetID + '-' + skid).html(
            neuronNameService.getName(skid));
      } else {
        // Remove
        delete skeletons[skid];
        ++removed;
      }
    } else {
      if (model.selected) {
        o[skid] = models[skid].baseName;
        ++added;
      }
    }
    return o;
  }, {});

  if (0 === removed && 0 === added) {
    return;
  }
  
  // Update existing ones and add new ones
  $.extend(this.skeletons, new_skeletons);
  for (var skid in new_skeletons) {
    this.ordered_skeleton_ids.push(skid);
  }

  // Add skeletons
  neuronNameService.registerAll(this, models, (function() {
    this.update();
    this.updateLink(models);
  }).bind(this));
};

SkeletonConnectivity.prototype.getName = function() {
  return "Connectivity " + this.widgetID;
};

SkeletonConnectivity.prototype.destroy = function() {
  this.unregisterInstance();
  this.unregisterSource();
  neuronNameService.unregister(this);
};

SkeletonConnectivity.prototype.clear = function(source_chain) {
  this.init();
  this.update();
  this.clearLink(source_chain);
};

SkeletonConnectivity.prototype.removeSkeletons = function(skeleton_ids) {
  skeleton_ids.forEach(function(skid) {
    delete this.skeletons[skid];
    var index = this.ordered_skeleton_ids.indexOf(skid);
    if (index > -1) {
      this.ordered_skeleton_ids.splice(index, 1);
    }
  }, this);
  this.update();
  this.updateLink(this.getSelectedSkeletonModels());
};

SkeletonConnectivity.prototype.hasSkeleton = function(skeleton_id) {
  return skeleton_id in this.skeletons;
};

SkeletonConnectivity.prototype.updateModels = function(models, source_chain) {
  if (source_chain && (this in source_chain)) return; // break propagation loop
  if (!source_chain) source_chain = {};
  source_chain[this] = this;

  this.append(models);
};

SkeletonConnectivity.prototype.highlight = function(skeleton_id) {
  // TODO color the table row in green if present, clear all others
};

SkeletonConnectivity.prototype.getSelectedSkeletons = function() {
  // TODO refactor to avoid unnecessary operations
  return Object.keys(this.getSelectedSkeletonModels());
};

SkeletonConnectivity.prototype.getSkeletonModel = function(skeleton_id) {
  var e_name = $('#a-connectivity-table-' + this.widgetID + '-' + skeleton_id);
  if (0 === e_name.length) return null;
  var name = e_name.text();

  var pre = $("#presynaptic_to-show-skeleton-" + this.widgetID + "-" + skeleton_id);
  var post = $("#postsynaptic_to-show-skeleton-" + this.widgetID + "-" + skeleton_id);

  var color = new THREE.Color();
  if (pre.length > 0) {
    if (post.length > 0) color.setRGB(0.8, 0.6, 1); // both
    else color.setRGB(1, 0.4, 0.4); // pre
  } else if (post.length > 0) color.setRGB(0.5, 1, 1); // post

  var model = new SelectionTable.prototype.SkeletonModel(skeleton_id, name, color);
  model.selected = pre.is(':checked') || post.is(':checked');
  return model;
};

SkeletonConnectivity.prototype.getSelectedSkeletonModels = function() {
  var widgetID = this.widgetID;
  var skeletons = this.skeletons;
  // Read out skeletons from neuron list
  var models = Object.keys(this.skeletons).reduce(function(o, skid) {
    // Test if checked
    var cb = $('input#neuron-selector-' + widgetID + '-' + skid +
        '[type=checkbox]');
    if (cb.is(':checked')) {
      var name = skeletons[skid];
      o[skid] = new SelectionTable.prototype.SkeletonModel(skid,
          skeletons[skid], new THREE.Color().setRGB(1, 1, 0));
    }
    return o;
  }, {});

  var colors = [new THREE.Color().setRGB(1, 0.4, 0.4),
                new THREE.Color().setRGB(0.5, 1, 1),
                new THREE.Color().setRGB(0.8, 0.6, 1)];
  // Read out all skeletons
  var sks = {};
  ['presynaptic_to', 'postsynaptic_to'].forEach(function(relation, index) {
    $("input[id^='" + relation + "-show-skeleton-" + widgetID + "-']").each(function(i, e) {
      var skid = parseInt(e.value);
      if (!(skid in sks)) sks[skid] = {};
      sks[skid][index] = e.checked;
    });
  });
  // Pick those for which at least one checkbox is checked (if they have more than one)
  Object.keys(sks).forEach(function(skid) {
    var sk = sks[skid];
    if (true === sk[0] || true === sk[1]) {
      var index = -1;
      if (0 in sk) {
        if (1 in sk) index = 2; // exists in both pre and post
        else index = 0;
      } else if (1 in sk) index = 1;
      var name = $('#a-connectivity-table-' + widgetID + '-' + skid).text();
      models[skid] = new SelectionTable.prototype.SkeletonModel(skid, name, colors[index].clone());
    }
  });

  return models;
};

/**
 * Clears the widgets content container.
 */
SkeletonConnectivity.prototype._clearGUI = function() {
  // Clear widget
  $("#connectivity_widget" + this.widgetID).empty();
};

SkeletonConnectivity.prototype.update = function() {
  var skids = Object.keys(this.skeletons);
  if (0 === skids.length) {
    this._clearGUI();
    return;
  };

  var self = this;

  requestQueue.replace(
      django_url + project.id + '/skeleton/connectivity',
      'POST',
      {'source': skids,
       'boolean_op': $('#connectivity_operation' + this.widgetID).val()},
      function(status, text) {
        var handle = function(status, text) {
          if (200 !== status) {
            self.incoming = {};
            self.outgoing = {};
            new ErrorDialog("Couldn't load connectivity information",
                "The server returned an unexpected status code: " +
                    status).show();
            return;
          }
          var json = $.parseJSON(text);
          if (json.error) {
            self.incoming = {};
            self.outgoing = {};
            new ErrorDialog("Couldn't load connectivity information",
                json.error).show();
            return;
          }

          // Save reference of incoming and outgoing nodes. These are needed to open
          // the connectivity plots in a separate widget.
          self.incoming = json.incoming;
          self.outgoing = json.outgoing

          // Register this widget with the name service for all neurons
          var createPartnerModels = function(partners, result) {
            for (var skid in partners) {
              result[skid] = new SelectionTable.prototype.SkeletonModel(skid,
                  partners[skid].name, null);
            }
          };
          var partnerModels = {};
          createPartnerModels(self.incoming, partnerModels);
          createPartnerModels(self.outgoing, partnerModels);

          // Make all partners known to the name service
          neuronNameService.registerAll(self, partnerModels, function() {
            // Create connectivity tables
            self.redraw();
          });
        };

        // Handle result and create tables, if possible
        handle(status, text);
      },
      'update_connectivity_table');
};

SkeletonConnectivity.prototype.redraw = function() {

  // Record the state of checkboxes
  var checkboxes = [{}, {}],
      widgetID = this.widgetID,
      relations = ['presynaptic_to', 'postsynaptic_to'];
  relations.forEach(function(relation, index) {
    $("[id^='" + relation + "-show-skeleton-" + widgetID + "-']").each(function(_, checkbox) {
      checkboxes[index][checkbox.value] = checkbox.checked;
    });
  });

  // Create connectivity tables
  this.createConnectivityTable();

  // Restore checkbox state
  checkboxes.forEach((function(c, i) {
    var relation = relations[i];
    Object.keys(c).forEach((function(skeleton_id) {
      var sel = $('#' + relation + '-show-skeleton-' + this.widgetID + '-' + skeleton_id);
      if (sel.length > 0) {
        sel.attr('checked', c[skeleton_id]);
      }
    }).bind(this));
  }).bind(this));
};

/**
 * This method is called from the neuron name service, if neuron names are
 * changed.
 */
SkeletonConnectivity.prototype.updateNeuronNames = function() {
  //this.update();
  this.redraw();
};

SkeletonConnectivity.prototype.createConnectivityTable = function() {
  // Simplify access to this widget's ID in sub functions
  var widgetID = this.widgetID;
  // Simplify access to pre-bound skeleton source and instance registry methods
  var getLinkTarget = this.getLinkTarget.bind(this);
  var getSkeletonModel = this.getSkeletonModel.bind(this);

  /**
   * Support function for creating a neuron/skeleton name link element in the
   * neuron list and both pre- and postsynaptic tables.
   */
  var createNameElement = function(name, skeleton_id) {
    var a = document.createElement('a');
    a.appendChild(document.createTextNode(neuronNameService.getName(skeleton_id)));
    a.setAttribute('href', '#');
    a.setAttribute('id', 'a-connectivity-table-' + widgetID + '-' + skeleton_id);
    a.onclick = function() {
      TracingTool.goToNearestInNeuronOrSkeleton('skeleton', skeleton_id);
      return false;
    };
    a.onmouseover = onmouseover;
    a.onmouseout = onmouseout;
    a.style.color = 'black';
    a.style.textDecoration = 'none';
    return a;
  };

  /**
   * Support function to updates the layout of the tables.
   */
  var layoutTables = function(sideBySide) {
    incoming.toggleClass('table_container_half', sideBySide);
    incoming.toggleClass('table_container_wide', !sideBySide);
    outgoing.toggleClass('table_container_half', sideBySide);
    outgoing.toggleClass('table_container_wide', !sideBySide);
  };

  /**
   * Helper to get the synpatic count of a skeleton ID dictionary.
   */
  var synaptic_count = function(skids_dict) {
    return Object.keys(skids_dict).reduce(function(sum, skid) {
      return sum + skids_dict[skid];
    }, 0);
  };

  /**
   * Helper to sort an array.
   */
  var to_sorted_array = function(partners) {
    return Object.keys(partners).reduce(function(list, skid) {
      var partner = partners[skid];
      partner['id'] = parseInt(skid);
      partner['synaptic_count'] = synaptic_count(partner.skids);
      list.push(partner);
      return list;
    }, []).sort(function(a, b) {
      return b.synaptic_count - a.synaptic_count;
    });
  };

  var onmouseover = function() { this.style.textDecoration = 'underline'; }
  var onmouseout = function() { this.style.textDecoration = 'none'; };

  /**
   * Support function for selecting a background color based on review state.
   */
  var getBackgroundColor = function(reviewed) {
    if (100 === reviewed) {
      return '#6fff5c';
    } else if (0 === reviewed) {
      return '#ff8c8c';
    } else {
      return '#ffc71d';
    }
  };

  /**
   *  Support function to update the visibility of a neuron in another widget.
   */
  var updateVisibility = function(skid, visible) {
      // Tell all linked widgets about this change or return if there are none
      var linkTarget = getLinkTarget();
      if (!linkTarget) return;

      var model = linkTarget.getSkeletonModel(skid);
      if (visible) {
        if (!model) model = getSkeletonModel(skid);
        else model.setVisible(true);
        linkTarget.updateOneModel(model);
      } else {
        if (model) {
          model.setVisible(false);
          linkTarget.updateOneModel(model);
        }
      }
  };

  /**
   * Support function for creating a partner table.
   */
  var create_table = function(skids, skeletons, thresholds, partners, title, relation,
      hideSingles, reviewFilter, collapsed, collapsedCallback) {
    /**
     * Helper to handle selection of a neuron.
     */
    var set_as_selected = function(ev) {
      var skelid = parseInt( ev.target.value );
      var checked = $('#' + relation + '-show-skeleton-' + widgetID + '-' + skelid).is(':checked');
      // Update the checkbox for the same skeleton on the other table, if any
      var r = {presynaptic_to: 'postsynaptic_to',
               postsynaptic_to: 'presynaptic_to'}[relation];
      $('#' + r + '-show-skeleton-' + widgetID + '-' + skelid).attr('checked', checked);

      // Remember this selection
      this.skeletonSelection[skelid] = checked;

      // Uncheck the select-all checkbox if it is checked and this checkbox is
      // now unchecked
      if (!this.checked) {
        $('#' + title.toLowerCase() + 'stream-selectall' + widgetID + ':checked')
            .prop('checked', false);
      }

      updateVisibility(skelid, checked);
    };

    var table = $('<table />').attr('id', 'incoming_connectivity_table' + widgetID)
            .attr('class', 'partner_table');

    /* The table header will be slightly different if there is more than one
     * neuron currently looked at. In this case, the 'syn count' column will
     * have sub columns for the sum and the respective individual columns. */
    var extraCols = skids.length > 1;
    var headerRows = extraCols ? 2 : 1;

    /**
     * Support function to sum up fields of elemends of an array.
     */
    var getSum = function(elements, field) {
      return elements.reduce(function(sum, e) {
        return sum + e[field];
      }, 0);
    }
    // The total synapse count
    var total_synaptic_count = getSum(partners, 'synaptic_count');
    // The total node count
    var total_node_count = getSum(partners, 'num_nodes');

    /**
     * Support function to accumulate the number of reviews for a set of
     * synaptic partners.
     */
    var getNrReviews = function(synPartners, filter) {
      if (filter) {
        return synPartners.reduce(function(sum, e) {
          var reviews = e.reviewed[filter];
          return reviews ? sum + reviews : sum;
        }, 0);
      } else {
        return synPartners.reduce(function(sum, e) {
          return sum + e.union_reviewed;
        }, 0);
      }
    };
    // The total review count
    var total_reviewed = getNrReviews(partners, reviewFilter);

    // The table header
    var thead = $('<thead />');
    table.append( thead );
    var row = $('<tr />')
    row.append( $('<th />').text(title + "stream neuron").attr('rowspan',
        headerRows));
    row.append( $('<th />').text("syn count").attr('rowspan', 1).attr('colspan',
        extraCols ? skids.length + 1 : 1));
    row.append( $('<th />').text("reviewed").attr('rowspan', headerRows));
    row.append( $('<th />').text("node count").attr('rowspan', headerRows));
    row.append( $('<th />').text("select").attr('rowspan', headerRows));
    thead.append( row );
    if (extraCols) {
      row = $('<tr />');
      row.append( $('<th />').text("Sum").attr('rowspan', '1').attr('colspan', '1'));
      skids.forEach(function(s, i) {
        this.append( $('<th />').text(i+1 + ".").attr('rowspan', '1').attr('colspan', '1'));
      }, row);
      thead.append(row);
    }

    // The aggregate row
    row = $('<tr />');
    var titleClass = collapsed ? "extend-box-closed" : "extend-box-open";
    var titleCell = $('<td />').html('<span class="' + titleClass +
            '"></span>ALL (' + partners.length + 'neurons)')
    row.append(titleCell);
    row.append($('<td />').addClass('syncount').text(total_synaptic_count));
    if (extraCols) {
      skids.forEach(function(skid) {
        var count = partners.reduce(function(sum, partner) {
          return sum + (partner.skids[skid] || 0);
        }, 0)
        this.append($('<td />').addClass('syncount').text(count));
      }, row);
    }
    var average = Math.floor(100 * total_reviewed / total_node_count) | 0;
    row.append( $('<td />').text(average + "%")
        .css('background-color', getBackgroundColor(average)));
    row.append( $('<td />').text(total_node_count));
    var el = $('<input type="checkbox" id="' + title.toLowerCase() + 'stream-selectall' +  widgetID + '" />');
    if (this.selectAllSelection[title.toLowerCase()]) {
      el.attr('checked', 'checked');
    }
    row.append( $('<td />').addClass('input-container').append( el ) );
    thead.append(row);

    var tbody = $('<tbody />');
    table.append( tbody );
    if (collapsed) {
      tbody.css('display', "none");
    }

    // Add handler to first row
    titleCell.click((function(element) {
      return function() {
        var $title = $(this);
        // Toggle visibility of the complete table body
        element.toggle(200, function() {
          // Change open/close indidicator box
          var open_elements = $(".extend-box-open", $title);
          if (open_elements.length > 0) {
            open_elements.attr('class', 'extend-box-closed');
          } else {
            var close_elements = $(".extend-box-closed", $title);
            if (close_elements.length > 0) {
              close_elements.attr('class', 'extend-box-open');
            }
          }
        });
        // Call back, if wanted
        if (collapsedCallback) {
          collapsedCallback();
        }
      };
    })(tbody));

    /**
     * Support function to add a table cell that links to a connector selection,
     * displaying a connector count.
     */
    function createSynapseCountCell(count, partner, skids, title) {
      var td = document.createElement('td');
      td.setAttribute('class', 'syncount');
      // Only add the actual count as text if it is greater zero. This reduces
      // the visual noise for larger tables.
      if (count > 0) {
        var a = document.createElement('a');
        td.appendChild(a);
        a.appendChild(document.createTextNode(count));
        a.setAttribute('href', '#');
        a.style.color = 'black';
        a.style.textDecoration = 'none';
        //showSharedConnectorsFn(partner.id, Object.keys(partner.skids), relation);
        a.onclick = ConnectorSelection.show_shared_connectors.bind(
            ConnectorSelection, partner.id, skids, relation);
        a.onmouseover = function() {
            a.style.textDecoration = 'underline';
            // TODO should show a div with the list of partners, with their names etc.
        };
        a.onmouseout = onmouseout;
        // Create tool-tip
        a.setAttribute('title', title)
      }
      td.setAttribute('title', title)
      return td;
    };

    // Create a table row for every partner and remember the ignored ones
    var filtered = partners.reduce((function(filtered, partner) {
      // Ignore this line if all its synapse counts are below the threshold. If
      // the threshold is 'undefined', false is returned and to semantics of
      // this test.
      var ignore = Object.keys(partner.skids).every(function(skid) {
        // Return true if object is below threshold
        return (partner.skids[skid] || 0) < thresholds[skid];
      });
      ignore = ignore || partner.synaptic_count < thresholds['sum'];
      // Ignore partner if it has only a single node, if requested
      if (hideSingles) {
        ignore = ignore || partner.num_nodes == 1;
      }
      if (ignore) {
        filtered.push(partner);
        return filtered;
      }

      var tr = document.createElement('tr');
      tbody.append(tr);

      // Cell with partner neuron name
      var td = document.createElement('td');
      var a = createNameElement(partner.name, partner.id);
      td.appendChild(a);
      tr.appendChild(td);

      // Cell with synapses with partner neuron
      tr.appendChild(createSynapseCountCell(partner.synaptic_count,
          partner, Object.keys(partner.skids), partner.synaptic_count +
          " synapses for all selected neurons."));
      // Extra columns for individual neurons
      if (extraCols) {
        skids.forEach(function(skid, i) {
          var count = partner.skids[skid] || 0;
          this.appendChild(createSynapseCountCell(count, partner, [skid],
              count + " synapse(s) for neuron '" + skeletons[skid]  + "'."));
        }, tr);
      }

      // Cell with percent reviewed of partner neuron
      var pReviewed = parseInt(Math.floor((100 *
          getNrReviews([partner], reviewFilter) / partner.num_nodes)));
      var td = document.createElement('td');
      td.appendChild(document.createTextNode(pReviewed + "%"));
      td.style.backgroundColor = getBackgroundColor(pReviewed);
      tr.appendChild(td);

      // Cell with number of nodes of partner neuron
      var td = document.createElement('td');
      td.appendChild(document.createTextNode(partner.num_nodes));
      tr.appendChild(td);

      // Cell with checkbox for adding to Selection Table
      var td = document.createElement('td');
      td.setAttribute('class', 'input-container');
      var input = document.createElement('input');
      input.setAttribute('id', relation + '-show-skeleton-' + widgetID + '-' + partner.id);
      input.setAttribute('type', 'checkbox');
      input.setAttribute('value', partner.id);
      input.onclick = set_as_selected.bind(this);<commit_msg>Connectivity widget: show neuron name service name in tooltip<commit_after>/* -*- mode: espresso; espresso-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set softtabstop=2 shiftwidth=2 tabstop=2 expandtab: */

"use strict";

var SkeletonConnectivity = function() {
  this.widgetID = this.registerInstance();
  this.registerSource();
  this.init();
  // Default table layout to be side by side. Have it seperate from init() as
  // long as it is part of the top button row.
  this.tablesSideBySide = true;
};

SkeletonConnectivity.prototype = {};
$.extend(SkeletonConnectivity.prototype, new InstanceRegistry());
$.extend(SkeletonConnectivity.prototype, new SkeletonSource());

/**
 * Initializes the connectivity widget by setting all fields to their default
 * value.
 */
SkeletonConnectivity.prototype.init = function() {
  // An ordered list of neurons/skeletons for display
  this.ordered_skeleton_ids = [];
  // An (per se unordered) object mapping skeletonIDs to skeleton names
  this.skeletons = {};
  // Incoming an outgoing connections of current neurons
  this.incoming = {};
  this.outgoing = {};
  // Default upstream and downstream tables to be not collapsed
  this.upstreamCollapsed = false;
  this.downstreamCollapsed = false;
  // Thresholds for current skeleton set
  this.upThresholds = {};
  this.downThresholds = {};
  // Indicates whether single nodes should be hidden
  this.hideSingleNodePartners = false;
  // ID of the user who is currently reviewing or null for 'union'
  this.reviewFilter = null;
  // An object mapping skeleton IDs to their selection state
  this.skeletonSelection = {};
  // An object for remembering the selection state of the select-all controls
  this.selectAllSelection = {
    'up': false,
    'down': false,
  };
};

/** Appends only to the top list, that is, the set of seed skeletons
 *  for which all pre- and postsynaptic partners are listed. */
SkeletonConnectivity.prototype.append = function(models) {
  var skeletons = this.skeletons,
      added = 0,
      removed = 0,
      widgetID = this.widgetID;
  var new_skeletons = Object.keys(models).reduce(function(o, skid) {
    var model = models[skid];
    if (skid in skeletons) {
      if (model.selected) {
        // Update name
        skeletons[skid] = model.baseName;
        $('#a-connectivity-table-' + widgetID + '-' + skid).html(
            neuronNameService.getName(skid));
      } else {
        // Remove
        delete skeletons[skid];
        ++removed;
      }
    } else {
      if (model.selected) {
        o[skid] = models[skid].baseName;
        ++added;
      }
    }
    return o;
  }, {});

  if (0 === removed && 0 === added) {
    return;
  }
  
  // Update existing ones and add new ones
  $.extend(this.skeletons, new_skeletons);
  for (var skid in new_skeletons) {
    this.ordered_skeleton_ids.push(skid);
  }

  // Add skeletons
  neuronNameService.registerAll(this, models, (function() {
    this.update();
    this.updateLink(models);
  }).bind(this));
};

SkeletonConnectivity.prototype.getName = function() {
  return "Connectivity " + this.widgetID;
};

SkeletonConnectivity.prototype.destroy = function() {
  this.unregisterInstance();
  this.unregisterSource();
  neuronNameService.unregister(this);
};

SkeletonConnectivity.prototype.clear = function(source_chain) {
  this.init();
  this.update();
  this.clearLink(source_chain);
};

SkeletonConnectivity.prototype.removeSkeletons = function(skeleton_ids) {
  skeleton_ids.forEach(function(skid) {
    delete this.skeletons[skid];
    var index = this.ordered_skeleton_ids.indexOf(skid);
    if (index > -1) {
      this.ordered_skeleton_ids.splice(index, 1);
    }
  }, this);
  this.update();
  this.updateLink(this.getSelectedSkeletonModels());
};

SkeletonConnectivity.prototype.hasSkeleton = function(skeleton_id) {
  return skeleton_id in this.skeletons;
};

SkeletonConnectivity.prototype.updateModels = function(models, source_chain) {
  if (source_chain && (this in source_chain)) return; // break propagation loop
  if (!source_chain) source_chain = {};
  source_chain[this] = this;

  this.append(models);
};

SkeletonConnectivity.prototype.highlight = function(skeleton_id) {
  // TODO color the table row in green if present, clear all others
};

SkeletonConnectivity.prototype.getSelectedSkeletons = function() {
  // TODO refactor to avoid unnecessary operations
  return Object.keys(this.getSelectedSkeletonModels());
};

SkeletonConnectivity.prototype.getSkeletonModel = function(skeleton_id) {
  var e_name = $('#a-connectivity-table-' + this.widgetID + '-' + skeleton_id);
  if (0 === e_name.length) return null;
  var name = e_name.text();

  var pre = $("#presynaptic_to-show-skeleton-" + this.widgetID + "-" + skeleton_id);
  var post = $("#postsynaptic_to-show-skeleton-" + this.widgetID + "-" + skeleton_id);

  var color = new THREE.Color();
  if (pre.length > 0) {
    if (post.length > 0) color.setRGB(0.8, 0.6, 1); // both
    else color.setRGB(1, 0.4, 0.4); // pre
  } else if (post.length > 0) color.setRGB(0.5, 1, 1); // post

  var model = new SelectionTable.prototype.SkeletonModel(skeleton_id, name, color);
  model.selected = pre.is(':checked') || post.is(':checked');
  return model;
};

SkeletonConnectivity.prototype.getSelectedSkeletonModels = function() {
  var widgetID = this.widgetID;
  var skeletons = this.skeletons;
  // Read out skeletons from neuron list
  var models = Object.keys(this.skeletons).reduce(function(o, skid) {
    // Test if checked
    var cb = $('input#neuron-selector-' + widgetID + '-' + skid +
        '[type=checkbox]');
    if (cb.is(':checked')) {
      var name = skeletons[skid];
      o[skid] = new SelectionTable.prototype.SkeletonModel(skid,
          skeletons[skid], new THREE.Color().setRGB(1, 1, 0));
    }
    return o;
  }, {});

  var colors = [new THREE.Color().setRGB(1, 0.4, 0.4),
                new THREE.Color().setRGB(0.5, 1, 1),
                new THREE.Color().setRGB(0.8, 0.6, 1)];
  // Read out all skeletons
  var sks = {};
  ['presynaptic_to', 'postsynaptic_to'].forEach(function(relation, index) {
    $("input[id^='" + relation + "-show-skeleton-" + widgetID + "-']").each(function(i, e) {
      var skid = parseInt(e.value);
      if (!(skid in sks)) sks[skid] = {};
      sks[skid][index] = e.checked;
    });
  });
  // Pick those for which at least one checkbox is checked (if they have more than one)
  Object.keys(sks).forEach(function(skid) {
    var sk = sks[skid];
    if (true === sk[0] || true === sk[1]) {
      var index = -1;
      if (0 in sk) {
        if (1 in sk) index = 2; // exists in both pre and post
        else index = 0;
      } else if (1 in sk) index = 1;
      var name = $('#a-connectivity-table-' + widgetID + '-' + skid).text();
      models[skid] = new SelectionTable.prototype.SkeletonModel(skid, name, colors[index].clone());
    }
  });

  return models;
};

/**
 * Clears the widgets content container.
 */
SkeletonConnectivity.prototype._clearGUI = function() {
  // Clear widget
  $("#connectivity_widget" + this.widgetID).empty();
};

SkeletonConnectivity.prototype.update = function() {
  var skids = Object.keys(this.skeletons);
  if (0 === skids.length) {
    this._clearGUI();
    return;
  };

  var self = this;

  requestQueue.replace(
      django_url + project.id + '/skeleton/connectivity',
      'POST',
      {'source': skids,
       'boolean_op': $('#connectivity_operation' + this.widgetID).val()},
      function(status, text) {
        var handle = function(status, text) {
          if (200 !== status) {
            self.incoming = {};
            self.outgoing = {};
            new ErrorDialog("Couldn't load connectivity information",
                "The server returned an unexpected status code: " +
                    status).show();
            return;
          }
          var json = $.parseJSON(text);
          if (json.error) {
            self.incoming = {};
            self.outgoing = {};
            new ErrorDialog("Couldn't load connectivity information",
                json.error).show();
            return;
          }

          // Save reference of incoming and outgoing nodes. These are needed to open
          // the connectivity plots in a separate widget.
          self.incoming = json.incoming;
          self.outgoing = json.outgoing

          // Register this widget with the name service for all neurons
          var createPartnerModels = function(partners, result) {
            for (var skid in partners) {
              result[skid] = new SelectionTable.prototype.SkeletonModel(skid,
                  partners[skid].name, null);
            }
          };
          var partnerModels = {};
          createPartnerModels(self.incoming, partnerModels);
          createPartnerModels(self.outgoing, partnerModels);

          // Make all partners known to the name service
          neuronNameService.registerAll(self, partnerModels, function() {
            // Create connectivity tables
            self.redraw();
          });
        };

        // Handle result and create tables, if possible
        handle(status, text);
      },
      'update_connectivity_table');
};

SkeletonConnectivity.prototype.redraw = function() {

  // Record the state of checkboxes
  var checkboxes = [{}, {}],
      widgetID = this.widgetID,
      relations = ['presynaptic_to', 'postsynaptic_to'];
  relations.forEach(function(relation, index) {
    $("[id^='" + relation + "-show-skeleton-" + widgetID + "-']").each(function(_, checkbox) {
      checkboxes[index][checkbox.value] = checkbox.checked;
    });
  });

  // Create connectivity tables
  this.createConnectivityTable();

  // Restore checkbox state
  checkboxes.forEach((function(c, i) {
    var relation = relations[i];
    Object.keys(c).forEach((function(skeleton_id) {
      var sel = $('#' + relation + '-show-skeleton-' + this.widgetID + '-' + skeleton_id);
      if (sel.length > 0) {
        sel.attr('checked', c[skeleton_id]);
      }
    }).bind(this));
  }).bind(this));
};

/**
 * This method is called from the neuron name service, if neuron names are
 * changed.
 */
SkeletonConnectivity.prototype.updateNeuronNames = function() {
  //this.update();
  this.redraw();
};

SkeletonConnectivity.prototype.createConnectivityTable = function() {
  // Simplify access to this widget's ID in sub functions
  var widgetID = this.widgetID;
  // Simplify access to pre-bound skeleton source and instance registry methods
  var getLinkTarget = this.getLinkTarget.bind(this);
  var getSkeletonModel = this.getSkeletonModel.bind(this);

  /**
   * Support function for creating a neuron/skeleton name link element in the
   * neuron list and both pre- and postsynaptic tables.
   */
  var createNameElement = function(name, skeleton_id) {
    var a = document.createElement('a');
    a.appendChild(document.createTextNode(neuronNameService.getName(skeleton_id)));
    a.setAttribute('href', '#');
    a.setAttribute('id', 'a-connectivity-table-' + widgetID + '-' + skeleton_id);
    a.onclick = function() {
      TracingTool.goToNearestInNeuronOrSkeleton('skeleton', skeleton_id);
      return false;
    };
    a.onmouseover = onmouseover;
    a.onmouseout = onmouseout;
    a.style.color = 'black';
    a.style.textDecoration = 'none';
    return a;
  };

  /**
   * Support function to updates the layout of the tables.
   */
  var layoutTables = function(sideBySide) {
    incoming.toggleClass('table_container_half', sideBySide);
    incoming.toggleClass('table_container_wide', !sideBySide);
    outgoing.toggleClass('table_container_half', sideBySide);
    outgoing.toggleClass('table_container_wide', !sideBySide);
  };

  /**
   * Helper to get the synpatic count of a skeleton ID dictionary.
   */
  var synaptic_count = function(skids_dict) {
    return Object.keys(skids_dict).reduce(function(sum, skid) {
      return sum + skids_dict[skid];
    }, 0);
  };

  /**
   * Helper to sort an array.
   */
  var to_sorted_array = function(partners) {
    return Object.keys(partners).reduce(function(list, skid) {
      var partner = partners[skid];
      partner['id'] = parseInt(skid);
      partner['synaptic_count'] = synaptic_count(partner.skids);
      list.push(partner);
      return list;
    }, []).sort(function(a, b) {
      return b.synaptic_count - a.synaptic_count;
    });
  };

  var onmouseover = function() { this.style.textDecoration = 'underline'; }
  var onmouseout = function() { this.style.textDecoration = 'none'; };

  /**
   * Support function for selecting a background color based on review state.
   */
  var getBackgroundColor = function(reviewed) {
    if (100 === reviewed) {
      return '#6fff5c';
    } else if (0 === reviewed) {
      return '#ff8c8c';
    } else {
      return '#ffc71d';
    }
  };

  /**
   *  Support function to update the visibility of a neuron in another widget.
   */
  var updateVisibility = function(skid, visible) {
      // Tell all linked widgets about this change or return if there are none
      var linkTarget = getLinkTarget();
      if (!linkTarget) return;

      var model = linkTarget.getSkeletonModel(skid);
      if (visible) {
        if (!model) model = getSkeletonModel(skid);
        else model.setVisible(true);
        linkTarget.updateOneModel(model);
      } else {
        if (model) {
          model.setVisible(false);
          linkTarget.updateOneModel(model);
        }
      }
  };

  /**
   * Support function for creating a partner table.
   */
  var create_table = function(skids, skeletons, thresholds, partners, title, relation,
      hideSingles, reviewFilter, collapsed, collapsedCallback) {
    /**
     * Helper to handle selection of a neuron.
     */
    var set_as_selected = function(ev) {
      var skelid = parseInt( ev.target.value );
      var checked = $('#' + relation + '-show-skeleton-' + widgetID + '-' + skelid).is(':checked');
      // Update the checkbox for the same skeleton on the other table, if any
      var r = {presynaptic_to: 'postsynaptic_to',
               postsynaptic_to: 'presynaptic_to'}[relation];
      $('#' + r + '-show-skeleton-' + widgetID + '-' + skelid).attr('checked', checked);

      // Remember this selection
      this.skeletonSelection[skelid] = checked;

      // Uncheck the select-all checkbox if it is checked and this checkbox is
      // now unchecked
      if (!this.checked) {
        $('#' + title.toLowerCase() + 'stream-selectall' + widgetID + ':checked')
            .prop('checked', false);
      }

      updateVisibility(skelid, checked);
    };

    var table = $('<table />').attr('id', 'incoming_connectivity_table' + widgetID)
            .attr('class', 'partner_table');

    /* The table header will be slightly different if there is more than one
     * neuron currently looked at. In this case, the 'syn count' column will
     * have sub columns for the sum and the respective individual columns. */
    var extraCols = skids.length > 1;
    var headerRows = extraCols ? 2 : 1;

    /**
     * Support function to sum up fields of elemends of an array.
     */
    var getSum = function(elements, field) {
      return elements.reduce(function(sum, e) {
        return sum + e[field];
      }, 0);
    }
    // The total synapse count
    var total_synaptic_count = getSum(partners, 'synaptic_count');
    // The total node count
    var total_node_count = getSum(partners, 'num_nodes');

    /**
     * Support function to accumulate the number of reviews for a set of
     * synaptic partners.
     */
    var getNrReviews = function(synPartners, filter) {
      if (filter) {
        return synPartners.reduce(function(sum, e) {
          var reviews = e.reviewed[filter];
          return reviews ? sum + reviews : sum;
        }, 0);
      } else {
        return synPartners.reduce(function(sum, e) {
          return sum + e.union_reviewed;
        }, 0);
      }
    };
    // The total review count
    var total_reviewed = getNrReviews(partners, reviewFilter);

    // The table header
    var thead = $('<thead />');
    table.append( thead );
    var row = $('<tr />')
    row.append( $('<th />').text(title + "stream neuron").attr('rowspan',
        headerRows));
    row.append( $('<th />').text("syn count").attr('rowspan', 1).attr('colspan',
        extraCols ? skids.length + 1 : 1));
    row.append( $('<th />').text("reviewed").attr('rowspan', headerRows));
    row.append( $('<th />').text("node count").attr('rowspan', headerRows));
    row.append( $('<th />').text("select").attr('rowspan', headerRows));
    thead.append( row );
    if (extraCols) {
      row = $('<tr />');
      row.append( $('<th />').text("Sum").attr('rowspan', '1').attr('colspan', '1'));
      skids.forEach(function(s, i) {
        this.append( $('<th />').text(i+1 + ".").attr('rowspan', '1').attr('colspan', '1'));
      }, row);
      thead.append(row);
    }

    // The aggregate row
    row = $('<tr />');
    var titleClass = collapsed ? "extend-box-closed" : "extend-box-open";
    var titleCell = $('<td />').html('<span class="' + titleClass +
            '"></span>ALL (' + partners.length + 'neurons)')
    row.append(titleCell);
    row.append($('<td />').addClass('syncount').text(total_synaptic_count));
    if (extraCols) {
      skids.forEach(function(skid) {
        var count = partners.reduce(function(sum, partner) {
          return sum + (partner.skids[skid] || 0);
        }, 0)
        this.append($('<td />').addClass('syncount').text(count));
      }, row);
    }
    var average = Math.floor(100 * total_reviewed / total_node_count) | 0;
    row.append( $('<td />').text(average + "%")
        .css('background-color', getBackgroundColor(average)));
    row.append( $('<td />').text(total_node_count));
    var el = $('<input type="checkbox" id="' + title.toLowerCase() + 'stream-selectall' +  widgetID + '" />');
    if (this.selectAllSelection[title.toLowerCase()]) {
      el.attr('checked', 'checked');
    }
    row.append( $('<td />').addClass('input-container').append( el ) );
    thead.append(row);

    var tbody = $('<tbody />');
    table.append( tbody );
    if (collapsed) {
      tbody.css('display', "none");
    }

    // Add handler to first row
    titleCell.click((function(element) {
      return function() {
        var $title = $(this);
        // Toggle visibility of the complete table body
        element.toggle(200, function() {
          // Change open/close indidicator box
          var open_elements = $(".extend-box-open", $title);
          if (open_elements.length > 0) {
            open_elements.attr('class', 'extend-box-closed');
          } else {
            var close_elements = $(".extend-box-closed", $title);
            if (close_elements.length > 0) {
              close_elements.attr('class', 'extend-box-open');
            }
          }
        });
        // Call back, if wanted
        if (collapsedCallback) {
          collapsedCallback();
        }
      };
    })(tbody));

    /**
     * Support function to add a table cell that links to a connector selection,
     * displaying a connector count.
     */
    function createSynapseCountCell(count, partner, skids, title) {
      var td = document.createElement('td');
      td.setAttribute('class', 'syncount');
      // Only add the actual count as text if it is greater zero. This reduces
      // the visual noise for larger tables.
      if (count > 0) {
        var a = document.createElement('a');
        td.appendChild(a);
        a.appendChild(document.createTextNode(count));
        a.setAttribute('href', '#');
        a.style.color = 'black';
        a.style.textDecoration = 'none';
        //showSharedConnectorsFn(partner.id, Object.keys(partner.skids), relation);
        a.onclick = ConnectorSelection.show_shared_connectors.bind(
            ConnectorSelection, partner.id, skids, relation);
        a.onmouseover = function() {
            a.style.textDecoration = 'underline';
            // TODO should show a div with the list of partners, with their names etc.
        };
        a.onmouseout = onmouseout;
        // Create tool-tip
        a.setAttribute('title', title)
      }
      td.setAttribute('title', title)
      return td;
    };

    // Create a table row for every partner and remember the ignored ones
    var filtered = partners.reduce((function(filtered, partner) {
      // Ignore this line if all its synapse counts are below the threshold. If
      // the threshold is 'undefined', false is returned and to semantics of
      // this test.
      var ignore = Object.keys(partner.skids).every(function(skid) {
        // Return true if object is below threshold
        return (partner.skids[skid] || 0) < thresholds[skid];
      });
      ignore = ignore || partner.synaptic_count < thresholds['sum'];
      // Ignore partner if it has only a single node, if requested
      if (hideSingles) {
        ignore = ignore || partner.num_nodes == 1;
      }
      if (ignore) {
        filtered.push(partner);
        return filtered;
      }

      var tr = document.createElement('tr');
      tbody.append(tr);

      // Cell with partner neuron name
      var td = document.createElement('td');
      var a = createNameElement(partner.name, partner.id);
      td.appendChild(a);
      tr.appendChild(td);

      // Cell with synapses with partner neuron
      tr.appendChild(createSynapseCountCell(partner.synaptic_count,
          partner, Object.keys(partner.skids), partner.synaptic_count +
          " synapses for all selected neurons."));
      // Extra columns for individual neurons
      if (extraCols) {
        skids.forEach(function(skid, i) {
          var count = partner.skids[skid] || 0;
          this.appendChild(createSynapseCountCell(count, partner, [skid],
              count + " synapse(s) for neuron '" +
              neuronNameService.getName(skid) + "'."));
        }, tr);
      }

      // Cell with percent reviewed of partner neuron
      var pReviewed = parseInt(Math.floor((100 *
          getNrReviews([partner], reviewFilter) / partner.num_nodes)));
      var td = document.createElement('td');
      td.appendChild(document.createTextNode(pReviewed + "%"));
      td.style.backgroundColor = getBackgroundColor(pReviewed);
      tr.appendChild(td);

      // Cell with number of nodes of partner neuron
      var td = document.createElement('td');
      td.appendChild(document.createTextNode(partner.num_nodes));
      tr.appendChild(td);

      // Cell with checkbox for adding to Selection Table
      var td = document.createElement('td');
      td.setAttribute('class', 'input-container');
      var input = document.createElement('input');
      input.setAttribute('id', relation + '-show-skeleton-' + widgetID + '-' + partner.id);
      input.setAttribute('type', 'checkbox');
      input.setAttribute('value', partner.id);
      input.onclick = set_as_selected.bind(this);
{'fraction_non_alphanumeric': 0.1405079600794916, 'fraction_numerical': 0.004455023912996003, 'mean_word_length': 2.7691991110379455, 'pattern_counts': {'":': 0, '<': 51, '<?xml version=': 0, '>': 65, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5009486', 'n_tokens_mistral': 14063, 'n_tokens_neox': 13478, 'n_words': 3443}