diff --git a/empress/support_files/js/barplot-layer.js b/empress/support_files/js/barplot-layer.js index f0c0119e..cc73d7f9 100644 --- a/empress/support_files/js/barplot-layer.js +++ b/empress/support_files/js/barplot-layer.js @@ -4,8 +4,9 @@ define([ "spectrum", "Colorer", "Legend", + "ColorOptionsHandler", "util", -], function ($, _, spectrum, Colorer, Legend, util) { +], function ($, _, spectrum, Colorer, Legend, ColorOptionsHandler, util) { /** * * @class BarplotLayer @@ -96,6 +97,9 @@ define([ this.colorByFMColorMap = null; this.colorByFMColorReverse = false; this.colorByFMContinuous = false; + this.colorByFMContinuousManualScale = false; + this.colorByFMContinuousMin = null; + this.colorByFMContinuousMax = null; this.colorByFMColorMapDiscrete = true; this.defaultLength = BarplotLayer.DEFAULT_LENGTH; this.scaleLengthByFM = false; @@ -295,79 +299,25 @@ define([ colorDetailsDiv.classList.add("indented"); colorDetailsDiv.classList.add("hidden"); - // Add a row for choosing the color map - var colormapP = colorDetailsDiv.appendChild( - document.createElement("p") - ); - var colormapLbl = colormapP.appendChild( - document.createElement("label") - ); - colormapLbl.innerText = "Color Map"; - var colormapSC = colormapP.appendChild(document.createElement("label")); - colormapSC.classList.add("select-container"); - var colormapSelector = document.createElement("select"); - Colorer.addColorsToSelect(colormapSelector); - colormapSC.appendChild(colormapSelector); - colormapSelector.id = - "barplot-layer-" + this.uniqueNum + "-fm-colormap"; - colormapLbl.setAttribute("for", colormapSelector.id); - - // Add a row for choosing whether the color scale should - // be reversed - var reverseColormapP = colorDetailsDiv.appendChild( - document.createElement("p") - ); - var reverseColormapLbl = reverseColormapP.appendChild( - document.createElement("label") - ); - reverseColormapLbl.innerText = "Reverse Color Map"; - var reverseColormapCheckbox = reverseColormapP.appendChild( - document.createElement("input") - ); - reverseColormapCheckbox.id = - "barplot-layer-" + this.uniqueNum + "-fmcolor-reverse-chk"; - reverseColormapCheckbox.setAttribute("type", "checkbox"); - reverseColormapCheckbox.classList.add("empress-input"); - reverseColormapLbl.setAttribute("for", reverseColormapCheckbox.id); - - // Add a row for choosing the scale type (i.e. whether to use - // continuous coloring or not) - // This mimics Emperor's "Continuous values" checkbox - var continuousValP = colorDetailsDiv.appendChild( - document.createElement("p") - ); - var continuousValLbl = continuousValP.appendChild( - document.createElement("label") - ); - continuousValLbl.innerText = "Continuous values?"; - var continuousValCheckbox = continuousValP.appendChild( - document.createElement("input") - ); - continuousValCheckbox.id = - "barplot-layer-" + this.uniqueNum + "-fmcolor-continuous-chk"; - continuousValCheckbox.setAttribute("type", "checkbox"); - continuousValCheckbox.classList.add("empress-input"); - continuousValLbl.setAttribute("for", continuousValCheckbox.id); - // Hide the "Continuous values?" stuff by default, since the default - // colormap is discrete - continuousValP.classList.add("hidden"); + var fColorOptions = new ColorOptionsHandler(colorDetailsDiv, true); // Initialize defaults to match the UI defaults (e.g. the default // feature metadata field for coloring is the first in the selector) + var colorOptions = fColorOptions.getOptions(); this.colorByFMField = chgColorFMFieldSelector.value; - this.colorByFMColorMap = colormapSelector.value; - this.colorByFMColorReverse = reverseColormapCheckbox.checked; + this.colorByFMColorMap = colorOptions.color; + this.colorByFMColorReverse = colorOptions.reverse; // Alter visibility of the color-changing details when the "Color // by..." checkbox is clicked $(chgColorCheckbox).change(function () { + colorOptions = fColorOptions.getOptions(); if (chgColorCheckbox.checked) { colorDetailsDiv.classList.remove("hidden"); chgColorFMFieldSelector.disabled = false; scope.colorByFM = true; scope.colorByFMField = chgColorFMFieldSelector.value; - scope.colorByFMColorMap = colormapSelector.value; - scope.colorByFMColorReverse = reverseColormapCheckbox.checked; - scope.colorByFMContinuous = continuousValCheckbox.checked; + scope.colorByFMColorMap = colorOptions.color; + scope.colorByFMColorReverse = colorOptions.reverse; // Hide the default color row (since default colors // aren't used when f.m. coloring is enabled) dfltColorP.classList.add("hidden"); @@ -386,24 +336,19 @@ define([ $(chgColorFMFieldSelector).change(function () { scope.colorByFMField = chgColorFMFieldSelector.value; }); - $(colormapSelector).change(function () { - scope.colorByFMColorMap = colormapSelector.value; - // Hide the "Continuous values?" row based on the selected - // colormap's type. This matches how Emperor's ColorViewController - // hides/shows its "Continuous values" elements. - if (Colorer.isColorMapDiscrete(scope.colorByFMColorMap)) { - continuousValP.classList.add("hidden"); - scope.colorByFMColorMapDiscrete = true; - } else { - continuousValP.classList.remove("hidden"); - scope.colorByFMColorMapDiscrete = false; - } - }); - $(reverseColormapCheckbox).change(function () { - scope.colorByFMColorReverse = reverseColormapCheckbox.checked; - }); - $(continuousValCheckbox).change(function () { - scope.colorByFMContinuous = continuousValCheckbox.checked; + + // register color options + fColorOptions.registerObserver({ + colorOptionsUpdate: function (options) { + scope.colorByFMColorMap = options.color; + scope.colorByFMColorReverse = options.reverse; + scope.colorByFMColorMapDiscrete = !options.continuousColoring; + scope.colorByFMContinuous = options.continuousColoring; + scope.colorByFMContinuousManualScale = + options.continuousManualScale; + scope.colorByFMContinuousMin = options.min; + scope.colorByFMContinuousMax = options.max; + }, }); // create default length settings @@ -561,39 +506,7 @@ define([ chgFieldLbl.setAttribute("for", chgFieldSMFieldSelector.id); chgFieldSC.appendChild(chgFieldSMFieldSelector); - // Add a row for choosing the color map - var colormapP = this.smDiv.appendChild(document.createElement("p")); - var colormapLbl = colormapP.appendChild( - document.createElement("label") - ); - colormapLbl.innerText = "Color Map"; - var colormapSC = colormapP.appendChild(document.createElement("label")); - colormapSC.classList.add("select-container"); - var colormapSelector = document.createElement("select"); - Colorer.addColorsToSelect(colormapSelector); - colormapSC.appendChild(colormapSelector); - colormapSelector.id = - "barplot-layer-" + this.uniqueNum + "-sm-colormap"; - colormapLbl.setAttribute("for", colormapSelector.id); - - // Add a row for choosing whether the color scale should - // be reversed - var reverseColormapP = this.smDiv.appendChild( - document.createElement("p") - ); - var reverseColormapLbl = reverseColormapP.appendChild( - document.createElement("label") - ); - reverseColormapLbl.innerText = "Reverse Color Map"; - var reverseColormapCheckbox = reverseColormapP.appendChild( - document.createElement("input") - ); - reverseColormapCheckbox.id = - "barplot-layer-" + this.uniqueNum + "-smcolor-reverse-chk"; - reverseColormapCheckbox.setAttribute("type", "checkbox"); - reverseColormapCheckbox.classList.add("empress-input"); - reverseColormapLbl.setAttribute("for", reverseColormapCheckbox.id); - + var sColorOptions = new ColorOptionsHandler(this.smDiv); var lenP = this.smDiv.appendChild(document.createElement("p")); var lenLbl = lenP.appendChild(document.createElement("label")); lenLbl.innerText = "Length"; @@ -606,18 +519,21 @@ define([ lenLbl.setAttribute("for", lenInput.id); // TODO initialize defaults more sanely + var options = sColorOptions.getOptions(); this.colorBySMField = chgFieldSMFieldSelector.value; - this.colorBySMColorMap = colormapSelector.value; - this.colorBySMColorReverse = reverseColormapCheckbox.checked; + this.colorBySMColorMap = options.color; + this.colorBySMColorReverse = options.reverse; $(chgFieldSMFieldSelector).change(function () { scope.colorBySMField = chgFieldSMFieldSelector.value; }); - $(colormapSelector).change(function () { - scope.colorBySMColorMap = colormapSelector.value; - }); - $(reverseColormapCheckbox).change(function () { - scope.colorBySMColorReverse = reverseColormapCheckbox.checked; + sColorOptions.registerObserver({ + colorOptionsUpdate: () => { + options = sColorOptions.getOptions(); + scope.colorBySMColorMap = options.color; + scope.colorBySMColorReverse = options.reverse; + }, }); + $(lenInput).change(function () { scope.lengthSM = util.parseAndValidateNum( lenInput, diff --git a/empress/support_files/js/color-options-handler.js b/empress/support_files/js/color-options-handler.js new file mode 100644 index 00000000..c9e06a3b --- /dev/null +++ b/empress/support_files/js/color-options-handler.js @@ -0,0 +1,275 @@ +define(["underscore", "Colorer", "util"], function (_, Colorer, util) { + var TotalColorOptionsHandlers = 0; + + function ColorOptionsHandler(container, enableContinuousColoring = false) { + // add count + TotalColorOptionsHandlers += 1; + + this.container = container; + this.observers = []; + this.defaultColor = "discrete-coloring-qiime"; + this.defaultReverseChk = false; + // create unique num + this.uniqueNum = TotalColorOptionsHandlers; + this.enableContinuousColoring = enableContinuousColoring; + + // Add a row for choosing the color map + var colormapP = this.container.appendChild(document.createElement("p")); + var colormapLbl = colormapP.appendChild( + document.createElement("label") + ); + colormapLbl.innerText = "Color Map"; + var colormapSC = colormapP.appendChild(document.createElement("label")); + colormapSC.classList.add("select-container"); + this.colormapSelector = document.createElement("select"); + Colorer.addColorsToSelect(this.colormapSelector); + colormapSC.appendChild(this.colormapSelector); + this.colormapSelector.id = + "color-options-handler-" + this.uniqueNum + "-colormap-select"; + colormapLbl.setAttribute("for", this.colormapSelector.id); + + // Add a row for choosing whether the color scale should + // be reversed + var reverseColormapP = this.container.appendChild( + document.createElement("p") + ); + var reverseColormapLbl = reverseColormapP.appendChild( + document.createElement("label") + ); + reverseColormapLbl.innerText = "Reverse Color Map"; + this.reverseColormapCheckbox = reverseColormapP.appendChild( + document.createElement("input") + ); + this.reverseColormapCheckbox.id = + "color-options-handler-" + this.uniqueNum + "-reverse-chk"; + this.reverseColormapCheckbox.setAttribute("type", "checkbox"); + this.reverseColormapCheckbox.classList.add("empress-input"); + reverseColormapLbl.setAttribute("for", this.reverseColormapCheckbox.id); + + var scope = this; + var notify = function () { + var options = scope.getOptions(); + _.each(scope.observers, function (obs) { + obs.colorOptionsUpdate(options); + }); + }; + + if (this.enableContinuousColoring) { + // add continuous values checkbox + var continuousValP = this.container.appendChild( + document.createElement("p") + ); + var continuousValLbl = continuousValP.appendChild( + document.createElement("label") + ); + continuousValLbl.innerText = "Continuous values?"; + this.continuousValCheckbox = continuousValP.appendChild( + document.createElement("input") + ); + this.continuousValCheckbox.id = + "color-options-handler-" + this.uniqueNum + "-continuous-chk"; + this.continuousValCheckbox.setAttribute("type", "checkbox"); + this.continuousValCheckbox.classList.add("empress-input"); + continuousValLbl.setAttribute("for", this.continuousValCheckbox.id); + // Hide the "Continuous values?" stuff by default, since the default + // colormap is discrete + continuousValP.classList.add("hidden"); + + // When we're working with a continuous colormap, provide users + // the ability to set the min/max of the input manually. See + // https://github.com/biocore/empress/pull/521. + var continuousManualScaleDiv = this.container.appendChild( + document.createElement("div") + ); + continuousManualScaleDiv.classList.add("hidden"); + continuousManualScaleDiv.classList.add("indented"); + var continuousManualScaleManualP = continuousManualScaleDiv.appendChild( + document.createElement("p") + ); + var continuousManualScaleLbl = continuousManualScaleManualP.appendChild( + document.createElement("label") + ); + continuousManualScaleLbl.innerText = + "Manually set gradient boundaries?"; + this.continuousManualScaleCheckbox = continuousManualScaleManualP.appendChild( + document.createElement("input") + ); + this.continuousManualScaleCheckbox.id = + "color-options-handler-" + + this.uniqueNum + + "-continuous-scale-chk"; + this.continuousManualScaleCheckbox.setAttribute("type", "checkbox"); + this.continuousManualScaleCheckbox.classList.add("empress-input"); + continuousManualScaleLbl.setAttribute( + "for", + this.continuousManualScaleCheckbox.id + ); + var continuousMinMaxDiv = continuousManualScaleDiv.appendChild( + document.createElement("div") + ); + continuousMinMaxDiv.classList.add("hidden"); + continuousMinMaxDiv.classList.add("indented"); + + // add min scale input + var continuousMinP = continuousMinMaxDiv.appendChild( + document.createElement("p") + ); + var continuousMinLbl = continuousMinP.appendChild( + document.createElement("label") + ); + continuousMinLbl.innerText = "Minimum value"; + this.continuousMinInput = continuousMinP.appendChild( + document.createElement("input") + ); + this.continuousMinInput.setAttribute("type", "number"); + this.continuousMinInput.classList.add("empress-input"); + this.continuousMinInput.value = null; + this.continuousMinInput.id = + "color-options-handler-" + + this.uniqueNum + + "-continuous-min-input"; + continuousMinLbl.setAttribute("for", this.continuousMinInput.id); + + // add max scale input + var continuousMaxP = continuousMinMaxDiv.appendChild( + document.createElement("p") + ); + var continuousMaxLbl = continuousMaxP.appendChild( + document.createElement("label") + ); + continuousMaxLbl.innerText = "Maximum value"; + this.continuousMaxInput = continuousMaxP.appendChild( + document.createElement("input") + ); + this.continuousMaxInput.setAttribute("type", "number"); + this.continuousMaxInput.classList.add("empress-input"); + this.continuousMaxInput.value = null; + this.continuousMaxInput.id = + "color-options-handler-" + + this.uniqueNum + + "-continuous-max-input"; + continuousMaxLbl.setAttribute("for", this.continuousMaxInput.id); + + var validateNumInput = function (input) { + util.parseAndValidateNum(input, null); + }; + + // add events + this.continuousValCheckbox.onchange = () => { + if (scope.continuousValCheckbox.checked) { + continuousManualScaleDiv.classList.remove("hidden"); + } else { + continuousManualScaleDiv.classList.add("hidden"); + } + notify(); + }; + this.continuousManualScaleCheckbox.onchange = () => { + if (scope.continuousManualScaleCheckbox.checked) { + continuousMinMaxDiv.classList.remove("hidden"); + } else { + continuousMinMaxDiv.classList.add("hidden"); + } + notify(); + }; + this.continuousMinInput.onchange = () => { + validateNumInput(scope.continuousMinInput, null); + notify(); + }; + this.continuousMinInput.addEventListener("focusout", () => { + validateNumInput(scope.continuousMinInput, null); + notify(); + }); + this.continuousMaxInput.onchange = () => { + validateNumInput(scope.continuousMaxInput, null); + notify(); + }; + this.continuousMaxInput.addEventListener("focusout", () => { + validateNumInput(scope.continuousMaxInput, null); + notify(); + }); + } + + this.colormapSelector.onchange = () => { + if (scope.enableContinuousColoring) { + if (Colorer.isColorMapDiscrete(scope.colormapSelector.value)) { + scope.continuousValCheckbox.checked = false; + continuousValP.classList.add("hidden"); + continuousManualScaleDiv.classList.add("hidden"); + } else { + continuousValP.classList.remove("hidden"); + } + } + notify(); + }; + this.reverseColormapCheckbox.onchange = notify; + } + + ColorOptionsHandler.prototype.registerObserver = function (obs) { + this.observers.push(obs); + }; + + ColorOptionsHandler.prototype.getOptions = function () { + var options = { + color: this.colormapSelector.value, + reverse: this.reverseColormapCheckbox.checked, + }; + if (this.enableContinuousColoring) { + this._getContinuousColoringOptions(options); + } + return options; + }; + + ColorOptionsHandler.prototype._getContinuousColoringOptions = function ( + options + ) { + options.continuousColoring = this.continuousValCheckbox.checked; + options.continuousManualScale = this.continuousManualScaleCheckbox.checked; + options.min = this.verifyMinBoundary(); + options.max = this.verifyMaxBoundary(); + + if (!options.continuousColoring) { + // set options to not use continuous coloring + options.continuousManualScale = false; + options.min = null; + options.max = null; + } else if (!options.continuousManualScale) { + // set options to use default continuous scale + options.min = null; + options.max = null; + } + }; + + ColorOptionsHandler.prototype.reset = function () { + this.colormapSelector.value = this.defaultColor; + this.reverseColormapCheckbox.checked = this.defaultReverseChk; + }; + + ColorOptionsHandler.prototype.verifyMinBoundary = function () { + var min = parseFloat(this.continuousMinInput.value); + + if (isNaN(min)) { + return "Minimum boundary value is missing."; + } + + return min; + }; + + ColorOptionsHandler.prototype.verifyMaxBoundary = function () { + var min = parseFloat(this.continuousMinInput.value); + var max = parseFloat(this.continuousMaxInput.value); + + if (isNaN(max)) { + return "Maximum boundary value is missing."; + } + + // It should be noted that if min isNaN that this will always return + // false + if (max <= min) { + return "Maximum boundary must be greater than minimum boundary."; + } + + return max; + }; + + return ColorOptionsHandler; +}); diff --git a/empress/support_files/js/colorer.js b/empress/support_files/js/colorer.js index f2c5f0f5..ee90b4c7 100644 --- a/empress/support_files/js/colorer.js +++ b/empress/support_files/js/colorer.js @@ -31,6 +31,13 @@ define(["chroma", "underscore", "util"], function (chroma, _, util) { * @param{Boolean} reverse Defaults to false. If true, the color scale * will be reversed, with respect to its default * orientation. + * @param{Array} domain Defaults to null. If this is not null, it is + * assumed to be an array of [min, max], where min and + * max are Numbers. If useQuantScale is true and the + * color map is sequential or diverging (i.e. we are + * creating a continuous colorscheme), then min and + * max will be used as the domain of the color map. + * * @return{Colorer} * constructs Colorer */ @@ -39,7 +46,8 @@ define(["chroma", "underscore", "util"], function (chroma, _, util) { values, useQuantScale = false, gradientIDSuffix = 0, - reverse = false + reverse = false, + domain = null ) { var scope = this; @@ -81,6 +89,26 @@ define(["chroma", "underscore", "util"], function (chroma, _, util) { // whether or not to show a warning) this._missingNonNumerics = false; + // Optional custom domain (or this will just be null) + this.domain = domain; + if (!_.isNull(this.domain)) { + // Perform sanity checking on the domain's entries (... or lack + // thereof). These problems should already have been caught by the + // ColorOptionsHandler, so this is just verifying that the code + // hasn't become haunted. + if (this.domain.length !== 2) { + throw new Error("Custom domain must have exactly 2 entries"); + } + if (!_.isFinite(this.domain[0]) || !_.isFinite(this.domain[1])) { + throw new Error("Custom domain entries must be finite nums"); + } + // I think chroma can actually handle this case -- it'll just + // flip the numbers. But let's not rely on that. + if (this.domain[0] >= this.domain[1]) { + throw new Error("Custom domain min must be < max"); + } + } + /*** End continuous-scaling-specific things ***/ // Based on the color map, container, type and the value of @@ -213,6 +241,35 @@ define(["chroma", "underscore", "util"], function (chroma, _, util) { } }; + /** + * Returns the minimum and maximum of a set of values, accounting for + * the possibility of a custom domain. + * + * Usually, this will just return the min and max values of the input + * array, but if this.domain has been set then this will just return the + * values from that instead. + * + * @param{Array} nums An array of values to be mapped to colors. + * + * @return {Object} minAndMax An object containing two keys: min (maps to + * the minimum value) and max (maps to the + * maximum value). + */ + Colorer.prototype.getContinuousColorRange = function (nums) { + var min, max; + if (_.isNull(this.domain)) { + min = _.min(nums); + max = _.max(nums); + } else { + min = this.domain[0]; + max = this.domain[1]; + } + return { + min: min, + max: max, + }; + }; + /** * Assigns colors from a sequential or diverging color palette (specified * by this.color) for every value in this.sortedUniqueValues, taking into @@ -243,14 +300,16 @@ define(["chroma", "underscore", "util"], function (chroma, _, util) { throw new Error("Category has less than 2 unique numeric values."); } var nums = _.map(split.numeric, parseFloat); - var min = _.min(nums); - var max = _.max(nums); + var range = this.getContinuousColorRange(nums); + var min = range.min; + var max = range.max; var domain; if (this.reverse) { domain = [max, min]; } else { domain = [min, max]; } + var interpolator; if (this.color === Colorer.__GN_OR_PR) { interpolator = chroma.scale(Colorer.__gnOrPr).domain(domain); @@ -266,7 +325,7 @@ define(["chroma", "underscore", "util"], function (chroma, _, util) { // Create SVG describing the gradient: basically, we sample the color // map along the domain 101 times, and use these 101 colors to define - // the for each integer percentage in the inclusve + // the for each integer percentage in the inclusive // range [0%, 100%]. See https://github.com/biocore/emperor/issues/788. var mid = (min + max) / 2; var stopColors = interpolator.colors(101); diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index fc803003..b4377dce 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -1924,20 +1924,71 @@ define([ this._featureMetadataColumns, layer.colorByFMField ); - // We pass the true/false value of the "Continuous values?" - // checkbox to Colorer regardless of if the selected color map - // is discrete or sequential/diverging. This is because the Colorer - // class constructor is smart enough to ignore useQuantScale = true - // if the color map is discrete in the first place. (This is tested - // in the Colorer tests; ctrl-F for "CVALDISCRETETEST" in - // tests/test-colorer.js to see this.) + // Prepare for having to throw an error at some point, maybe... + var msg = "Layer " + layer.num + ": "; + + // Has the user requested a custom domain for a gradient colormap? + // (If not, we'll leave domain as null when we create a Colorer + // object later, and it'll handle things normally.) + var domain = null; + if ( + layer.colorByFMContinuous && + layer.colorByFMContinuousManualScale + ) { + // If the min / max boundary fields are missing, these will not + // be numeric values. Instead, they'll actually be string error + // messages returned by ColorOptionsHandler.verifyMaxBoundary() + // -- we can check for this using _.isFinite(). + var min = layer.colorByFMContinuousMin; + var max = layer.colorByFMContinuousMax; + + var msgHeader = "Barplot coloring error"; + var minErr = !_.isFinite(min); + var maxErr = !_.isFinite(max); + var boundaryErrors = true; + if (minErr) { + if (maxErr) { + // Just concatenate the error messages together. + // I guess if we wanna get ~~really~~ fancy with + // this we could separate them by a semicolon or + // something (and then make the first character of + // the "max" error message lowercase), but that's + // probs too much work + msg += min + " " + max; + } else { + msg += min; + } + } else { + if (maxErr) { + msg += max; + } else { + boundaryErrors = false; + } + } + + if (boundaryErrors) { + util.toastMsg(msgHeader, msg, (duration = 5000)); + throw msg; + } + + domain = [min, max]; + } try { + // We pass the true/false value of the "Continuous values?" + // checkbox to Colorer regardless of if the selected color map + // is discrete or sequential/diverging. This is because the + // Colorer class constructor is smart enough to ignore + // useQuantScale = true if the color map is discrete in the + // first place. (This is tested in the Colorer tests; ctrl-F + // for "CVALDISCRETETEST" in tests/test-colorer.js to see + // this.) colorer = new Colorer( layer.colorByFMColorMap, sortedUniqueColorValues, layer.colorByFMContinuous, layer.uniqueNum, - layer.colorByFMColorReverse + layer.colorByFMColorReverse, + domain ); } catch (err) { // If the Colorer construction failed (should only have @@ -1948,11 +1999,8 @@ 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. - var msg = - "Error with assigning colors in barplot layer " + - layer.num + - ": " + - 'the feature metadata field "' + + msg += + ' the feature metadata field "' + layer.colorByFMField + '" has less than 2 unique numeric values.'; util.toastMsg("Barplot coloring error", msg, (duration = 5000)); diff --git a/empress/support_files/js/side-panel-handler.js b/empress/support_files/js/side-panel-handler.js index 5a50128e..a57ade1f 100644 --- a/empress/support_files/js/side-panel-handler.js +++ b/empress/support_files/js/side-panel-handler.js @@ -1,4 +1,9 @@ -define(["underscore", "Colorer", "util"], function (_, Colorer, util) { +define(["underscore", "Colorer", "ColorOptionsHandler", "util"], function ( + _, + Colorer, + ColorOptionsHandler, + util +) { /** * * @class SidePanel @@ -52,10 +57,10 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { this.sChk = document.getElementById("sample-chk"); this.sSel = document.getElementById("sample-options"); this.sAddOpts = document.getElementById("sample-add"); - this.sColor = document.getElementById("sample-color"); - this.sReverseColor = document.getElementById( - "sample-reverse-color-chk" + this.sColorOptions = new ColorOptionsHandler( + document.getElementById("sample-color-options-div") ); + this.sColorOptions.registerObserver(this); this.sCollapseCladesChk = document.getElementById( "sample-collapse-chk" ); @@ -67,10 +72,10 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { this.fChk = document.getElementById("feature-chk"); this.fSel = document.getElementById("feature-options"); this.fAddOpts = document.getElementById("feature-add"); - this.fColor = document.getElementById("feature-color"); - this.fReverseColor = document.getElementById( - "feature-reverse-color-chk" + this.fColorOptions = new ColorOptionsHandler( + document.getElementById("feature-color-options-div") ); + this.fColorOptions.registerObserver(this); this.fCollapseCladesChk = document.getElementById( "feature-collapse-chk" ); @@ -189,6 +194,18 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { }; } + SidePanel.prototype.colorOptionsUpdate = function (options) { + this.showUpdateBtn(); + }; + + SidePanel.prototype.resetColorOptions = function () { + if (this.sChk.checked) { + this.fColorOptions.reset(); + } else if (this.fChk.checked) { + this.sColorOptions.reset(); + } + }; + /** * Utility function that resets various HTML elements, then resets the * tree and its legends. @@ -218,6 +235,7 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { _.each(elesToHide, function (ele) { ele.classList.add("hidden"); }); + this.resetColorOptions(); // Reset tree and then clear legend this.empress.resetTree(); this.empress.drawTree(); @@ -230,8 +248,6 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { { sChk: { checked: false }, sSel: { disabled: true }, - sColor: { value: "discrete-coloring-qiime" }, - sReverseColor: { checked: false }, sLineWidth: { value: 0 }, sCollapseCladesChk: { checked: false }, }, @@ -245,8 +261,6 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { { fChk: { checked: false }, fSel: { disabled: true }, - fColor: { value: "discrete-coloring-qiime" }, - fReverseColor: { checked: false }, fLineWidth: { value: 0 }, fMethodChk: { checked: true }, fCollapseCladesChk: { checked: false }, @@ -319,10 +333,12 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { */ SidePanel.prototype._colorSampleTree = function () { var colBy = this.sSel.value; - var col = this.sColor.value; - var reverse = this.sReverseColor.checked; - var keyInfo = this.empress.colorBySampleCat(colBy, col, reverse); - + var colorOptions = this.sColorOptions.getOptions(); + var keyInfo = this.empress.colorBySampleCat( + colBy, + colorOptions.color, + colorOptions.reverse + ); if (keyInfo === null) { util.toastMsg( "Sample metadata coloring error", @@ -338,14 +354,13 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { */ SidePanel.prototype._colorFeatureTree = function () { var colBy = this.fSel.value; - var col = this.fColor.value; var coloringMethod = this.fMethodChk.checked ? "tip" : "all"; - var reverse = this.fReverseColor.checked; + var colorOptions = this.fColorOptions.getOptions(); var keyInfo = this.empress.colorByFeatureMetadata( colBy, - col, + colorOptions.color, coloringMethod, - reverse + colorOptions.reverse ); if (_.isEmpty(keyInfo)) { util.toastMsg( @@ -512,6 +527,14 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { }; }; + SidePanel.prototype.showUpdateBtn = function () { + if (this.sChk.checked) { + this.sUpdateBtn.classList.remove("hidden"); + } else if (this.fChk.checked) { + this.fUpdateBtn.classList.remove("hidden"); + } + }; + /** * Initializes sample components */ @@ -529,9 +552,6 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { this.sSel.appendChild(opt); } - // The color map selector - Colorer.addColorsToSelect(this.sColor); - // toggle the sample/color map selectors this.sChk.onclick = function () { if (scope.sChk.checked) { @@ -544,13 +564,12 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { } }; - var showUpdateBtn = function () { - scope.sUpdateBtnP.classList.remove("hidden"); + this.sSel.onchange = () => { + scope.showUpdateBtn(); + }; + this.sLineWidth.onchange = () => { + scope.showUpdateBtn(); }; - this.sSel.onchange = showUpdateBtn; - this.sColor.onchange = showUpdateBtn; - this.sReverseColor.onchange = showUpdateBtn; - this.sLineWidth.onchange = showUpdateBtn; this.sUpdateBtn.onclick = function () { scope._updateColoring( @@ -619,9 +638,6 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { this.fSel.appendChild(opt); } - // The color map selector - Colorer.addColorsToSelect(this.fColor); - // toggle the sample/color map selectors this.fChk.onclick = function () { if (scope.fChk.checked) { @@ -635,16 +651,15 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { } }; - var showUpdateBtn = function () { - scope.fUpdateBtnP.classList.remove("hidden"); + this.fSel.onchange = () => { + scope.showUpdateBtn(); + }; + this.fLineWidth.onchange = () => { + scope.showUpdateBtn(); }; - this.fSel.onchange = showUpdateBtn; - this.fColor.onchange = showUpdateBtn; - this.fReverseColor.onchange = showUpdateBtn; - this.fLineWidth.onchange = showUpdateBtn; this.fMethodChk.onchange = function () { scope.updateFeatureMethodDesc(); - showUpdateBtn(); + scope.showUpdateBtn(); }; this.fUpdateBtn.onclick = function () { diff --git a/empress/support_files/js/util.js b/empress/support_files/js/util.js index 9ada67bc..7b83e865 100644 --- a/empress/support_files/js/util.js +++ b/empress/support_files/js/util.js @@ -171,14 +171,20 @@ define(["underscore", "toastr"], function (_, toastr) { * element. * @param {Number} min Defaults to 0. Minimum acceptable value for line * width/for whatever numeric quality is being - * considered. + * considered. min can also be set to null if + * inputEle.value can be null. * @return {Number} Sanitized number that can be used as input to * Empress.thickenColoredNodes(). */ function parseAndValidateNum(inputEle, min = 0) { if (isValidNumber(inputEle.value)) { var pfVal = parseFloat(inputEle.value); - if (pfVal >= min) { + // if min is null, then the number in the input element doesn't + // have a defined lower limit -- so, for example, negative numbers + // will be accepted. This is the case when, for example, we want + // to allow users to set continuous colormaps that range from + // [-5, 5] or something. + if (_.isNull(min) || pfVal >= min) { return pfVal; } } diff --git a/empress/support_files/templates/empress-template.html b/empress/support_files/templates/empress-template.html index f7172fc9..98e9211d 100644 --- a/empress/support_files/templates/empress-template.html +++ b/empress/support_files/templates/empress-template.html @@ -115,11 +115,11 @@ 'LayoutsUtil': './js/layouts-util', 'ExportUtil': './js/export-util', 'TreeController': './js/tree-controller', + 'ColorOptionsHandler': './js/color-options-handler', 'Shearer': './js/shearer', 'EnableDisableTab': './js/enable-disable-tab', 'EnableDisableSidePanelTab': './js/enable-disable-side-panel-tab', 'EnableDisableAnimationTab': './js/enable-disable-animation-tab', - } }); diff --git a/empress/support_files/templates/side-panel.html b/empress/support_files/templates/side-panel.html index 92bc82a2..62fb3597 100644 --- a/empress/support_files/templates/side-panel.html +++ b/empress/support_files/templates/side-panel.html @@ -30,17 +30,7 @@