-
Notifications
You must be signed in to change notification settings - Fork 57
Scripting
CATMAID is written in javascript on the client side. The whole of CATMAID is available at your fingertips if only you knew what to type into the javascript console.
In Google Chrome, push shift+control+j (shift+command+j in MacOSX) or go to the menu "Tools - Javascript Console".
Every widget is an object, and the prototype of that object has an instances
array. For example, open a Selection Table by clicking on its icon that looks like this: ['S']
... and then open the Javascript Console, and type:
var tables = SelectionTable.prototype.getInstances();
The tables
array should contain one single entry:
var st = tables[0];
The SkeletonSource
is an Object that provides an interface for adding and getting skeletons. All widgets that can list and deliver skeletons extend SkeletonSource
, and will be listed, when open, in the pulldown menu next to the "Append" button of every widget.
Each skeleton is represented by a SkeletonModel
object, which is a simple object holding fields for the ID and its selected state, with separate fields for the visibility of presynaptic sites, postsynaptic sites, meta information like "uncertain end", low-confidence edges etc., for skeleton text tags, and for the skeleton as a whole.
Well-implemented widgets return copies of the SkeletonModel
internally representing each skeleton in that widget. To alter the model, you'll have to append it back to the widget. Here is how:
Following from the SelectionTable
example, now manually add skeletons to it by selecting them in the canvas (click on an existing skeleton node) and then pushing the "Append" button of the Selection Table widget. Then we can get the SkeletonModel instances representing each Skeleton, in this case just one:
var st = SelectionTable.prototype.getInstances()[0];
var models = st.getSkeletonModels();
Here, models
is an Object with skeleton ID as keys and SkeletonModel
instances as values. To change the visibility of all the skeletons we will change the selected
field of each, along with all other fields, via the method setVisible
:
Object.keys(models).forEach(function(skeleton_id) {
models[skeleton_id].setVisible(false);
};
To update the SelectionTable, the modified models must be appended back, which won't duplicate them: will simply read the new values of the updated ones and append new ones if any:
st.append(models);
Now the checkbox of the listed neurons in the Selection Table should have been unticked.
Assuming you have a list of neurons in a Selection widget, and at least a subset of them is selected (their checkboxes are ticked):
// Assuming it is the first one opened:
var st = SelectionTable.prototype.getInstances()[0];
// List of skeleton IDs
var skids = st.getSelectedSkeletons();
// Pick one skeleton at random:
var skid = skids[Math.floor(Math.random() * skids.length)];
// Select the skeleton in the canvas
TracingTool.goToNearestInNeuronOrSkeleton("skeleton", skid);
If the Selection Table is linked to a 3D Viewer (as is the default when the 3D Viewer is open), then this will hide all neurons except the one selected at random:
var selectAtRandom = function() {
var st = SelectionTable.prototype.getInstances()[0];
var models = st.getSkeletonModels();
var skids = Object.keys(models);
var skid = skids[Math.floor(Math.random() * skids.length)];
console.log("Picking: ", skid);
TracingTool.goToNearestInNeuronOrSkeleton("skeleton", skid);
// Leave selected only skid
Object.keys(models).forEach(function(id) {
models[id].setVisible(id == skid);
});
st.append(models);
};
selectAtRandom();
Given the annotation as text, we obtain first its ID from the local cache (which is loaded on startup), and then request from the server the list of neurons along with their associated skeleton IDs:
var selectAtRandom = function(annotation) {
if (!annotation) return alert("What annotation?");
var annot = annotations.annotation_ids[annotation];
if (!annot) return alert("Invalid annotation");
var request = function(url, post, callback) {
requestQueue.register(
django_url + project.id + url, "POST", post,
function(status, text) {
if (200 !== status) return;
var json = $.parseJSON(text);
if (json.error) return alert(json.error);
callback(json);
});
};
request(
"/neuron/query-by-annotations",
{neuron_query_by_annotation: [annot]},
function(json) {
var skids = json.entities.map(function(e) {
return e.skeleton_ids[0];
});
var skid = skids[Math.floor(Math.random() * skids.length)];
console.log("Picking: ", skid);
TracingTool.goToNearestInNeuronOrSkeleton("skeleton", skid);
});
};
To print for each 3D viewer a CSV table with the root node position for every skeleton ID, the following function can be used:
function printRootPositions() {
var viewers = WebGLApplication.prototype.instances;
for (var wid in viewers) {
var w = viewers[wid];
console.log("Root nodes for skeletons in widget '" + w.getName() + "'");
console.log(["Skeleton ID", "Root X", "Root Y", "Root Z"].join(", "));
for (var sid in w.space.content.skeletons) {
var skeleton = w.space.content.skeletons[sid];
var arbor = skeleton.createArbor();
var pos = skeleton.getPositions()[arbor.root];
console.log([skeleton.id, pos.x, pos.y, pos.z].join(", "));
}
}
}
The "run" function fetches the statistics of each skeleton in a SelectionTable and then sorts them. In the example below we use the "n_post" value in the sorter function, descending, but it could be any value, including e.g. number of nodes contributed by a specific user.
The fetch for statistics returns a JSON object with the data displayed in a dialog when pushing the "Info" button in a Selection Table. Open first the javascript console, push the "Info" button for a neuron, and then go to "Network" in the console, click on the row showing the last call (named "contributor_statistics"), and look into what JSON data was returned in the "Preview". It will have values like "n_pre", "n_post", and an object like "node_contributors" that is a map of user ID vs number of skeleton nodes done by that user; or "post_contributors" which is a map of user ID vs number of postsynaptic relations with the skeleton made by that user. Any of these values is accessible below from the "sorter" function.
(To find out the ID of a specific user, type "Users.all()" in the console and look for it by expanding each user. If too many, write a loop over all users to match by e.g. username.)
var run = function() {
// Selection Table to be sorted
var st = SelectionTable.prototype.getInstances()[0];
// Copies of SkeletonModel instances and skeleton IDs in the table
var models = st.getSelectedSkeletonModels();
var skids = Object.keys(models).map(Number);
// Array to accumulate results in
var skid_json = [];
// Function to sort the skeletons according to e.g. n_post,
// aka the number of postsynaptic sites:
var sorter = function(a, b) {
var an = a.json.n_post;
var bn = b.json.n_post;
return an === bn ? 0 : (an < bn ? 1 : -1);
};
// Function to sort the table when having fetch all the data
var sortTable = function() {
// Sort fetched data
skid_json.sort(sorter);
// Recreate, sorted, the data structures of the SelectionTable
var ids = {};
var skeletons = [];
for (var i=0; i<skid_json.length; ++i) {
var skid = skid_json[i].skid;
ids[skid] = i;
skeletons.push(models[skid]);
}
// Replace internal data structures
st.skeletons = skeletons;
st.skeleton_ids = ids;
st.gui.update();
};
// Function to asynchronously fetch statistics for one skeleton from the server
var fetch = function(skid, callback) {
requestQueue.register(django_url + project.id + '/skeleton/' + skid + '/contributor_statistics', 'POST', {}, function(status, text) {
if (200 !== status) return;
var json = $.parseJSON(text);
if (json.error) return console.log(skid, json.error);
callback(skid, json);
});
};
// Callback function: accumulate fetched data into skid_json
var pusher = function(skid, json) {
skid_json.push({skid: skid, json: json});
// Sort the table when done
if (skid_json.length === skids.length) {
sortTable();
}
};
// Fetch statistics for each skeleton
skids.forEach(function(skid) {
console.log("fetching " + skid);
fetch(skid, pusher);
});
};
run();
The 3D Viewer is coded by the WebGLApplication prototype. The top fields of an instance include the "space" that holds onto the "content", the latter including the map of skeleton IDs vs instances of WebGLApplication.prototype.Skeleton.
A "Skeleton" is an object that holds the geometry (vertices, lines, spheres) and material (color, etc.) of the skeleton that represents a neuron in the 3D Viewer.
One of the members of a Skeleton is the "actor", an object that contains the three kinds of THREE.Line geometry objects: the "neurite" (the cable), the "postsynaptic_to" (a collection of cyan lines, one for each postsynaptic relation), and the "presynaptic_to" (a collection of red lines, one for each presynaptic relation). For the later two, each line starts at a skeleton node and ends at the coordinates of the connector that represents the synapse.
Each of these "Line" is an object (THREE.Line) that extends THREE.Geometry, containing members such as "visible" (used below) and "vertices" (the list of pairs of Vector3 instances, each representing a point in space in calibrated coordinates).
Below, we acquire the first instance of a 3D Viewer, declare a variable "sks" that points to its table of skeleton IDs vs Skeleton instances, and then iterate the latter to set the visibility of its postsynaptic lines to false, hiding them:
var w = WebGLApplication.prototype.getFirstInstance();
var sks = w.space.content.skeletons;
Object.keys(sks).forEach(function(skid) {
sks[skid].actor.postsynaptic_to.visible = false;
});
w.space.render();
Given that the geometry is in calibrated coordinates (albeit translated to conform with the coordinate system in WebGL, that has its origin of coordinates at top left), we can retrieve the position of all skeleton nodes by directly reading them from the vertices of the Line object that defines its skeleton. Remember that the Line object is an array of Vector3, where each pair of consecutive Vector3 instances defines an edge of the skeleton (so there's lots of repeats).
var w = WebGLApplication.prototype.getFirstInstance();
var sks = w.space.content.skeletons;
// The selected skeleton in the 2D canvas
var skid = SkeletonAnnotations.getActiveSkeletonId();
var sk = sks[skid];
// Equivalent to the function sk.getPositions(), expanded here for illustration:
var positions = {};
sk.geometry.neurite.vertices.forEach(function(v) {
positions[v.node_id] = v;
});
Remember, these positions are correct in relative terms, but they are translated. To undo the translation, apply the function "w.space.coordsToUnscaledSpace2" to each "V" (a Vector3).
For measurements, though, the translation is irrelevant. For example, to measure the cable length of an arbor you could do:
// An instance of Arbor as defined in Arbor.js
var arbor = sk.createArbor();
var cable = arbor.cableLength(sk.getPositions());
The value will be identical to that obtained by the "Measure" button in a Selection widget.
For creative renderings of the 3D Viewer it is useful to know how to modify the transparency of e.g. the spheres that represent the somas of the neurons. The "radiusVolumes" object of the Skeleton instance contains all spheres that result from a skeleton node having a radius larger than zero (and its neighboring nodes not having it over zero, otherwise it would be defined as a cylinder), and these include the somas. The spheres are keyed by skeleton treenode ID.
Here is a script to hide all spheres, including the somas. The spheres for pre- and postsynaptic locations are not included in the "radiusVolumes" but in the "synapticSpheres" object, so the same approach would work for hiding/showing those instead:
var sks = WebGLApplication.prototype.getFirstInstance().space.content.skeletons;
Object.keys(sks).forEach(function(skid) {
var sk = sks[skid];
Object.keys(sk.radiusVolumes).forEach(function(node_id) {
var sphere = sk.radiusVolumes[node_id];
sphere.visible = false;
});
});
If instead of hiding the spheres the goal is to make them transparent, replace this:
sphere.visible = false;
with this:
sphere.material.transparent = true;
sphere.material.opacity = 0.4;
Note that the "material" contains other properties such as "color", which is an instance of THREE.Color.
And finally don't forget to render: either rotate the view with the mouse (or zoom, etc.), or run this:
w.space.render();
Select one or more nodes in the Graph widget by clicking on it (shift-click to select more than one, or click and drag to draw a box that selects multiple nodes). Then run the following function:
var selectConnected = function(min_in_synapses, min_out_synapses) {
var gg = GroupGraph.prototype.getFirstInstance();
var selected = gg.getSelectedSkeletonModels();
var skids = Object.keys(selected);
if (0 === skids.length) return alert("None selected!");
// For each selected node
skids.forEach(function(skid) {
var node = gg.cy.nodes('#' + skid);
var neighbors = node.neighborhood().nodes().toArray();
// For each neighbor of a selected node
neighbors.forEach(function(neighbor) {
var to = neighbor.edgesTo(node);
if (to && to.data('weight') >= min_in_synapses) {
neighbor.select();
}
var from = node.edgesTo(neighbor);
if (from && from.data('weight') >= min_out_synapses) {
neighbor.select();
}
});
});
};
// Select upstream nodes that make 10 or more synapses onto selected nodes
selectConnected(10, 0);