Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix a bug with barplot length scaling; abstract and test length-scaling code #309

Merged
merged 5 commits into from
Aug 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 14 additions & 46 deletions empress/support_files/js/empress.js
Original file line number Diff line number Diff line change
Expand Up @@ -1212,7 +1212,6 @@ define([
var maxX = prevLayerMaxX;
var fm2color, colorFMIdx;
var fm2length, lengthFMIdx;
var msg;
// Map feature metadata values to colors, if requested (i.e. if
// layer.colorByFM is true). If not requested, we'll just use the
// layer's default color.
Expand Down Expand Up @@ -1251,7 +1250,7 @@ define([
// name / barplot layer number). This lets us bail out of
// drawing barplots while still keeping the user aware of why
// nothing just got drawn/updated.
msg =
var msg =
"Error with assigning colors in barplot layer " +
layer.num +
": " +
Expand All @@ -1274,51 +1273,20 @@ define([
this._featureMetadataColumns,
layer.scaleLengthByFMField
);
// Taken from ColorViewController.getScaledColors() in Emperor
var split = util.splitNumericValues(sortedUniqueLengthValues);
if (split.numeric.length < 2) {
msg =
"Error with scaling lengths in barplot layer " +
layer.num +
": " +
'the feature metadata field "' +
layer.scaleLengthByFMField +
'" has less than 2 unique numeric values.';
util.toastMsg(msg, 5000);
throw msg;
}
fm2length = {};
// Compute the maximum and minimum values in the field to use to
// scale length by
var nums = _.map(split.numeric, parseFloat);
var valMin = _.min(nums);
var valMax = _.max(nums);
// Compute the value range (based on the min/max values in the
// field) and the length range (based on the min/max length that
// the user has set for this barplot layer)
var valRange = valMax - valMin;
var lengthRange =
layer.scaleLengthByFMMax - layer.scaleLengthByFMMin;
if (lengthRange < 0) {
msg =
"Error with scaling lengths in barplot layer " +
layer.num +
": " +
"Maximum length is greater than minimum length.";
util.toastMsg(msg, 5000);
throw msg;
try {
fm2length = util.assignBarplotLengths(
sortedUniqueLengthValues,
layer.scaleLengthByFMMin,
layer.scaleLengthByFMMax,
layer.num,
layer.scaleLengthByFMField
);
} catch (err) {
// Fail gracefully, similarly to how we handle Colorer errors
// above
util.toastMsg(err.message, 5000);
throw err.message;
}
_.each(split.numeric, function (n) {
var fn = parseFloat(n);
// uses linear interpolation (we could add fancier
// scaling methods in the future as options if desired)
// TODO: verify that this handles negative values properly
// and/or support drawing negative values in the opposite
// direction as positive ones
fm2length[fn] =
((fn - valMin) / valRange) * lengthRange +
layer.scaleLengthByFMMin;
});
}

// Now that we know how to encode each tip's bar, we can finally go
Expand Down
83 changes: 83 additions & 0 deletions empress/support_files/js/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -190,12 +190,95 @@ define(["underscore"], function (_) {
return min;
}

/**
* Produces an Object mapping feature metadata values to barplot lengths.
*
* This code was based on ColorViewController.getScaledColors() in Emperor:
* https://github.com/biocore/emperor/blob/b959aed7ffcb9fa3e4d019c6e93a1af3850564d9/emperor/support_files/js/color-view-controller.js#L398
*
* @param {Array} sortedUniqueValues Array of unique values present in a
* feature metadata field. Should have
* been sorted using util.naturalSort().
* Since these are expected to already be
* *unique*, there shouldn't be any
* duplicate values in this array.
* @param {Number} minLength Minimum length value to use for scaling: the
* minimum numeric value in sortedUniqueValues
* will get assigned this length.
* @param {Number} maxLength Maximum length value to use for scaling; works
* analogously to minLength above.
* @param {Number} layerNum Number of the barplot layer for which these
* scaling computations are being done. This
* will only be used if something goes wrong and
* this function needs to throw an error message.
* @param {String} fieldName Name of the feature metadata field represented
* by sortedUniqueValues. As with layerNum, this
* will only be used if this throws an error
* message.
* @return {Object} fm2length Maps the numeric items in sortedUniqueValues
* to their corresponding barplot lengths.
* Each length is guaranteed to be within the
* inclusive range [minLength, maxLength].
*/
function assignBarplotLengths(
sortedUniqueValues,
minLength,
maxLength,
layerNum,
fieldName
) {
var split = splitNumericValues(sortedUniqueValues);
if (split.numeric.length < 2) {
throw new Error(
"Error with scaling lengths in barplot layer " +
layerNum +
': the feature metadata field "' +
fieldName +
'" has less than 2 unique numeric values.'
);
}
fm2length = {};
// Compute the maximum and minimum values in the field to use to
// scale length by
var nums = _.map(split.numeric, parseFloat);
var valMin = _.min(nums);
var valMax = _.max(nums);
// Compute the value range (based on the min/max values in the
// field) and the length range (based on the min/max length that
// the user has set for this barplot layer)
var valRange = valMax - valMin;
var lengthRange = maxLength - minLength;
if (lengthRange < 0) {
throw new Error(
"Error with scaling lengths in barplot layer " +
layerNum +
": Maximum length is greater than minimum length."
);
}
_.each(split.numeric, function (n) {
// uses linear interpolation (we could add fancier
// scaling methods in the future as options if desired)
//
// NOTE: we purposefully use the original feature metadata value
// (i.e. n) as the key in fm2length, not parseFloat(n). This is
// because parseFloat(n) can have a different string representation
// than n, so using parseFloat(n) as a key would make these lengths
// unretrievable without calling parseFloat() multiple times. (An
// example of this is the metadata value "0.0", which parseFloat()
// converts to 0.)
fm2length[n] =
((parseFloat(n) - valMin) / valRange) * lengthRange + minLength;
});
return fm2length;
}

return {
keepUniqueKeys: keepUniqueKeys,
naturalSort: naturalSort,
splitNumericValues: splitNumericValues,
isValidNumber: isValidNumber,
parseAndValidateNum: parseAndValidateNum,
toastMsg: toastMsg,
assignBarplotLengths: assignBarplotLengths,
};
});
69 changes: 68 additions & 1 deletion tests/test-util.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require(["jquery", "util"], function ($, util) {
require(["jquery", "underscore", "util"], function ($, _, util) {
$(document).ready(function () {
module("Utilities");
/**
Expand Down Expand Up @@ -305,5 +305,72 @@ require(["jquery", "util"], function ($, util) {
deepEqual(n, 1);
deepEqual(tni.value, "1");
});
test("Test assignBarplotLengths", function () {
var fm2length = util.assignBarplotLengths(
["1", "2", "3", "4"],
0,
1,
100,
"testField"
);
deepEqual(_.keys(fm2length).length, 4);
deepEqual(fm2length["1"], 0);
deepEqual(fm2length["2"], 1 / 3);
deepEqual(fm2length["3"], 2 / 3);
deepEqual(fm2length["4"], 1);
});
test("Test assignBarplotLengths (negative values)", function () {
var fm2length = util.assignBarplotLengths(
["-1", "-2", "-3", "-4"],
0,
1,
100,
"testField"
);
deepEqual(_.keys(fm2length).length, 4);
deepEqual(fm2length["-4"], 0);
deepEqual(fm2length["-3"], 1 / 3);
deepEqual(fm2length["-2"], 2 / 3);
deepEqual(fm2length["-1"], 1);
// Check that mixed negative / positive values are handled normally
var o = util.assignBarplotLengths(["1", "0", "-1"], 1, 5, 1, "t");
deepEqual(_.keys(o).length, 3);
deepEqual(o["-1"], 1);
deepEqual(o["0"], 3);
deepEqual(o["1"], 5);
});
test("Test assignBarplotLengths (non-numeric field error)", function () {
throws(function () {
util.assignBarplotLengths(["1"], 0, 1, 100, "testField");
}, /Error with scaling lengths in barplot layer 100: the feature metadata field "testField" has less than 2 unique numeric values./);
throws(function () {
util.assignBarplotLengths(
["abc", "def", "ghi"],
0,
1,
3,
"fie fi fo fum"
);
}, /Error with scaling lengths in barplot layer 3: the feature metadata field "fie fi fo fum" has less than 2 unique numeric values./);
throws(function () {
util.assignBarplotLengths([], 0, 1, 1, "asdf");
}, /Error with scaling lengths in barplot layer 1: the feature metadata field "asdf" has less than 2 unique numeric values./);
// Check that if both this error AND the max < min error are
// triggered, that this error has precedence. As with various other
// places in the code, the actual precedence doesn't matter too
// much; the main thing we're verifying here is that both errors
// happening don't somehow "cancel out". Because ... that'd be bad.
throws(function () {
util.assignBarplotLengths(["1"], 1, 0, 100, "funkyField");
}, /Error with scaling lengths in barplot layer 100: the feature metadata field "funkyField" has less than 2 unique numeric values./);
});
test("Test assignBarplotLengths (max len < min len error)", function () {
throws(function () {
util.assignBarplotLengths(["1", "2"], 1, 0, 5, "field");
}, /Error with scaling lengths in barplot layer 5: Maximum length is greater than minimum length./);
throws(function () {
util.assignBarplotLengths(["1", "2"], 10, 9.9999, 6, "field");
}, /Error with scaling lengths in barplot layer 6: Maximum length is greater than minimum length./);
});
});
});