Skip to content

Commit

Permalink
ENH: Add optgroup headers to search field selects
Browse files Browse the repository at this point in the history
Closes #191.

Although this might seem like a pretty simple change, it required a
decent amount of restructuring to get the code + tests to play nicely.
To elaborate a bit:

- Refactored populateSelect() to support taking in an object of
  options, where keys correspond to optgroup labels and values
  correspond to a list of child option values/names

- Added tests of this new populateSelect() behavior to test_dom_utils
    - This necessitated updating getChildValuesFromSelect() and
      assertSelected() to handle optgroups properly. This was kind of
      a pain, but it's working ok now.

- Broke off some now-redundant code within populateSelect() to a
  helper function (addOptionsToParentElement()).

- Stopped storing featureFields as a property of RRVDisplay (since it's
  only used once)

- Also, moved "Feature ID" out of featureMetadataFields. This makes life
  a bit more convenient for us. (necessitated changing up the feature
  text DOM tests a bit, as well as changing up featureRowListToText() a
  bit to add in "Feature ID" to a copy of featureMetadataFields)

I think that's it. Might add another test or two for the "empty"
corner-case of populateSelect(), but this should be fine.
  • Loading branch information
fedarko committed Jul 15, 2019
1 parent b270b37 commit b5349ba
Show file tree
Hide file tree
Showing 13 changed files with 573 additions and 226 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
([#186](https://github.com/biocore/qurro/issues/186))
- Added feature rankings as searchable fields in the "Selecting Features"
controls.
- Feature metadata fields and feature ranking fields are now grouped under
`Feature Metadata` or `Feature Ranking` headers in the searchable fields
dropdowns. ([#191](https://github.com/biocore/qurro/issues/191))
- Added numeric searching: now you can search through numeric feature metadata or
feature rankings using basic comparison operators. (Non-numeric input search
text will result in the search not identifying any features, and non-numeric
Expand Down
52 changes: 19 additions & 33 deletions docs/demos/byrd/js/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,6 @@ define(["./feature_computation", "./dom_utils", "vega", "vega-embed"], function(
this.rankOrdering = undefined;
// Ordered list of all feature metadata fields
this.featureMetadataFields = undefined;
// Ordered list of all feature metadata + feature ranking fields
// (starting with "Feature ID"). These are used to populate the
// <select>s where the user can choose what field to search by.
this.featureFields = undefined;

this.rankPlotView = undefined;
this.samplePlotView = undefined;
Expand Down Expand Up @@ -184,33 +180,23 @@ define(["./feature_computation", "./dom_utils", "vega", "vega-embed"], function(
this.rankOrdering,
this.rankOrdering[0]
);
// NOTE: we use .slice() to make a copy of the initial array so
// that when we unshift "Feature ID" onto
// this.featureFields, the original array (in the rank
// plot JSON) isn't modified.
this.featureMetadataFields = this.rankPlotJSON.datasets.qurro_feature_metadata_ordering.slice();
// Add "Feature ID" to the beginning of the feature metadata
// fields list.
this.featureMetadataFields.unshift("Feature ID");

// featureFields is a superset of featureMetadataFields that
// includes all feature ranking fields. featureMetadataFields
// is used for providing information about selected features,
// while featureFields is used for searching.
this.featureFields = this.featureMetadataFields.concat(
this.rankOrdering
);
// ...And here, we populate the topSearch and botSearch
// <select>s with featureFields.
this.featureMetadataFields = this.rankPlotJSON.datasets.qurro_feature_metadata_ordering;
var searchableFields = {
standalone: ["Feature ID"],
"Feature Metadata": this.featureMetadataFields,
"Feature Rankings": this.rankOrdering
};
dom_utils.populateSelect(
"topSearch",
this.featureFields,
"Feature ID"
searchableFields,
"Feature ID",
true
);
dom_utils.populateSelect(
"botSearch",
this.featureFields,
"Feature ID"
searchableFields,
"Feature ID",
true
);
// Figure out which bar size type to default to.
// We determine this based on how many features there are.
Expand Down Expand Up @@ -613,20 +599,20 @@ define(["./feature_computation", "./dom_utils", "vega", "vega-embed"], function(
}
var outputText = "";
var currVal, anotherFieldLeft;
// fmFields is just the feature metadata fields, with "Feature ID"
// added on at the beginning
var fmFields = this.featureMetadataFields.slice();
fmFields.unshift("Feature ID");
for (var i = 0; i < featureRowList.length; i++) {
if (i > 0) {
outputText += "\n";
}
// Note that "Feature ID" is included in
// this.featureMetadataFields, since we added it in
// makeRankPlot() above
for (var c = 0; c < this.featureMetadataFields.length; c++) {
currVal = featureRowList[i][this.featureMetadataFields[c]];
for (var c = 0; c < fmFields.length; c++) {
currVal = featureRowList[i][fmFields[c]];
// If currVal *is* null or undefined, this will look like
// / / in the output for this metadata field -- which is
// fine.
anotherFieldLeft =
c + 1 < this.featureMetadataFields.length;
anotherFieldLeft = c + 1 < fmFields.length;
if (currVal !== null && currVal !== undefined) {
outputText += currVal;
if (anotherFieldLeft) {
Expand Down
86 changes: 77 additions & 9 deletions docs/demos/byrd/js/dom_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,90 @@ define(["vega"], function(vega) {
return elementIDs;
}

/* Populates a <select> DOM element with a list of options.
/* Adds <option> elements to a parent DOM element.
*
* For each item "x" in optionList, a new <option> element is created
* inside parentElement with a value and name of "x".
*/
function addOptionsToParentElement(optionList, parentElement) {
var optionEle;
for (var m = 0; m < optionList.length; m++) {
optionEle = document.createElement("option");
optionEle.value = optionEle.text = optionList[m];
parentElement.appendChild(optionEle);
}
}

/* Populates a <select> DOM element with a list or object of options.
*
* This will remove any options already present in the <select> first.
*
* By default -- if optgroupMap is falsy -- this function assumes that
* "options" is just a list of options to add directly to the <select>.
*
* If optgroupMap is truthy, this assumes that "options" is actually an
* object of the form {"g1": ["o1", ...], ..., "gX": ["oX", ...]}.
* This will still populate the <select> with all of the options ("o1",
* "oX", ...) in these lists, so the behavior will functionally be the
* same -- but the keys of the "options" object ("g1", "gX", ...) will be
* used to create <optgroup>s surrounding their respective options. (So
* "o1" would be an <option> within an <optgroup> labelled "g1" and "oX"
* would be an <option> within an <optgroup> labelled "gX", for example.)
*
* (If one of the <optgroup> names is "standalone", then its children will
* be added to the <select> directly. This functionality can be used to
* create "global" options that aren't within a specific <optgroup>.)
*/
function populateSelect(selectID, optionList, defaultVal) {
if (optionList.length <= 0) {
throw new Error("optionList must have at least one value");
function populateSelect(selectID, options, defaultVal, optgroupMap) {
var optgroups;
if (optgroupMap) {
optgroups = Object.keys(options);
if (optgroups.length <= 0) {
throw new Error(
"options must have at least one optgroup specified"
);
}
} else {
if (options.length <= 0) {
throw new Error("options must have at least one value");
}
}
var optionEle;
var optionEle, groupEle;
var selectEle = document.getElementById(selectID);
// Remove any options already present in the <select>
clearDiv(selectID);
for (var m = 0; m < optionList.length; m++) {
optionEle = document.createElement("option");
optionEle.value = optionEle.text = optionList[m];
selectEle.appendChild(optionEle);
// Actually populate the <select>
if (optgroupMap) {
for (var g = 0; g < optgroups.length; g++) {
// Ignore empty optgroups. (In practice, this means that
// datasets without any specified feature metadata won't have
// an empty "Feature Metadata" optgroup shown in the search
// field <select>s.)
if (options[optgroups[g]].length > 0) {
if (optgroups[g] === "standalone") {
// If we find an optgroups with the label "standalone"
// then we'll just add the option(s) within that label
// to the <select> directly.
addOptionsToParentElement(
options["standalone"],
selectEle
);
} else {
// For all other optgroups, though, actually create an
// <optgroup> element, populate that, then add that to
// the <select>.
groupEle = document.createElement("optgroup");
groupEle.label = optgroups[g];
addOptionsToParentElement(
options[optgroups[g]],
groupEle
);
selectEle.appendChild(groupEle);
}
}
}
} else {
addOptionsToParentElement(options, selectEle);
}
// Set the default value of the <select>. Note that we escape this
// value in quotes, just in case it contains a period or some other
Expand Down
52 changes: 19 additions & 33 deletions docs/demos/q2_moving_pictures/js/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,6 @@ define(["./feature_computation", "./dom_utils", "vega", "vega-embed"], function(
this.rankOrdering = undefined;
// Ordered list of all feature metadata fields
this.featureMetadataFields = undefined;
// Ordered list of all feature metadata + feature ranking fields
// (starting with "Feature ID"). These are used to populate the
// <select>s where the user can choose what field to search by.
this.featureFields = undefined;

this.rankPlotView = undefined;
this.samplePlotView = undefined;
Expand Down Expand Up @@ -184,33 +180,23 @@ define(["./feature_computation", "./dom_utils", "vega", "vega-embed"], function(
this.rankOrdering,
this.rankOrdering[0]
);
// NOTE: we use .slice() to make a copy of the initial array so
// that when we unshift "Feature ID" onto
// this.featureFields, the original array (in the rank
// plot JSON) isn't modified.
this.featureMetadataFields = this.rankPlotJSON.datasets.qurro_feature_metadata_ordering.slice();
// Add "Feature ID" to the beginning of the feature metadata
// fields list.
this.featureMetadataFields.unshift("Feature ID");

// featureFields is a superset of featureMetadataFields that
// includes all feature ranking fields. featureMetadataFields
// is used for providing information about selected features,
// while featureFields is used for searching.
this.featureFields = this.featureMetadataFields.concat(
this.rankOrdering
);
// ...And here, we populate the topSearch and botSearch
// <select>s with featureFields.
this.featureMetadataFields = this.rankPlotJSON.datasets.qurro_feature_metadata_ordering;
var searchableFields = {
standalone: ["Feature ID"],
"Feature Metadata": this.featureMetadataFields,
"Feature Rankings": this.rankOrdering
};
dom_utils.populateSelect(
"topSearch",
this.featureFields,
"Feature ID"
searchableFields,
"Feature ID",
true
);
dom_utils.populateSelect(
"botSearch",
this.featureFields,
"Feature ID"
searchableFields,
"Feature ID",
true
);
// Figure out which bar size type to default to.
// We determine this based on how many features there are.
Expand Down Expand Up @@ -613,20 +599,20 @@ define(["./feature_computation", "./dom_utils", "vega", "vega-embed"], function(
}
var outputText = "";
var currVal, anotherFieldLeft;
// fmFields is just the feature metadata fields, with "Feature ID"
// added on at the beginning
var fmFields = this.featureMetadataFields.slice();
fmFields.unshift("Feature ID");
for (var i = 0; i < featureRowList.length; i++) {
if (i > 0) {
outputText += "\n";
}
// Note that "Feature ID" is included in
// this.featureMetadataFields, since we added it in
// makeRankPlot() above
for (var c = 0; c < this.featureMetadataFields.length; c++) {
currVal = featureRowList[i][this.featureMetadataFields[c]];
for (var c = 0; c < fmFields.length; c++) {
currVal = featureRowList[i][fmFields[c]];
// If currVal *is* null or undefined, this will look like
// / / in the output for this metadata field -- which is
// fine.
anotherFieldLeft =
c + 1 < this.featureMetadataFields.length;
anotherFieldLeft = c + 1 < fmFields.length;
if (currVal !== null && currVal !== undefined) {
outputText += currVal;
if (anotherFieldLeft) {
Expand Down
86 changes: 77 additions & 9 deletions docs/demos/q2_moving_pictures/js/dom_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,90 @@ define(["vega"], function(vega) {
return elementIDs;
}

/* Populates a <select> DOM element with a list of options.
/* Adds <option> elements to a parent DOM element.
*
* For each item "x" in optionList, a new <option> element is created
* inside parentElement with a value and name of "x".
*/
function addOptionsToParentElement(optionList, parentElement) {
var optionEle;
for (var m = 0; m < optionList.length; m++) {
optionEle = document.createElement("option");
optionEle.value = optionEle.text = optionList[m];
parentElement.appendChild(optionEle);
}
}

/* Populates a <select> DOM element with a list or object of options.
*
* This will remove any options already present in the <select> first.
*
* By default -- if optgroupMap is falsy -- this function assumes that
* "options" is just a list of options to add directly to the <select>.
*
* If optgroupMap is truthy, this assumes that "options" is actually an
* object of the form {"g1": ["o1", ...], ..., "gX": ["oX", ...]}.
* This will still populate the <select> with all of the options ("o1",
* "oX", ...) in these lists, so the behavior will functionally be the
* same -- but the keys of the "options" object ("g1", "gX", ...) will be
* used to create <optgroup>s surrounding their respective options. (So
* "o1" would be an <option> within an <optgroup> labelled "g1" and "oX"
* would be an <option> within an <optgroup> labelled "gX", for example.)
*
* (If one of the <optgroup> names is "standalone", then its children will
* be added to the <select> directly. This functionality can be used to
* create "global" options that aren't within a specific <optgroup>.)
*/
function populateSelect(selectID, optionList, defaultVal) {
if (optionList.length <= 0) {
throw new Error("optionList must have at least one value");
function populateSelect(selectID, options, defaultVal, optgroupMap) {
var optgroups;
if (optgroupMap) {
optgroups = Object.keys(options);
if (optgroups.length <= 0) {
throw new Error(
"options must have at least one optgroup specified"
);
}
} else {
if (options.length <= 0) {
throw new Error("options must have at least one value");
}
}
var optionEle;
var optionEle, groupEle;
var selectEle = document.getElementById(selectID);
// Remove any options already present in the <select>
clearDiv(selectID);
for (var m = 0; m < optionList.length; m++) {
optionEle = document.createElement("option");
optionEle.value = optionEle.text = optionList[m];
selectEle.appendChild(optionEle);
// Actually populate the <select>
if (optgroupMap) {
for (var g = 0; g < optgroups.length; g++) {
// Ignore empty optgroups. (In practice, this means that
// datasets without any specified feature metadata won't have
// an empty "Feature Metadata" optgroup shown in the search
// field <select>s.)
if (options[optgroups[g]].length > 0) {
if (optgroups[g] === "standalone") {
// If we find an optgroups with the label "standalone"
// then we'll just add the option(s) within that label
// to the <select> directly.
addOptionsToParentElement(
options["standalone"],
selectEle
);
} else {
// For all other optgroups, though, actually create an
// <optgroup> element, populate that, then add that to
// the <select>.
groupEle = document.createElement("optgroup");
groupEle.label = optgroups[g];
addOptionsToParentElement(
options[optgroups[g]],
groupEle
);
selectEle.appendChild(groupEle);
}
}
}
} else {
addOptionsToParentElement(options, selectEle);
}
// Set the default value of the <select>. Note that we escape this
// value in quotes, just in case it contains a period or some other
Expand Down
Loading

0 comments on commit b5349ba

Please sign in to comment.