diff --git a/CHANGELOG.md b/CHANGELOG.md index 9317bd8166..32670496e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,14 @@ Data views: - Stacks are now explicitly returned and rendered in order they are linked to projects (projects/ API). +Measurement widget: + +- A new column is added, which shows the estimated volume based on the available + radius information. The volume is computed by simply adding all volumes of the + cylinders and cones formed around each edge. To make interpretation of that + number easier, a percentage is shown as well for each entry that reflects how + many nodes of the respective skeleton have a radius assigned. + Miscellaneous: - The built-in API docs (/apis endpoint) now supports deep links, which allows diff --git a/django/applications/catmaid/static/js/widgets/measurements-table.js b/django/applications/catmaid/static/js/widgets/measurements-table.js index 8413e05cd6..9b2e76c3e3 100644 --- a/django/applications/catmaid/static/js/widgets/measurements-table.js +++ b/django/applications/catmaid/static/js/widgets/measurements-table.js @@ -45,7 +45,7 @@ let labels = ['Neuron', 'Skeleton', 'Raw cable (nm)', 'Smooth cable (nm)', 'Lower-bound cable (nm)', 'N inputs', 'N outputs', 'N presynaptic sites', 'N nodes', - 'N branch nodes', 'N end nodes']; + 'N branch nodes', 'N end nodes', 'Est. radius volume (nm^3)']; if (forceAll || (this.applyFilterRules && this.filterRules.length > 0)) { labels.splice(2, 0, 'Fragment start', 'Fragment'); @@ -229,6 +229,7 @@ let ap = new CATMAID.ArborParser().init('compact-arbor', json), arbor = ap.arbor, positions = ap.positions, + radii = ap.radii, api = models[skid].api, name = CATMAID.NeuronNameService.getInstance(api).getName(skid); @@ -244,6 +245,7 @@ let fractionArborParser = new CATMAID.ArborParser(); fractionArborParser.arbor = fractionArbor; fractionArborParser.positions = positions; + fractionArborParser.radii = radii; fractionArborParser.synapses(json[1], true); let raw_cable = Math.round(fractionArbor.cableLength(positions)) | 0, @@ -255,12 +257,14 @@ n_nodes = fractionArbor.countNodes(), be = fractionArbor.findBranchAndEndNodes(), n_branching = be.n_branches, - n_ends = be.ends.length; + n_ends = be.ends.length, + radius_volume = fractionArbor.estimateRadiusVolume(positions, radii), + radius_ratio = Math.floor(100.0 * fractionArbor.radiusRatio(radii)); let fragmentRow = [SkeletonMeasurementsTable.prototype._makeStringLink(name, skid), skid, fractionArbor.root, `${i+1}/${fractions.length}`, raw_cable, smooth_cable, lower_bound_cable, n_inputs, n_outputs, - n_presynaptic_sites, n_nodes, n_branching, n_ends]; + n_presynaptic_sites, n_nodes, n_branching, n_ends, {volume: radius_volume, ratio: radius_ratio}]; if (self.aggregateFragments) { if (aggRow) { aggRow[2].push(fragmentRow[2]); @@ -301,11 +305,14 @@ n_nodes = arbor.countNodes(), be = arbor.findBranchAndEndNodes(), n_branching = be.n_branches, - n_ends = be.ends.length; + n_ends = be.ends.length, + radius_volume = arbor.estimateRadiusVolume(positions, radii), + radius_ratio = Math.floor(100.0 * arbor.radiusRatio(radii)); rows.push([SkeletonMeasurementsTable.prototype._makeStringLink(name, skid), skid, arbor.root, '1/1', raw_cable, smooth_cable, lower_bound_cable, - n_inputs, n_outputs, n_presynaptic_sites, n_nodes, n_branching, n_ends]); + n_inputs, n_outputs, n_presynaptic_sites, n_nodes, + n_branching, n_ends, {volume: radius_volume, ratio: radius_ratio}]); } } }, @@ -439,7 +446,8 @@ let labels = this.getLabels(true); let self = this; - this.table = $('table#skeleton_measurements_table' + this.widgetID).DataTable({ + const tableId = 'table#skeleton_measurements_table' + this.widgetID; + this.table = $(tableId).DataTable({ destroy: true, dom: '<"H"lr>t<"F"ip>', processing: true, @@ -470,10 +478,21 @@ return `${data}`; } }, + }, { + targets: -1, + render: function(data, type, row, meta) { + return `${Math.round(data.volume)} (${data.ratio}%)`; + }, }], createdRow: function(row, data, index) { row.dataset.skeletonId = data[1]; - } + }, + initComplete: function(settings) { + let headers = $(tableId).find('thead th'); + headers[headers.length - 1].title = 'The estimated volume in nm^3 based on the sum of all ' + + 'cylinders/cones formed by all skeleton edges with their node radii. The percentage shows how ' + + 'many nodes have a radius attached'; + }, }) .on('draw.dt', (function() { this.highlightActiveSkeleton(); diff --git a/django/applications/catmaid/static/libs/catmaid/Arbor.js b/django/applications/catmaid/static/libs/catmaid/Arbor.js index edd93375cb..b411f9509a 100644 --- a/django/applications/catmaid/static/libs/catmaid/Arbor.js +++ b/django/applications/catmaid/static/libs/catmaid/Arbor.js @@ -2334,3 +2334,33 @@ Arbor.prototype.collapseArtifactualBranches = function(tags, callback) { } } }; + +/** + * Sum the volume of all cylinders/cones between nodes of an arbor. + */ +Arbor.prototype.estimateRadiusVolume = function(positions, radii) { + var children = this.childrenArray(), + sum = 0; + for (var i=0; i 0 ? 1 : 0; + for (var i=0; i 0 ? 1 : 0; + } + return radiusCount / (1 + children.length); +}; diff --git a/django/applications/catmaid/static/libs/catmaid/arbor_parser.js b/django/applications/catmaid/static/libs/catmaid/arbor_parser.js index b927bf53a0..8bf25916e0 100644 --- a/django/applications/catmaid/static/libs/catmaid/arbor_parser.js +++ b/django/applications/catmaid/static/libs/catmaid/arbor_parser.js @@ -37,7 +37,8 @@ ArborParser.prototype.tree = function(rows) { var arbor = new Arbor(), - positions = {}; + positions = {}, + radii = {}; for (var i=0; i