diff --git a/.travis.yml b/.travis.yml index b85b6dc2..784cfe64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,8 +21,11 @@ before_install: - travis_retry conda env create -n qiime2-dev --file qiime2-latest-py36-linux-conda.yml - source activate qiime2-dev # 3. Install Songbird and DEICODE - # (this will let us rerun the corresponding example notebooks) - - conda install -c conda-forge songbird deicode + # (this will let us rerun the Sleep Apnea, Moving Pictures, Qarcoal, and + # Red Sea example notebooks) + # These are installed using pip due to conda-forge-related problems at the + # moment: see https://github.com/biocore/songbird/issues/126. + - pip install songbird deicode # 4. Install seaborn so we can run the colors example notebook # (should be already installed in a QIIME 2 environment, but we may as well # make sure that this won't break in the future) @@ -30,6 +33,11 @@ before_install: # 5. Install ALDEx2 and mygene # (this will let us rerun the ALDEx2 example notebook) - conda install -c bioconda bioconductor-aldex2 mygene + # 6. Update Node.js to the latest version, since Prettier relies on us + # having at least version 10.13 and the default on Travis as of writing is + # apparently 8.12.0. See https://github.com/nvm-sh/nvm#usage and + # https://github.com/travis-ci/travis-ci/issues/4090#issuecomment-122688955 + - nvm install node install: - pip install -e .[dev] # Install various JS testing/stylechecking packages that we use for diff --git a/CHANGELOG.md b/CHANGELOG.md index 9157d3a6..cf488d8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,51 @@ # Qurro changelog +## Qurro 0.7.0 (TBD) +### Features added +- Added the ability to **easily search using multiple text queries at once**: + this is done using the `contains text separated by | (pipe)` searching + option. You can pass in, e.g. `abc | def | ghi` to select any features where + the selected field contains at least one of `abc`, `def`, or `ghi`. + ([#224](https://github.com/biocore/qurro/issues/224)) + - This works more intuitively than the `separated text fragment(s)` option, + and should be useful for a few cases that that option can't handle (e.g. + polyphyletic taxa, as discussed in issue 224). +- Added a **`Draw borders on scatterplot points?` checkbox**, which is useful for making + light-colored points in the sample plot easier to see on the white background. + ([#240](https://github.com/biocore/qurro/issues/240)) +- Added the ability to **enter in negative numbers in autoselection** to flip + the selection (selecting the numerator from the lowest-ranked features and + the denominator from the highest-ranked features). + ([#264](https://github.com/biocore/qurro/issues/264)) +- Added the **`Classic QIIME Colors` categorical color scheme** used in some other + visualization tools, including [Emperor](https://biocore.github.io/emperor/) + and [Empress](https://github.com/biocore/empress), to the sample plot's + categorical color scheme options. + ([#300](https://github.com/biocore/qurro/issues/300)) + - (`tableau10` is still the default categorical color scheme in Qurro, though.) +- Added a **["selection" tutorial](https://nbviewer.jupyter.org/github/biocore/qurro/blob/master/example_notebooks/selection/selection.ipynb)** + describing the various ways of selecting features in Qurro in detail. + ([#123](https://github.com/biocore/qurro/issues/123)) + - (This was previously the appendix in the moving pictures tutorial, but now + it's been split off and expanded into its own thing.) +### Backward-incompatible changes +- For the time being, we are only supporting Qurro for Python versions of **at + least 3.6 and less than 3.8**. The code hasn't really changed, but this seems + like it'll be the simplest option for maintenance in the short term. +### Bug fixes +- Previously, the autoselection number field had an implicit "step size" of 1. + I don't think this should have prevented users from entering in + floating-point numbers here, but some people's browsers may have complained + on seeing a floating-point number. This problem should be resolved now. + (See [here](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number#step) for details about this.) +### Performance enhancements +### Miscellaneous +- Qurro is now installable on conda through the conda-forge channel! + ([#153](https://github.com/biocore/qurro/issues/153)) +- Various minor documentation updates, including adding citation info for + Qurro's recently-published paper to the README. + ([#169](https://github.com/biocore/qurro/issues/169)) + ## Qurro 0.6.0 (March 10, 2020) ### Features added - Added **tooltips** throughout the Qurro interface explaining what certain diff --git a/README.md b/README.md index c0c51db0..1ed3427a 100644 --- a/README.md +++ b/README.md @@ -9,41 +9,49 @@ Code Coverage DOI PyPI +conda-forge

(Pronounced "churro.")

-### What does this tool do? -(mostly taken from our paper abstract/intro.) - +## What does this tool do? Lots of tools for analyzing " 'omic" datasets can produce -__feature rankings__. Regardless of if they're *differentials* (corresponding -to the log-fold change in relative abundance re: a covariate) produced by a -tool like [Songbird](https://github.com/biocore/songbird/), -[ALDEx2](https://bioconductor.org/packages/release/bioc/html/ALDEx2.html), -etc., or the *feature loadings* in a (compositional) -biplot produced by a tool like [DEICODE](https://github.com/biocore/DEICODE), -either of these input types can be sorted numerically to -"rank" features based on their association with some sort of variation in your -dataset. +__feature rankings__. These rankings can be used as a guide to look at the __log-ratios__ of certain features in a dataset. Qurro is a tool for visualizing and exploring both of these types of data. + +### What are feature rankings? +The term "feature rankings" includes __differentials__, which we define as the estimated log-fold changes for features' abundances across different sample types. You can get this sort of output from lots of "differential abundance" tools, including but definitely not limited to [ALDEx2](https://bioconductor.org/packages/release/bioc/html/ALDEx2.html), [Songbird](https://github.com/biocore/songbird/), [Corncob](https://github.com/bryandmartin/corncob/), [DESeq2](https://bioconductor.org/packages/release/bioc/html/DESeq2.html), [edgeR](https://bioconductor.org/packages/release/bioc/html/edgeR.html), etc. + +The term "feature rankings" also includes __feature loadings__ in a [biplot](https://en.wikipedia.org/wiki/Biplot) (see [Aitchison and Greenacre 2002](https://rss.onlinelibrary.wiley.com/doi/full/10.1111/1467-9876.00275)); you can get biplots from running [DEICODE](https://github.com/biocore/DEICODE), +which is a tool that works well with microbiome datasets, or from a variety of other methods. + +Differentials and feature loadings alike can be interpreted as rankings -- you +can sort them numerically to "rank" features based on their association with +some sort of variation in your dataset. + +### What can we do with feature rankings? A common use of these rankings is examining the __log-ratios__ of particularly high- or low-ranked features across the samples in your dataset, and seeing how these log-ratios relate to your sample metadata (e.g. "does -this log-ratio differ between 'healthy' and 'sick' samples?"). For more -details (why rankings, why log-ratios, ...), check out +this log-ratio differ between 'healthy' and 'sick' samples?"). For +details as to why this approach is useful, check out [this open access paper](https://www.nature.com/articles/s41467-019-10656-5). +### How does this tool help? + __Qurro is an interactive web application for visualizing feature rankings and log-ratios.__ It does this -using a two-plot interface: on the left of the screen, a "rank plot" shows -how features are ranked for a selected ranking, and on the right of the screen -a "sample plot" shows the log-ratios of selected features' abundances within -samples. There are a variety of controls available for selecting features for -a log-ratio, and changing the selected log-ratio updates both the rank plot -(highlighting selected features) and the sample plot (changing the y-axis -value of each sample to match the selected log-ratio). +using a two-plot interface: on the left side of the screen, a "rank plot" shows +how features are ranked for a selected ranking, and on the right side of the +screen a "sample plot" shows the log-ratios of selected features' abundances +within samples. There are a variety of controls available for selecting +features for a log-ratio, and changing the selected log-ratio updates both the +rank plot (highlighting selected features) and the sample plot (changing the +y-axis value of each sample to match the selected log-ratio). -### How do I use it? +**A paper describing Qurro is now available at NAR Genomics and Bioinformatics +[here](https://academic.oup.com/nargab/article/2/2/lqaa023/5826153).** + +### How do I use this tool? Qurro can be used standalone (as a Python 3 script that generates a folder containing a HTML/JS/CSS visualization) or as a @@ -76,14 +84,18 @@ can be viewed online [here](https://biocore.github.io/qurro/demos/red_sea/index. ## Installation and Usage -You can install Qurro using [pip](https://pip.pypa.io/en/stable/): +You can install Qurro using [pip](https://pip.pypa.io/en/stable/) or [conda](https://docs.conda.io/en/latest/). In either case, a python version of at least 3.6 and less than 3.8 is required to use Qurro. +### Installing with `pip` ```bash pip install cython "numpy >= 1.12.0" pip install qurro ``` -A python version of at least 3.5.3 is required to use Qurro. +### Installing with `conda` +```bash +conda install -c conda-forge qurro +``` ### Temporary Caveat @@ -103,13 +115,13 @@ for context. ## Tutorials -### In-depth +### In-depth tutorials These tutorials are all good places to start, depending on what sort of data and feature rankings you have. - [Color Composition tutorial](https://nbviewer.jupyter.org/github/biocore/qurro/blob/master/example_notebooks/color_compositions/color_example.ipynb) - **Data Summary:** Color composition data from abstract paintings - - Feature rankings: Feature loadings in a compositional biplot + - Feature rankings: Feature loadings in an arbitrary compositional biplot - Qurro used through QIIME 2 or standalone?: Standalone - ["Moving Pictures" tutorial](https://nbviewer.jupyter.org/github/biocore/qurro/blob/master/example_notebooks/moving_pictures/moving_pictures.ipynb) @@ -122,12 +134,20 @@ feature rankings you have. - Feature rankings: [ALDEx2](https://bioconductor.org/packages/release/bioc/html/ALDEx2.html) differentials - Qurro used through QIIME 2 or standalone?: Standalone +### Selection tutorial +There are a lot of different ways to select features in Qurro, and the +interface can be difficult to get used to. This document describes all of these +methods, and provides some examples of where they could be useful in practice. + +- [Selection tutorial](https://nbviewer.jupyter.org/github/biocore/qurro/blob/master/example_notebooks/selection/selection.ipynb) + ### Basic command-line tutorials These tutorials show examples of using Qurro in identical ways both inside and outside of QIIME 2. - [Sleep Apnea tutorial](https://nbviewer.jupyter.org/github/biocore/qurro/blob/master/example_notebooks/DEICODE_sleep_apnea/deicode_example.ipynb) - Feature rankings: feature loadings in a [DEICODE](https://github.com/biocore/DEICODE) biplot + - [Red Sea tutorial](https://nbviewer.jupyter.org/github/biocore/qurro/blob/master/example_notebooks/songbird_red_sea/songbird_example.ipynb) - Feature rankings: [Songbird](https://github.com/biocore/songbird/) differentials @@ -247,20 +267,24 @@ And thanks to a bunch of the Knight Lab for helping name the tool :) ## Citing Qurro -For now, the preferred citation for Qurro is its -[bioRxiv preprint](https://www.biorxiv.org/content/10.1101/2019.12.17.880047v1). -Here's the generated BibTeX: +If you use Qurro in your research, please cite it! +The preferred citation for Qurro is [this manuscript at NAR Genomics and +Bioinformatics](https://academic.oup.com/nargab/article/2/2/lqaa023/5826153). +Here's the BibTeX: ``` -@article {fedarko2019, - author = {Fedarko, Marcus W. and Martino, Cameron and Morton, James T. and Gonz{\'a}lez, Antonio and Rahman, Gibraan and Marotz, Clarisse A. and Minich, Jeremiah J. and Allen, Eric E. and Knight, Rob}, - title = {Visualizing {\textquoteright}omic feature rankings and log-ratios using {Q}urro}, - elocation-id = {2019.12.17.880047}, - year = {2019}, - doi = {10.1101/2019.12.17.880047}, - publisher = {Cold Spring Harbor Laboratory}, - URL = {https://www.biorxiv.org/content/early/2019/12/18/2019.12.17.880047}, - eprint = {https://www.biorxiv.org/content/early/2019/12/18/2019.12.17.880047.full.pdf}, - journal = {bioRxiv} +@article {fedarko2020, + author = {Fedarko, Marcus W and Martino, Cameron and Morton, James T and González, Antonio and Rahman, Gibraan and Marotz, Clarisse A and Minich, Jeremiah J and Allen, Eric E and Knight, Rob}, + title = "{Visualizing ’omic feature rankings and log-ratios using Qurro}", + journal = {NAR Genomics and Bioinformatics}, + volume = {2}, + number = {2}, + year = {2020}, + month = {04}, + issn = {2631-9268}, + doi = {10.1093/nargab/lqaa023}, + url = {https://doi.org/10.1093/nargab/lqaa023}, + note = {lqaa023}, + eprint = {https://academic.oup.com/nargab/article-pdf/2/2/lqaa023/33137933/lqaa023.pdf}, } ``` diff --git a/docs/demos/byrd/index.html b/docs/demos/byrd/index.html index ceadf50f..245100c1 100644 --- a/docs/demos/byrd/index.html +++ b/docs/demos/byrd/index.html @@ -94,7 +94,7 @@ class="questionmark" data-toggle="tooltip" data-placement="top" - title="This slider adjusts the width of each bar in the rank plot." + title='This slider adjusts the width of each bar in the rank plot. In order for this slider to take effect, the "fit bar widths" checkbox must be unchecked.' > Bar width @@ -275,6 +275,9 @@ + @@ -415,21 +418,24 @@ - Autoselecting Features class="questionmark" data-toggle="tooltip" data-placement="top" - title="This group of controls supports autoselection, which is a quick way of taking the log-ratio of the highest- to the lowest- ranked features in the rank plot. The text box in the middle here specifies the number of features (or the percentage of features) to select from each side of the rank plot for a log-ratio. Ties are broken arbitrarily." + title="This group of controls supports autoselection, which is a quick way of taking the log-ratio of the highest- to the lowest- ranked features in the rank plot. The text box in the middle here specifies the number of features (or the percentage of features) to select from each side of the rank plot for a log-ratio. You can put down a negative number to flip the selection (selecting the log-ratio of the lowest- to highest-ranked features). Ties are broken arbitrarily." > Using the current ranking, select the top and bottom @@ -546,6 +552,7 @@

Autoselecting Features

type="number" class="form-control" id="autoSelectNumber" + step="any" /> + class="questionmark mr-0" data-toggle="tooltip" data-placement="top" - title="The next selector adjusts the method used to filter features by a metadata/ranking field. Most of these methods should be self-explanatory; note that 'contains the separated text fragment(s)' filters to features by 1) splitting up the selected field by commas, semicolons, and whitespace, 2) splitting up the input text (in the next text box) in the same way, and 3) selecting features where there's a 'fragment' that matches between that feature's field and the input text. This is useful for filtering on particular taxonomy levels, or by filtering if you have multiple taxonomy level inputs." + title="The next selector adjusts the method used to filter features by a feature metadata / ranking field. Most of these methods should be self-explanatory; please see the Selection Tutorial (linked from Qurro's README) for information on the details of these methods, and some examples of using them." > @@ -654,6 +664,9 @@

- Autoselecting Features

class="questionmark" data-toggle="tooltip" data-placement="top" - title="This group of controls supports autoselection, which is a quick way of taking the log-ratio of the highest- to the lowest- ranked features in the rank plot. The text box in the middle here specifies the number of features (or the percentage of features) to select from each side of the rank plot for a log-ratio. Ties are broken arbitrarily." + title="This group of controls supports autoselection, which is a quick way of taking the log-ratio of the highest- to the lowest- ranked features in the rank plot. The text box in the middle here specifies the number of features (or the percentage of features) to select from each side of the rank plot for a log-ratio. You can put down a negative number to flip the selection (selecting the log-ratio of the lowest- to highest-ranked features). Ties are broken arbitrarily." > Using the current ranking, select the top and bottom @@ -546,6 +552,7 @@

Autoselecting Features

type="number" class="form-control" id="autoSelectNumber" + step="any" /> + class="questionmark mr-0" data-toggle="tooltip" data-placement="top" - title="The next selector adjusts the method used to filter features by a metadata/ranking field. Most of these methods should be self-explanatory; note that 'contains the separated text fragment(s)' filters to features by 1) splitting up the selected field by commas, semicolons, and whitespace, 2) splitting up the input text (in the next text box) in the same way, and 3) selecting features where there's a 'fragment' that matches between that feature's field and the input text. This is useful for filtering on particular taxonomy levels, or by filtering if you have multiple taxonomy level inputs." + title="The next selector adjusts the method used to filter features by a feature metadata / ranking field. Most of these methods should be self-explanatory; please see the Selection Tutorial (linked from Qurro's README) for information on the details of these methods, and some examples of using them." > @@ -654,6 +664,9 @@

- Autoselecting Features

class="questionmark" data-toggle="tooltip" data-placement="top" - title="This group of controls supports autoselection, which is a quick way of taking the log-ratio of the highest- to the lowest- ranked features in the rank plot. The text box in the middle here specifies the number of features (or the percentage of features) to select from each side of the rank plot for a log-ratio. Ties are broken arbitrarily." + title="This group of controls supports autoselection, which is a quick way of taking the log-ratio of the highest- to the lowest- ranked features in the rank plot. The text box in the middle here specifies the number of features (or the percentage of features) to select from each side of the rank plot for a log-ratio. You can put down a negative number to flip the selection (selecting the log-ratio of the lowest- to highest-ranked features). Ties are broken arbitrarily." > Using the current ranking, select the top and bottom @@ -546,6 +552,7 @@

Autoselecting Features

type="number" class="form-control" id="autoSelectNumber" + step="any" /> + class="questionmark mr-0" data-toggle="tooltip" data-placement="top" - title="The next selector adjusts the method used to filter features by a metadata/ranking field. Most of these methods should be self-explanatory; note that 'contains the separated text fragment(s)' filters to features by 1) splitting up the selected field by commas, semicolons, and whitespace, 2) splitting up the input text (in the next text box) in the same way, and 3) selecting features where there's a 'fragment' that matches between that feature's field and the input text. This is useful for filtering on particular taxonomy levels, or by filtering if you have multiple taxonomy level inputs." + title="The next selector adjusts the method used to filter features by a feature metadata / ranking field. Most of these methods should be self-explanatory; please see the Selection Tutorial (linked from Qurro's README) for information on the details of these methods, and some examples of using them." > @@ -654,6 +664,9 @@

- Autoselecting Features

class="questionmark" data-toggle="tooltip" data-placement="top" - title="This group of controls supports autoselection, which is a quick way of taking the log-ratio of the highest- to the lowest- ranked features in the rank plot. The text box in the middle here specifies the number of features (or the percentage of features) to select from each side of the rank plot for a log-ratio. Ties are broken arbitrarily." + title="This group of controls supports autoselection, which is a quick way of taking the log-ratio of the highest- to the lowest- ranked features in the rank plot. The text box in the middle here specifies the number of features (or the percentage of features) to select from each side of the rank plot for a log-ratio. You can put down a negative number to flip the selection (selecting the log-ratio of the lowest- to highest-ranked features). Ties are broken arbitrarily." > Using the current ranking, select the top and bottom @@ -546,6 +552,7 @@

Autoselecting Features

type="number" class="form-control" id="autoSelectNumber" + step="any" /> + class="questionmark mr-0" data-toggle="tooltip" data-placement="top" - title="The next selector adjusts the method used to filter features by a metadata/ranking field. Most of these methods should be self-explanatory; note that 'contains the separated text fragment(s)' filters to features by 1) splitting up the selected field by commas, semicolons, and whitespace, 2) splitting up the input text (in the next text box) in the same way, and 3) selecting features where there's a 'fragment' that matches between that feature's field and the input text. This is useful for filtering on particular taxonomy levels, or by filtering if you have multiple taxonomy level inputs." + title="The next selector adjusts the method used to filter features by a feature metadata / ranking field. Most of these methods should be self-explanatory; please see the Selection Tutorial (linked from Qurro's README) for information on the details of these methods, and some examples of using them." > @@ -654,6 +664,9 @@

- Autoselecting Features

class="questionmark" data-toggle="tooltip" data-placement="top" - title="This group of controls supports autoselection, which is a quick way of taking the log-ratio of the highest- to the lowest- ranked features in the rank plot. The text box in the middle here specifies the number of features (or the percentage of features) to select from each side of the rank plot for a log-ratio. Ties are broken arbitrarily." + title="This group of controls supports autoselection, which is a quick way of taking the log-ratio of the highest- to the lowest- ranked features in the rank plot. The text box in the middle here specifies the number of features (or the percentage of features) to select from each side of the rank plot for a log-ratio. You can put down a negative number to flip the selection (selecting the log-ratio of the lowest- to highest-ranked features). Ties are broken arbitrarily." > Using the current ranking, select the top and bottom @@ -546,6 +552,7 @@

Autoselecting Features

type="number" class="form-control" id="autoSelectNumber" + step="any" /> + class="questionmark mr-0" data-toggle="tooltip" data-placement="top" - title="The next selector adjusts the method used to filter features by a metadata/ranking field. Most of these methods should be self-explanatory; note that 'contains the separated text fragment(s)' filters to features by 1) splitting up the selected field by commas, semicolons, and whitespace, 2) splitting up the input text (in the next text box) in the same way, and 3) selecting features where there's a 'fragment' that matches between that feature's field and the input text. This is useful for filtering on particular taxonomy levels, or by filtering if you have multiple taxonomy level inputs." + title="The next selector adjusts the method used to filter features by a feature metadata / ranking field. Most of these methods should be self-explanatory; please see the Selection Tutorial (linked from Qurro's README) for information on the details of these methods, and some examples of using them." > @@ -654,6 +664,9 @@

- Autoselecting Features

class="questionmark" data-toggle="tooltip" data-placement="top" - title="This group of controls supports autoselection, which is a quick way of taking the log-ratio of the highest- to the lowest- ranked features in the rank plot. The text box in the middle here specifies the number of features (or the percentage of features) to select from each side of the rank plot for a log-ratio. Ties are broken arbitrarily." + title="This group of controls supports autoselection, which is a quick way of taking the log-ratio of the highest- to the lowest- ranked features in the rank plot. The text box in the middle here specifies the number of features (or the percentage of features) to select from each side of the rank plot for a log-ratio. You can put down a negative number to flip the selection (selecting the log-ratio of the lowest- to highest-ranked features). Ties are broken arbitrarily." > Using the current ranking, select the top and bottom @@ -546,6 +552,7 @@

Autoselecting Features

type="number" class="form-control" id="autoSelectNumber" + step="any" /> + class="questionmark mr-0" data-toggle="tooltip" data-placement="top" - title="The next selector adjusts the method used to filter features by a metadata/ranking field. Most of these methods should be self-explanatory; note that 'contains the separated text fragment(s)' filters to features by 1) splitting up the selected field by commas, semicolons, and whitespace, 2) splitting up the input text (in the next text box) in the same way, and 3) selecting features where there's a 'fragment' that matches between that feature's field and the input text. This is useful for filtering on particular taxonomy levels, or by filtering if you have multiple taxonomy level inputs." + title="The next selector adjusts the method used to filter features by a feature metadata / ranking field. Most of these methods should be self-explanatory; please see the Selection Tutorial (linked from Qurro's README) for information on the details of these methods, and some examples of using them." > @@ -654,6 +664,9 @@

+ @@ -138,6 +140,8 @@ element with options", function() { + describe("Populating a ", function() { + it("Clears the option list when called on an already-populated ", "value &2;", - "value<3.>{!$@" + "value<3.>{!$@", ]; dom_utils.populateSelect(selectID, vals, "value &2;"); chai.assert.sameMembers( @@ -117,12 +117,12 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( ); assertSelected(selectID, "value &2;"); }); - it("Throws an error when passed an empty list", function() { - chai.assert.throws(function() { + it("Throws an error when passed an empty list", function () { + chai.assert.throws(function () { dom_utils.populateSelect(selectID, [], "I'm irrelevant!"); }, /options must have at least one value/); }); - it("Creates s when optgroupMap is truthy", function() { + it("Creates s when optgroupMap is truthy", function () { var vals = { g1: ["o1", "o2"], g2: ["o3"] }; dom_utils.populateSelect(selectID, vals, "o2", true); // Check that the select has two optgroups with the correct @@ -133,11 +133,11 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( ); assertSelected(selectID, "o2"); }); - it('Creates global options for optgroups labelled "standalone"', function() { + it('Creates global options for optgroups labelled "standalone"', function () { var vals = { g1: ["o1", "o2"], g2: ["o3"], - standalone: ["o4", "o5"] + standalone: ["o4", "o5"], }; dom_utils.populateSelect(selectID, vals, "o3", true); chai.assert.sameDeepMembers( @@ -146,13 +146,13 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( ); assertSelected(selectID, "o3"); }); - it("Throws an error when passed an empty optgroups object", function() { - chai.assert.throws(function() { + it("Throws an error when passed an empty optgroups object", function () { + chai.assert.throws(function () { dom_utils.populateSelect(selectID, {}, "", true); }, /options must have at least one optgroup specified/); }); - it("Throws an error when passed an optgroups object with all empty children", function() { - chai.assert.throws(function() { + it("Throws an error when passed an optgroups object with all empty children", function () { + chai.assert.throws(function () { dom_utils.populateSelect( selectID, { abc: [], def: [] }, @@ -163,8 +163,8 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( }); }); - describe("Changing the enabled status of an element", function() { - it("Properly disables elements", function() { + describe("Changing the enabled status of an element", function () { + it("Properly disables elements", function () { dom_utils.changeElementsEnabled( ["qurro_enabled_test", "qurro_enabled_test2"], false @@ -172,7 +172,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( testing_utilities.assertEnabled("qurro_enabled_test", false); testing_utilities.assertEnabled("qurro_enabled_test2", false); }); - it("Properly enables elements", function() { + it("Properly enables elements", function () { dom_utils.changeElementsEnabled( ["qurro_enabled_test", "qurro_enabled_test2"], true @@ -182,8 +182,8 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( }); }); - describe("Clearing children of an element", function() { - it("Works properly on nested elements", function() { + describe("Clearing children of an element", function () { + it("Works properly on nested elements", function () { var currID = "qurro_cleardiv_test"; dom_utils.clearDiv(currID); chai.assert.isEmpty(document.getElementById(currID).children); @@ -192,7 +192,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( "grandchild", "child2", "grandchild2", - "greatgrandchild" + "greatgrandchild", ]; for (var c = 0; c < descendantIDs.length; c++) { chai.assert.notExists( @@ -200,7 +200,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( ); } }); - it("Doesn't do anything on empty elements", function() { + it("Doesn't do anything on empty elements", function () { var currID = "qurro_cleardiv_emptyelement"; dom_utils.clearDiv(currID); var ele = document.getElementById(currID); @@ -213,7 +213,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( }); }); - describe("Setting onchange and onclick element bindings", function() { + describe("Setting onchange and onclick element bindings", function () { // Silly little test functions function give4() { return 4; @@ -221,7 +221,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( function give8() { return 8; } - it("Properly sets the onchange attribute", function() { + it("Properly sets the onchange attribute", function () { var eleList = dom_utils.setUpDOMBindings( { qurro_bindingtest1: give4 }, "onchange" @@ -233,7 +233,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( 4 ); }); - it("Properly sets the onclick attribute", function() { + it("Properly sets the onclick attribute", function () { var eleList = dom_utils.setUpDOMBindings( { qurro_bindingtest2: give4 }, "onclick" @@ -243,7 +243,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( 4 ); }); - it("Works with multiple elements at once", function() { + it("Works with multiple elements at once", function () { var eleList = dom_utils.setUpDOMBindings( { qurro_bindingtest1: give8, qurro_bindingtest3: give4 }, "onchange" @@ -263,10 +263,10 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( } }); }); - describe("Informing the user re: sample dropping statistics", function() { - describe('Updating the "main" samples-shown div', function() { + describe("Informing the user re: sample dropping statistics", function () { + describe('Updating the "main" samples-shown div', function () { var htmlSuffix = " currently shown."; - it("Works properly with normal inputs", function() { + it("Works properly with normal inputs", function () { dom_utils.updateMainSampleShownDiv( { a: [1, 2, 3], b: [2, 3, 4, 5] }, 15 @@ -293,30 +293,30 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( ); }); - it("Throws an error if totalSampleCount is 0", function() { - chai.assert.throws(function() { + it("Throws an error if totalSampleCount is 0", function () { + chai.assert.throws(function () { dom_utils.updateMainSampleShownDiv({ a: [1, 2, 3] }, 0); }); - chai.assert.throws(function() { + chai.assert.throws(function () { dom_utils.updateMainSampleShownDiv({ a: [] }, 0); }); - chai.assert.throws(function() { + chai.assert.throws(function () { dom_utils.updateMainSampleShownDiv({}, 0); }); }); - it("Throws an error if droppedSampleCount > totalSampleCount", function() { - chai.assert.throws(function() { + it("Throws an error if droppedSampleCount > totalSampleCount", function () { + chai.assert.throws(function () { dom_utils.updateMainSampleShownDiv({ a: [1, 2, 3] }, 2); }); }); - describe("Computing the size of a union of arrays", function() { - it("Works properly with normal inputs", function() { + describe("Computing the size of a union of arrays", function () { + it("Works properly with normal inputs", function () { chai.assert.equal( dom_utils.unionSize({ a: [1, 2, 3], - b: [2, 3, 4, 5] + b: [2, 3, 4, 5], }), 5 ); @@ -328,7 +328,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( dom_utils.unionSize({ a: [1, 2], b: [2, 3, 4, 5], - c: [6] + c: [6], }), 6 ); @@ -336,12 +336,12 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( dom_utils.unionSize({ a: ["Sample 1", "Sample 2"], b: ["Sample 2", "Sample 3"], - c: ["Sample 1"] + c: ["Sample 1"], }), 3 ); }); - it("Works properly with empty list(s)", function() { + it("Works properly with empty list(s)", function () { chai.assert.equal( dom_utils.unionSize({ a: [], b: [], c: [6] }), 1 @@ -350,7 +350,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( dom_utils.unionSize({ a: ["Sample 1"], b: [], - c: ["Sample 2"] + c: ["Sample 2"], }), 2 ); @@ -359,13 +359,13 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( 0 ); }); - it("Works properly with an empty input mapping", function() { + it("Works properly with an empty input mapping", function () { chai.assert.equal(dom_utils.unionSize({}), 0); }); }); }); - describe("Updating the other sample-dropped divs", function() { - it('Works properly for the x-axis "reason"', function() { + describe("Updating the other sample-dropped divs", function () { + it('Works properly for the x-axis "reason"', function () { var divID = "xAxisSamplesDroppedDiv"; dom_utils.updateSampleDroppedDiv( [1, 2, 3, 4, 5], @@ -386,7 +386,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( .classList.contains("invisible") ); }); - it('Works properly for the color "reason"', function() { + it('Works properly for the color "reason"', function () { var divID = "colorSamplesDroppedDiv"; dom_utils.updateSampleDroppedDiv( [1, 2], @@ -407,7 +407,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( .classList.contains("invisible") ); }); - it("Properly escapes weird characters in field names due to use of .textContent instead of .innerHTML", function() { + it("Properly escapes weird characters in field names due to use of .textContent instead of .innerHTML", function () { var divID = "colorSamplesDroppedDiv"; dom_utils.updateSampleDroppedDiv( [1, 2], @@ -423,7 +423,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( "<p>field!&name; field." ); }); - it('Works properly for the balance "reason"', function() { + it('Works properly for the balance "reason"', function () { var divID = "balanceSamplesDroppedDiv"; dom_utils.updateSampleDroppedDiv( [1, 2, 3, 4], @@ -443,7 +443,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( .classList.contains("invisible") ); }); - it("Makes the div invisible if the number of dropped samples is 0", function() { + it("Makes the div invisible if the number of dropped samples is 0", function () { var divID = "balanceSamplesDroppedDiv"; dom_utils.updateSampleDroppedDiv([], 8, divID, "balance"); chai.assert.isTrue( @@ -452,8 +452,8 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( .classList.contains("invisible") ); }); - it("Throws an error if totalSampleCount is 0", function() { - chai.assert.throws(function() { + it("Throws an error if totalSampleCount is 0", function () { + chai.assert.throws(function () { dom_utils.updateSampleDroppedDiv( [1, 2, 3], 0, @@ -462,7 +462,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( "fieldName" ); }); - chai.assert.throws(function() { + chai.assert.throws(function () { dom_utils.updateSampleDroppedDiv( [], 0, @@ -473,8 +473,8 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( }); }); - it("Throws an error if droppedSampleCount > totalSampleCount", function() { - chai.assert.throws(function() { + it("Throws an error if droppedSampleCount > totalSampleCount", function () { + chai.assert.throws(function () { dom_utils.updateSampleDroppedDiv( [1, 2, 3], 2, diff --git a/qurro/tests/web_tests/tests/test_filter_features.js b/qurro/tests/web_tests/tests/test_filter_features.js index db2e2201..e6a5080e 100644 --- a/qurro/tests/web_tests/tests/test_filter_features.js +++ b/qurro/tests/web_tests/tests/test_filter_features.js @@ -1,4 +1,4 @@ -define(["feature_computation", "mocha", "chai", "testing_utilities"], function( +define(["feature_computation", "mocha", "chai", "testing_utilities"], function ( feature_computation, mocha, chai, @@ -9,34 +9,34 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( datasets: { dataName: [], qurro_feature_metadata_ordering: [], - qurro_rank_ordering: [] - } + qurro_rank_ordering: [], + }, }; - describe("Filtering lists of features based on text/number searching", function() { + describe("Filtering lists of features based on text/number searching", function () { var rpJSON1 = JSON.parse(JSON.stringify(rankPlotSkeleton)); rpJSON1.datasets.dataName.push({ "Feature ID": "Feature 1", n: 1.2, x: null, - same: 5 + same: 5, }); rpJSON1.datasets.dataName.push({ "Feature ID": "Featurelol 2", n: 2, x: "asdf", - same: 5 + same: 5, }); rpJSON1.datasets.dataName.push({ "Feature ID": "Feature 3", n: 3.0, x: "0", - same: 5 + same: 5, }); rpJSON1.datasets.dataName.push({ "Feature ID": "Feature 4|lol", n: 4.5, x: "Infinity", - same: 5 + same: 5, }); rpJSON1.datasets.qurro_rank_ordering.push("n"); rpJSON1.datasets.qurro_rank_ordering.push("x"); @@ -45,7 +45,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( "Feature 1", "Featurelol 2", "Feature 3", - "Feature 4|lol" + "Feature 4|lol", ]; var lolMatches = ["Featurelol 2", "Feature 4|lol"]; @@ -53,42 +53,42 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( rpJSON2.datasets.dataName.push({ "Feature ID": "Feature 1", Taxonomy: - "Archaea;Crenarchaeota;Thermoprotei;Desulfurococcales;Desulfurococcaceae;Desulfurococcus;Desulfurococcus_kamchatkensis" + "Archaea;Crenarchaeota;Thermoprotei;Desulfurococcales;Desulfurococcaceae;Desulfurococcus;Desulfurococcus_kamchatkensis", }); rpJSON2.datasets.dataName.push({ "Feature ID": "Feature 2", Taxonomy: - "Bacteria;Firmicutes;Bacilli;Bacillales;Staphylococcaceae;Staphylococcus;Staphylococcus_aureus" + "Bacteria;Firmicutes;Bacilli;Bacillales;Staphylococcaceae;Staphylococcus;Staphylococcus_aureus", }); rpJSON2.datasets.dataName.push({ "Feature ID": "Feature 3", Taxonomy: - "Bacteria;Firmicutes;Bacilli;Bacillales;Staphylococcaceae;Staphylococcus;Staphylococcus_epidermidis" + "Bacteria;Firmicutes;Bacilli;Bacillales;Staphylococcaceae;Staphylococcus;Staphylococcus_epidermidis", }); rpJSON2.datasets.dataName.push({ "Feature ID": "Feature 4", Taxonomy: - "Viruses;Caudovirales;Myoviridae;Twortlikevirus;Staphylococcus_phage_Twort" + "Viruses;Caudovirales;Myoviridae;Twortlikevirus;Staphylococcus_phage_Twort", }); rpJSON2.datasets.dataName.push({ "Feature ID": "Feature 5", - Taxonomy: "Viruses;Caudovirales;Xanthomonas_phage_Xp15" + Taxonomy: "Viruses;Caudovirales;Xanthomonas_phage_Xp15", }); rpJSON2.datasets.dataName.push({ "Feature ID": "Feature 6", - Taxonomy: "null" + Taxonomy: "null", }); rpJSON2.datasets.dataName.push({ "Feature ID": "Feature 7", - Taxonomy: null + Taxonomy: null, }); rpJSON2.datasets.qurro_feature_metadata_ordering.push("Taxonomy"); var bacteriaMatches = ["Feature 2", "Feature 3"]; var caudoviralesMatches = ["Feature 4", "Feature 5"]; var staphTextMatches = ["Feature 2", "Feature 3", "Feature 4"]; - describe('"Text"-mode searching', function() { - it("Correctly searches through feature IDs", function() { + describe('"Text"-mode searching', function () { + it("Correctly searches through feature IDs", function () { chai.assert.sameOrderedMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -113,7 +113,25 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }); - it("Correctly searches through feature metadata fields", function() { + it("Supports searching for features containing the | character", function () { + // This tests that the "or support" doesn't break things + // In the funky case that the user WANTS to filter to some + // feature(s) that contain the pipe character, default text + // matching will let them do this. + chai.assert.sameOrderedMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + "|", + "Feature ID", + "text" + ) + ), + ["Feature 4|lol"] + ); + }); + + it("Correctly searches through feature metadata fields", function () { // Default text search ignores taxonomic ranks (i.e. semicolons) chai.assert.sameOrderedMembers( testing_utilities.getFeatureIDsFromObjectArray( @@ -162,7 +180,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }); - it("Searching is case *insensitive*", function() { + it("Searching is case *insensitive*", function () { chai.assert.sameOrderedMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -187,7 +205,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }); - it("Doesn't find anything if inputText is empty, but can do just-text-searching using whitespace", function() { + it("Doesn't find anything if inputText is empty, but can do just-text-searching using whitespace", function () { chai.assert.isEmpty( feature_computation.filterFeatures( rpJSON1, @@ -232,7 +250,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( inputFeatures ); }); - it("Ignores actual null values", function() { + it("Ignores actual null values", function () { // Feature 6's Taxonomy value is "null", while Feature 7's // Taxonomy value is null (literally a null value). So // searching methods shouldn't look at Feature 7's Taxonomy @@ -250,8 +268,8 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }); }); - describe('"Does not contain the text" searching', function() { - it("Correctly searches through feature IDs", function() { + describe('"Does not contain the text" searching', function () { + it("Correctly searches through feature IDs", function () { // Unlike the normal "text" searching version of this // particular test, we want to make sure that the features // returned *do not* contain the given text. So, in this case, @@ -283,7 +301,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }); - it("Correctly searches through feature metadata fields", function() { + it("Correctly searches through feature metadata fields", function () { // Default text search ignores taxonomic ranks (i.e. semicolons) // In this case, get all features with taxonomies that do not // contain the text "Staphylococcus". @@ -339,7 +357,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }); - it("Searching is case *insensitive*", function() { + it("Searching is case *insensitive*", function () { chai.assert.sameOrderedMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -363,7 +381,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }); - it("Doesn't find anything if inputText is empty, but can do just-text-searching using whitespace", function() { + it("Doesn't find anything if inputText is empty, but can do just-text-searching using whitespace", function () { // If inputText is empty, the searching will automatically end. chai.assert.isEmpty( feature_computation.filterFeatures( @@ -414,7 +432,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( "Feature 3", "Feature 4", "Feature 5", - "Feature 6" + "Feature 6", ] ); // All feature IDs in rpJSON1 contain a space. Since we're @@ -429,7 +447,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ) ); }); - it("Ignores actual null values", function() { + it("Ignores actual null values", function () { // Feature 6's Taxonomy value is "null", while Feature 7's // Taxonomy value is null (literally a null value). So // searching methods shouldn't look at Feature 7's Taxonomy @@ -452,13 +470,214 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( "Feature 2", "Feature 3", "Feature 4", - "Feature 5" + "Feature 5", ] ); }); }); - describe('"Rank"-mode searching', function() { - it("Finds matching features based on full, exact taxonomic rank", function() { + describe('"or"-mode searching (with | separators)', function () { + it("Correctly searches through feature IDs using 2 strings", function () { + // Checks that "lol | 1" shows us hits containing either "lol" + // or "1" + chai.assert.sameOrderedMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + "lol|1", + "Feature ID", + "or" + ) + ), + ["Feature 1", "Featurelol 2", "Feature 4|lol"] + ); + }); + it("Still works even if no | separators in input text", function () { + chai.assert.sameOrderedMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + "lol", + "Feature ID", + "or" + ) + ), + lolMatches + ); + }); + it("Trims leading/trailing whitespace for each |-separated term", function () { + chai.assert.sameOrderedMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + " lol\t |\n1", + "Feature ID", + "or" + ) + ), + ["Feature 1", "Featurelol 2", "Feature 4|lol"] + ); + }); + it("Trims leading/trailing whitespace even when no | separators", function () { + chai.assert.sameOrderedMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + " \n lol\t \t", + "Feature ID", + "or" + ) + ), + lolMatches + ); + }); + it("Preserves internal whitespace in search terms", function () { + chai.assert.sameOrderedMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + "Feature 1 | Featurelol 2", + "Feature ID", + "or" + ) + ), + ["Feature 1", "Featurelol 2"] + ); + }); + it("(Still) case insensitive", function () { + chai.assert.sameOrderedMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + "LoL | 1", + "Feature ID", + "or" + ) + ), + ["Feature 1", "Featurelol 2", "Feature 4|lol"] + ); + }); + it("Doesn't cause matches due to |s being in feature fields", function () { + // Although one of the features has a feature ID of + // "Feature 4|lol", we can't use |s as part of a query without + // using the exact text matching from before. So the following + // attempt will be unsuccessful. + chai.assert.empty( + feature_computation.filterFeatures( + rpJSON1, + "butts | FeatureButWithExtraStuffAtTheEndOfTheWordLol", + "Feature ID", + "or" + ) + ); + }); + it("Doesn't find anything when input only has |s or whitespace", function () { + var queries = [ + "|", + " | ", + " |", + "||", + "|||", + "||||", + "| | \t | ", + ]; + for (var i = 0; i < queries.length; i++) { + chai.assert.empty( + feature_computation.filterFeatures( + rpJSON1, + queries[i], + "Feature ID", + "or" + ) + ); + } + }); + it("Correctly ignores empty | terms", function () { + chai.assert.sameOrderedMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + " || | | |\t | lol |", + "Feature ID", + "or" + ) + ), + lolMatches + ); + }); + it("Correctly handles 'polyphyletic taxa' searching problem", function () { + var rpJSONn = JSON.parse(JSON.stringify(rankPlotSkeleton)); + // I based the taxonomy information here on what Wikipedia + // said for P. gingivalis and H. gingivalis, please don't + // interpret this test data as the *definitive* taxonomy + // names of any of this stuff + rpJSONn.datasets.dataName.push({ + "Feature ID": "1", + ord: 5, + tax: + "k__Bacteria;p__Bacteroidetes;c__Bacteroidetes;o__Bacteroidales;f__Porphyromonadaceae;g__Porphyromonas;s__gingivalis", + }); + rpJSONn.datasets.dataName.push({ + "Feature ID": "2", + ord: 5, + tax: + "k__Animalia;p__Nematoda;c__Secernentea;o__Rhabditida;f__Panagrolaimidae;g__Halicephalobus;s__gingivalis", + }); + rpJSONn.datasets.dataName.push({ + "Feature ID": "3", + ord: 5, + tax: + "k__Bacteria;p__Firmicutes;c__Bacilli;o__Bacillales;f__Staphylococcaceae;g__Staphylococcus;s__aureus", + }); + rpJSONn.datasets.dataName.push({ + "Feature ID": "4", + ord: 5, + tax: + "k__Whatever;p__Something;c__This;o__Isnt;f__Supposed;g__ToBe;s__selectedlol", + }); + rpJSONn.datasets.dataName.push({ + "Feature ID": "5", + ord: 5, + tax: + "k__Bacteria;p__Bacteroidetes;c__Bacteroidetes;o__Bacteroidales;f__Porphyromonadaceae;g__Porphyromonas;s__levii", + }); + rpJSONn.datasets.qurro_rank_ordering.push("ord"); + rpJSONn.datasets.qurro_feature_metadata_ordering.push("tax"); + // Test that we can isolate a particular genus and species + // as part of a query WITHOUT getting individual hits to that + // genus and species. (Note that features 2 and 5 are NOT + // selected, which is as intended.) + chai.assert.sameOrderedMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSONn, + "g__Porphyromonas;s__gingivalis | s__aureus", + "tax", + "or" + ) + ), + ["1", "3"] + ); + }); + it("Ignores non-(string or number) values, including nulls", function () { + // This test is just copied from the corresponding normal text + // filtering tests above. TLDR: Feature 6's Taxonomy value is + // "null", and Feature 7's Taxonomy value is null (an actual + // null, not a string). + chai.assert.sameMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON2, + "null", + "Taxonomy", + "or" + ) + ), + ["Feature 6"] + ); + }); + }); + describe('"Rank"-mode searching', function () { + it("Finds matching features based on full, exact taxonomic rank", function () { chai.assert.sameOrderedMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -489,7 +708,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( // A TODO here is reducing the redunancy in these tests, but it's // not like efficiency in the JS testing process is a super huge // priority for us right now. - it("Searching is (still) case insensitive", function() { + it("Searching is (still) case insensitive", function () { chai.assert.sameOrderedMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -513,7 +732,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ["Feature 1", "Feature 3", "Feature 4|lol"] ); }); - it("Doesn't find anything if inputText is empty or contains just whitespace/separator characters", function() { + it("Doesn't find anything if inputText is empty or contains just whitespace/separator characters", function () { /* Just a helper function to alleviate redundant code here. * * Asserts that filterFeatures() with the given input text is @@ -544,7 +763,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( assertEmpty(" ,; \t ;;\n"); assertEmpty("\n ,; \t ;;\n"); }); - it("Ignores actual null values", function() { + it("Ignores actual null values", function () { chai.assert.sameMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -558,8 +777,8 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }); }); - describe("Basic number-based searching", function() { - it('Less than (< or "lt") finds features < a given value', function() { + describe("Basic number-based searching", function () { + it('Less than (< or "lt") finds features < a given value', function () { chai.assert.sameMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -605,7 +824,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( inputFeatures ); }); - it('Greater than (> or "gt") finds features > a given value', function() { + it('Greater than (> or "gt") finds features > a given value', function () { chai.assert.sameMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -651,7 +870,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( inputFeatures ); }); - it('Less than or equal (<= or "lte") finds features <= a given value', function() { + it('Less than or equal (<= or "lte") finds features <= a given value', function () { chai.assert.sameMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -685,7 +904,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( inputFeatures ); }); - it('Greater than or equal (>= or "gte") finds features >= a given value', function() { + it('Greater than or equal (>= or "gte") finds features >= a given value', function () { chai.assert.sameMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -719,7 +938,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( inputFeatures ); }); - it("Non-finite / non-numeric feature field values are ignored", function() { + it("Non-finite / non-numeric feature field values are ignored", function () { chai.assert.sameMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -732,7 +951,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ["Feature 3"] ); }); - it("Non-finite / non-numeric input field values are ignored", function() { + it("Non-finite / non-numeric input field values are ignored", function () { chai.assert.isEmpty( feature_computation.filterFeatures( rpJSON1, @@ -793,15 +1012,15 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ) ); }); - describe("operatorToCompareFunc()", function() { - it("Passing in an invalid operator results in an error", function() { + describe("operatorToCompareFunc()", function () { + it("Passing in an invalid operator results in an error", function () { // This should never happen since we screen for invalid // operators in filterFeatures(), but still good to check for - chai.assert.throws(function() { + chai.assert.throws(function () { feature_computation.operatorToCompareFunc("asdf", 3); }, /unrecognized operator passed/); }); - it("Passing in a valid operator results in a valid comparison function", function() { + it("Passing in a valid operator results in a valid comparison function", function () { // The other basic numerical comparison operators (lte, gt, // gte) have already been unit-tested above. This just // double-checks that operatorToCompareFunc() itself works @@ -817,15 +1036,15 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( }); }); }); - describe("Autoselecting features", function() { + describe("Autoselecting features", function () { var literalSearchTypes = ["autoLiteralTop", "autoLiteralBot"]; var percentSearchTypes = ["autoPercentTop", "autoPercentBot"]; var autoSearchTypes = literalSearchTypes.concat(percentSearchTypes); - describe("Inputs are in numbers of features", function() { - it("Returns empty for 0 features", function() { + describe("Inputs are in numbers of features", function () { + it("Returns empty for 0 features", function () { var literalSearchTypes = [ "autoLiteralTop", - "autoLiteralBot" + "autoLiteralBot", ]; for (var s = 0; s < literalSearchTypes.length; s++) { chai.assert.empty( @@ -838,7 +1057,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); } }); - it("Gets all features if the input number is > number of features", function() { + it("Gets all features if magnitude of the input number is > number of features", function () { var vals = [ "4.1", "4.2", @@ -846,7 +1065,14 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( "4.4", "20", "100", - "99999" + "99999", + "-4.1", + "-4.2", + "-4.3", + "-4.4", + "-20", + "-100", + "-99999", ]; for (var i = 0; i < vals.length; i++) { for (var s = 0; s < literalSearchTypes.length; s++) { @@ -864,7 +1090,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( } } }); - it("Works properly when 1 feature requested", function() { + it("Works properly when 1 feature requested", function () { chai.assert.sameMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -888,7 +1114,31 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ["Feature 1"] ); }); - it("Works properly when 2 features requested", function() { + it("Works properly when -1 feature requested", function () { + chai.assert.sameMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + "-1", + "n", + "autoLiteralTop" + ) + ), + ["Feature 1"] + ); + chai.assert.sameMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + "-1", + "n", + "autoLiteralBot" + ) + ), + ["Feature 4|lol"] + ); + }); + it("Works properly when 2 features requested", function () { chai.assert.sameMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -912,9 +1162,33 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ["Feature 1", "Featurelol 2"] ); }); - it("Chooses correct number of features when all have equal ranking column value", function() { + it("Works properly when -2 features requested", function () { + chai.assert.sameMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + "-2", + "n", + "autoLiteralTop" + ) + ), + ["Feature 1", "Featurelol 2"] + ); + chai.assert.sameMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + "-2", + "n", + "autoLiteralBot" + ) + ), + ["Feature 3", "Feature 4|lol"] + ); + }); + it("Chooses correct number of features when all have equal ranking column value", function () { for (var s = 0; s < literalSearchTypes.length; s++) { - for (var i = 0; i < 5; i++) { + for (var i = -4; i < 5; i++) { chai.assert.lengthOf( feature_computation.filterFeatures( rpJSON1, @@ -922,14 +1196,38 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( "same", literalSearchTypes[s] ), - i + Math.abs(i) ); } } }); + it("Takes the floor of the number of features requested", function () { + chai.assert.sameMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + "-1.99", + "n", + "autoLiteralBot" + ) + ), + ["Feature 4|lol"] + ); + chai.assert.sameMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + "1.99", + "n", + "autoLiteralBot" + ) + ), + ["Feature 1"] + ); + }); }); - describe("Inputs are in percentages of features", function() { - it("Works properly when math is easy (top 25% of 4 features)", function() { + describe("Inputs are in percentages of features", function () { + it("Works properly when math is easy (top 25% of 4 features)", function () { chai.assert.sameMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -942,7 +1240,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ["Feature 4|lol"] ); }); - it("Works properly when math is less easy (bottom 57% of 4 features)", function() { + it("Works properly when math is less easy (bottom 57% of 4 features)", function () { chai.assert.sameMembers( testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( @@ -955,7 +1253,48 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ["Feature 1", "Featurelol 2"] ); }); - it("Returns empty if 0% of features are requested", function() { + it("Works properly with top -25% of 4 features", function () { + chai.assert.sameMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + "-25", + "n", + "autoPercentTop" + ) + ), + ["Feature 1"] + ); + }); + it("Works properly with bottom -57% of 4 features", function () { + chai.assert.sameMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + "-57", + "n", + "autoPercentBot" + ) + ), + ["Feature 3", "Feature 4|lol"] + ); + }); + it("Flooring is done consistently with negative-number inputs", function () { + // 74% of 4 is 2.96 features from each side; this should be + // floored down to 2, even if we pass in -74% + chai.assert.sameMembers( + testing_utilities.getFeatureIDsFromObjectArray( + feature_computation.filterFeatures( + rpJSON1, + "-74", + "n", + "autoPercentBot" + ) + ), + ["Feature 3", "Feature 4|lol"] + ); + }); + it("Returns empty if 0% of features are requested", function () { for (var s = 0; s < percentSearchTypes.length; s++) { chai.assert.empty( feature_computation.filterFeatures( @@ -967,14 +1306,20 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); } }); - it("Gets all features if the input number is > 100%", function() { + it("Gets all features if the input number is > 100% or < -100%", function () { var vals = [ "100.00001", "101", "102", "999", "99999", - "999999" + "999999", + "-100.00001", + "-101", + "-102", + "-999", + "-99999", + "-999999", ]; for (var i = 0; i < vals.length; i++) { for (var s = 0; s < percentSearchTypes.length; s++) { @@ -992,9 +1337,9 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( } } }); - it("Chooses correct percentage of features when all have equal ranking column value", function() { + it("Chooses correct percentage of features when all have equal ranking column value", function () { for (var s = 0; s < percentSearchTypes.length; s++) { - for (var i = 0; i < 125; i += 25) { + for (var i = -100; i < 125; i += 25) { chai.assert.lengthOf( feature_computation.filterFeatures( rpJSON1, @@ -1002,13 +1347,13 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( "same", percentSearchTypes[s] ), - i / 25 + Math.abs(i) / 25 ); } } }); }); - it("Works properly when >50% of features requested", function() { + it("Works properly when >50% of features requested", function () { /* Tests all auto-selection search types when we expect * either *all* features to be returned, or 3/4 features to * be returned @@ -1055,7 +1400,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); } }); - it("Returns empty if input number isn't a finite, nonnegative number", function() { + it("Returns empty if input number isn't a finite number", function () { var invalidValsToTest = [ "asdf", "NaN", @@ -1068,10 +1413,6 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( NaN, Infinity, -Infinity, - -1, - "-1", - "-100.23", - -100.23 ]; for (var i = 0; i < invalidValsToTest.length; i++) { for (var s = 0; s < autoSearchTypes.length; s++) { @@ -1086,12 +1427,12 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( } } }); - it("Throws an error if a ranking isn't present in all features", function() { + it("Throws an error if a ranking isn't present in all features", function () { // NOTE: we can use a number (2) here because we're calling // extremeFilterFeatures() directly, instead of calling // filterFeatures() first (which expects inputText to be a // string) - chai.assert.throws(function() { + chai.assert.throws(function () { // We get a list of "feature rows" to mimic what // filterFeatures() would give to extremeFilterFeatures() var potentialFeatures = rpJSON1.datasets[rpJSON1.data.name]; @@ -1103,8 +1444,8 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }, /aosdifj ranking not present and\/or numeric for all features/); }); - it("Throws an error if a ranking isn't numeric for all features", function() { - chai.assert.throws(function() { + it("Throws an error if a ranking isn't numeric for all features", function () { + chai.assert.throws(function () { // We get a list of "feature rows" to mimic what // filterFeatures() would give to extremeFilterFeatures() var potentialFeatures = rpJSON1.datasets[rpJSON1.data.name]; @@ -1117,8 +1458,8 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( }, /x ranking not present and\/or numeric for all features/); }); }); - describe("existsIntersection()", function() { - it("Returns true if an intersection exists", function() { + describe("existsIntersection()", function () { + it("Returns true if an intersection exists", function () { chai.assert.isTrue( feature_computation.existsIntersection( ["a", "b", "c"], @@ -1129,7 +1470,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( feature_computation.existsIntersection(["a"], ["a"]) ); }); - it("Returns false if no intersection exists", function() { + it("Returns false if no intersection exists", function () { chai.assert.isFalse( feature_computation.existsIntersection( ["a", "b", "c"], @@ -1140,7 +1481,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( feature_computation.existsIntersection(["a"], ["b"]) ); }); - it("Returns false when >= 1 input array is empty", function() { + it("Returns false when >= 1 input array is empty", function () { chai.assert.isFalse( feature_computation.existsIntersection([], []) ); @@ -1152,8 +1493,8 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }); }); - describe("textToRankArray()", function() { - it("Works with basic, simple taxonomy strings", function() { + describe("textToRankArray()", function () { + it("Works with basic, simple taxonomy strings", function () { chai.assert.sameOrderedMembers( feature_computation.textToRankArray( "Viruses;Caudovirales;Myoviridae;Twortlikevirus;Staphylococcus_phage_Twort" @@ -1163,11 +1504,11 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( "Caudovirales", "Myoviridae", "Twortlikevirus", - "Staphylococcus_phage_Twort" + "Staphylococcus_phage_Twort", ] ); }); - it("Works with Greengenes-style taxonomy strings", function() { + it("Works with Greengenes-style taxonomy strings", function () { chai.assert.sameOrderedMembers( feature_computation.textToRankArray( "k__Bacteria; p__Bacteroidetes; c__Bacteroidia; o__Bacteroidales; f__Bacteroidaceae; g__Bacteroides; s__" @@ -1179,11 +1520,11 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( "o__Bacteroidales", "f__Bacteroidaceae", "g__Bacteroides", - "s__" + "s__", ] ); }); - it("Works with SILVA-style taxonomy strings", function() { + it("Works with SILVA-style taxonomy strings", function () { chai.assert.sameOrderedMembers( feature_computation.textToRankArray( // Thanks to Justin for the example data @@ -1195,11 +1536,11 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( "D_2__Bacteroidia", "D_3__Bacteroidales", "D_4__Bacteroidaceae", - "D_5__Bacteroides" + "D_5__Bacteroides", ] ); }); - it('Ignores "empty" taxonomic ranks', function() { + it('Ignores "empty" taxonomic ranks', function () { chai.assert.sameOrderedMembers( // Currently, we don't treat __ specially, so it'll get // treated as a taxonomic rank. (See @@ -1220,7 +1561,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ["Viruses", "Caudovirales", "lol"] ); }); - it("Returns [] when strings without actual text are passed in", function() { + it("Returns [] when strings without actual text are passed in", function () { chai.assert.isEmpty(feature_computation.textToRankArray("")); chai.assert.isEmpty( feature_computation.textToRankArray(" \n \t ") @@ -1229,7 +1570,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( feature_computation.textToRankArray(" ; ") ); }); - it("Behaves as expected when passed a comma-separated list", function() { + it("Behaves as expected when passed a comma-separated list", function () { chai.assert.sameOrderedMembers( feature_computation.textToRankArray("Viruses, Bacteria"), ["Viruses", "Bacteria"] @@ -1243,7 +1584,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ["Viruses"] ); }); - it("Separates on spaces, in addition to semicolons and commas", function() { + it("Separates on spaces, in addition to semicolons and commas", function () { chai.assert.sameOrderedMembers( feature_computation.textToRankArray( "Abc def ghi ;,; j[k]l m(nop) , qrs;tuv wxy|z" @@ -1256,11 +1597,11 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( "m(nop)", "qrs", "tuv", - "wxy|z" + "wxy|z", ] ); }); - it("Works with oddly formatted input lists", function() { + it("Works with oddly formatted input lists", function () { chai.assert.sameOrderedMembers( feature_computation.textToRankArray( "Viruses;Bacteria , Stuff 2; lol,5" @@ -1283,13 +1624,13 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( "c__Bacilli", "o__Bacillales", "f__Staphylococcaceae", - "lol" + "lol", ] ); }); }); - describe("tryTextSearchable()", function() { - it("Lowercases (but otherwise doesn't modify) strings", function() { + describe("tryTextSearchable()", function () { + it("Lowercases (but otherwise doesn't modify) strings", function () { chai.assert.equal( feature_computation.tryTextSearchable("abc"), "abc" @@ -1313,7 +1654,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( "null" ); }); - it("Converts numbers to strings", function() { + it("Converts numbers to strings", function () { chai.assert.equal( feature_computation.tryTextSearchable(3.14), "3.14" @@ -1323,7 +1664,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( "5" ); }); - it("Returns null when a non-string + non-number passed in", function() { + it("Returns null when a non-string + non-number passed in", function () { chai.assert.isNull(feature_computation.tryTextSearchable([3])); chai.assert.isNull( feature_computation.tryTextSearchable([3, 4, 5]) @@ -1343,9 +1684,9 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }); }); - describe("Various filterFeatures() logistics", function() { - it("Throws an error when nonexistent feature metadata field passed", function() { - chai.assert.throws(function() { + describe("Various filterFeatures() logistics", function () { + it("Throws an error when nonexistent feature metadata field passed", function () { + chai.assert.throws(function () { testing_utilities.getFeatureIDsFromObjectArray( feature_computation.filterFeatures( rpJSON1, @@ -1354,9 +1695,9 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( "text" ) ); - }); + }, /featureField "Taxonomy" not found in data/); // test that feature metadata field names are case-sensitive - chai.assert.throws(function() { + chai.assert.throws(function () { feature_computation.filterFeatures( rpJSON1, "I'm the input text!", @@ -1365,7 +1706,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }); // test that feature metadata field names "preserve" whitespace - chai.assert.throws(function() { + chai.assert.throws(function () { feature_computation.filterFeatures( rpJSON1, "I'm the input text!", @@ -1374,8 +1715,8 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }); }); - it("Throws an error when nonexistent search type passed", function() { - chai.assert.throws(function() { + it("Throws an error when nonexistent search type passed", function () { + chai.assert.throws(function () { feature_computation.filterFeatures( rpJSON1, "I'm irrelevant!", @@ -1384,7 +1725,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }); // test that search type names are case-sensitive - chai.assert.throws(function() { + chai.assert.throws(function () { feature_computation.filterFeatures( rpJSON2, "I'm the input text!", @@ -1393,7 +1734,7 @@ define(["feature_computation", "mocha", "chai", "testing_utilities"], function( ); }); }); - it("Returns [] when inputText.length is 0", function() { + it("Returns [] when inputText.length is 0", function () { chai.assert.isEmpty( feature_computation.filterFeatures( rpJSON1, diff --git a/qurro/tests/web_tests/tests/test_identify_sample_ids.js b/qurro/tests/web_tests/tests/test_identify_sample_ids.js index 53420c4a..58743022 100644 --- a/qurro/tests/web_tests/tests/test_identify_sample_ids.js +++ b/qurro/tests/web_tests/tests/test_identify_sample_ids.js @@ -1,4 +1,4 @@ -define(["display", "mocha", "chai"], function(display, mocha, chai) { +define(["display", "mocha", "chai"], function (display, mocha, chai) { // Just the sample plot from the python "matching" integration test // prettier-ignore var samplePlotJSON = {"$schema": "https://vega.github.io/schema/vega-lite/v3.3.0.json", "autosize": {"resize": true}, "background": "#FFFFFF", "config": {"axis": {"labelBound": true}, "mark": {"tooltip": null}, "range": {"category": {"scheme": "tableau10"}, "ramp": {"scheme": "blues"}}, "view": {"height": 300, "width": 400}}, "data": {"name": "data-17ad6d7eb8d11fdb67d65d9f4abd5654"}, "datasets": {"data-17ad6d7eb8d11fdb67d65d9f4abd5654": [{"Metadata1": "1", "Metadata2": "2", "Metadata3": "3", "Sample ID": "Sample1", "qurro_balance": null}, {"Metadata1": "4", "Metadata2": "5", "Metadata3": "6", "Sample ID": "Sample2", "qurro_balance": null}, {"Metadata1": "7", "Metadata2": "8", "Metadata3": "9", "Sample ID": "Sample3", "qurro_balance": null}, {"Metadata1": "13", "Metadata2": "14", "Metadata3": "15", "Sample ID": "Sample5", "qurro_balance": null}, {"Metadata1": "16", "Metadata2": "17", "Metadata3": "18", "Sample ID": "Sample6", "qurro_balance": null}, {"Metadata1": "19", "Metadata2": "20", "Metadata3": "21", "Sample ID": "Sample7", "qurro_balance": null}], "qurro_sample_metadata_fields": ["Metadata1", "Metadata2", "Metadata3", "Sample ID"]}, "encoding": {"color": {"field": "Metadata1", "type": "nominal"}, "tooltip": [{"field": "Sample ID", "type": "nominal"}, {"field": "qurro_balance", "type": "quantitative"}], "x": {"axis": {"labelAngle": -45}, "field": "Metadata1", "scale": {"zero": false}, "type": "nominal"}, "y": {"field": "qurro_balance", "scale": {"zero": false}, "title": "Current Natural Log-Ratio", "type": "quantitative"}}, "mark": {"type": "circle"}, "selection": {"selector006": {"bind": "scales", "encodings": ["x", "y"], "type": "interval"}}, "title": "Samples"}; @@ -6,8 +6,8 @@ define(["display", "mocha", "chai"], function(display, mocha, chai) { // metadata fields defined var badSpec = JSON.parse(JSON.stringify(samplePlotJSON)); badSpec.datasets[badSpec.data.name] = [{}, {}, {}, {}, {}, {}]; - describe("Identifying sample IDs in the sample plot JSON", function() { - it("Properly identifies all sample IDs", function() { + describe("Identifying sample IDs in the sample plot JSON", function () { + it("Properly identifies all sample IDs", function () { chai.assert.sameMembers( [ "Sample1", @@ -15,12 +15,12 @@ define(["display", "mocha", "chai"], function(display, mocha, chai) { "Sample3", "Sample5", "Sample6", - "Sample7" + "Sample7", ], display.RRVDisplay.identifySampleIDs(samplePlotJSON) ); }); - it("Returns an empty list if no samples in dataset", function() { + it("Returns an empty list if no samples in dataset", function () { chai.assert.empty(display.RRVDisplay.identifySampleIDs(badSpec)); }); }); diff --git a/qurro/tests/web_tests/tests/test_rrvdisplay.js b/qurro/tests/web_tests/tests/test_rrvdisplay.js index 31534338..cc8b1096 100644 --- a/qurro/tests/web_tests/tests/test_rrvdisplay.js +++ b/qurro/tests/web_tests/tests/test_rrvdisplay.js @@ -1,4 +1,5 @@ -define(["mocha", "chai", "testing_utilities", "dom_utils"], function( +define(["vega", "mocha", "chai", "testing_utilities", "dom_utils"], function ( + vega, mocha, chai, testing_utilities, @@ -12,7 +13,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( // prettier-ignore var countJSON = {"Taxon1": {"Sample2": 1.0, "Sample3": 2.0, "Sample5": 4.0, "Sample6": 5.0, "Sample7": 6.0}, "Taxon2": {"Sample1": 6.0, "Sample2": 5.0, "Sample3": 4.0, "Sample5": 2.0, "Sample6": 1.0}, "Taxon3": {"Sample1": 2.0, "Sample2": 3.0, "Sample3": 4.0, "Sample5": 4.0, "Sample6": 3.0, "Sample7": 2.0}, "Taxon4": {"Sample1": 1.0, "Sample2": 1.0, "Sample3": 1.0, "Sample5": 1.0, "Sample6": 1.0, "Sample7": 1.0}, "Taxon5": {"Sample3": 1.0, "Sample5": 2.0}}; - describe("Dynamic RRVDisplay class functionality", function() { + describe("Dynamic RRVDisplay class functionality", function () { var rrv, dataName; async function resetRRVDisplay() { await rrv.destroy(true, true, true); @@ -33,7 +34,33 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( ); chai.assert.notExists(rrv.samplePlotJSON.encoding.x.axis); } - before(async function() { + function testBoxplotEncodings(xField) { + chai.assert.equal("boxplot", rrv.samplePlotJSON.mark.type); + chai.assert.exists(rrv.samplePlotJSON.mark.median); + chai.assert.equal("#000000", rrv.samplePlotJSON.mark.median.color); + // Also, the color field encoding should match the x-axis + // field encoding and be nominal + chai.assert.equal(xField, rrv.samplePlotJSON.encoding.x.field); + chai.assert.equal(xField, rrv.samplePlotJSON.encoding.color.field); + chai.assert.equal( + "nominal", + rrv.samplePlotJSON.encoding.color.type + ); + } + async function testSwitchToBoxplot(currXField) { + await document.getElementById("boxplotCheckbox").click(); + var xScaleEle = document.getElementById("xAxisScale"); + xScaleEle.value = "nominal"; + await xScaleEle.onchange(); + // Now the sample plot should be a boxplot + testBoxplotEncodings(currXField); + // In "boxplot mode", the color controls (and the sample border + // checkbox) should all be disabled + testing_utilities.assertEnabled("colorField", false); + testing_utilities.assertEnabled("colorScale", false); + testing_utilities.assertEnabled("borderCheckbox", false); + } + before(async function () { rrv = testing_utilities.getNewRRVDisplay( rankPlotJSON, samplePlotJSON, @@ -42,11 +69,11 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( dataName = rrv.samplePlotJSON.data.name; await rrv.makePlots(); }); - after(async function() { + after(async function () { await rrv.destroy(true, true, true); }); - it("Initializes an RRVDisplay object", function() { + it("Initializes an RRVDisplay object", function () { // We don't bother checking that the JSONs are equal b/c all we do // is just set them equal. Also, there are some things done on init // (that modify the RRVDisplay object's copy of the JSON) that @@ -75,16 +102,71 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( ); }); - it("RRVDisplay.validateSampleID() identifies nonexistent sample IDs", function() { - chai.assert.doesNotThrow(function() { + it("Adds the 'qiimediscrete' (Classic QIIME Colors) color scheme", function () { + // 1. check that the scheme was added to Vega + // (see https://vega.github.io/vega/docs/schemes/#registering-additional-schemes) + chai.assert.sameOrderedMembers(vega.scheme("qiimediscrete"), [ + "#ff0000", + "#0000ff", + "#f27304", + "#008000", + "#91278d", + "#ffff00", + "#7cecf4", + "#f49ac2", + "#5da09e", + "#6b440b", + "#808080", + "#f79679", + "#7da9d8", + "#fcc688", + "#80c99b", + "#a287bf", + "#fff899", + "#c49c6b", + "#c0c0c0", + "#ed008a", + "#00b6ff", + "#a54700", + "#808000", + "#008080", + ]); + // 2. check that the scheme is accessible in the Categorical color + // scheme selection. There is definitely a more elegant way to do + // this but here we use a simple DOM way based on + // https://stackoverflow.com/a/19130255/10730311. + // + // (NOTE: this is kind of a silly test because this color scheme is + // present in the HTML independently of any of the JS stuff. Also, + // the web test index.html and the actual index.html are separate + // files as of writing, so I had to manually add in the QIIME color + // scheme for both of them. Suffice it to say that the first part + // of this test is much more important.) + var ccsSelect = document.getElementById("catColorScheme"); + var foundQIIMEColors = false; + for (var i = 0; i < ccsSelect.length; i++) { + if (ccsSelect[i].value === "qiimediscrete") { + chai.assert.equal( + ccsSelect[i].text, + "Classic QIIME Colors" + ); + foundQIIMEColors = true; + break; + } + } + chai.assert.isTrue(foundQIIMEColors); + }); + + it("RRVDisplay.validateSampleID() identifies nonexistent sample IDs", function () { + chai.assert.doesNotThrow(function () { rrv.validateSampleID("Sample2"); }); - chai.assert.throws(function() { + chai.assert.throws(function () { rrv.validateSampleID("SuperFakeSampleName"); }); }); - describe("Selecting features to update the plots", function() { + describe("Selecting features to update the plots", function () { /* Resets an RRVDisplay object so we're working with a blank slate * before test(s). */ @@ -131,11 +213,11 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( await document.getElementById("multiFeatureButton").onclick(); } - describe("Single-feature selections", function() { - beforeEach(async function() { + describe("Single-feature selections", function () { + beforeEach(async function () { await resetRRVDisplay(); }); - it("Doesn't do anything if .newFeatureLow and/or .newFeatureHigh is null or undefined", async function() { + it("Doesn't do anything if .newFeatureLow and/or .newFeatureHigh is null or undefined", async function () { // Since we just called resetRRVDisplay(), // rrv.newFeatureLow and rrv.newFeatureHigh should both be // undefined. @@ -174,7 +256,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( rrv.newFeatureHigh = undefined; await updateSingleAndCheckAllBalancesNull(); }); - it("Doesn't do anything if the newly selected features don't differ from the old ones", async function() { + it("Doesn't do anything if the newly selected features don't differ from the old ones", async function () { rrv.oldFeatureLow = testing_utilities.getFeatureRow( rrv, "Taxon1" @@ -193,7 +275,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( ); await updateSingleAndCheckAllBalancesNull(); }); - it("Works properly when actually changing the plots", async function() { + it("Works properly when actually changing the plots", async function () { rrv.newFeatureHigh = testing_utilities.getFeatureRow( rrv, "Taxon1" @@ -273,7 +355,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( .classList.contains("invisible") ); }); - it("Selecting the same feature as numerator and denominator causes balances to be 0, and causes a warning about 'Both'-classification features to appear; this warning goes away when next selecting a log-ratio without 'Both'-classified features", async function() { + it("Selecting the same feature as numerator and denominator causes balances to be 0, and causes a warning about 'Both'-classification features to appear; this warning goes away when next selecting a log-ratio without 'Both'-classified features", async function () { // The only way a feature is in both the numerator and // denominator for single-feature selections is if the // same feature is double-clicked @@ -334,8 +416,8 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( ); }); }); - describe("Multi-feature selections (text-based filtering, one basic case)", function() { - beforeEach(async function() { + describe("Multi-feature selections (text-based filtering, one basic case)", function () { + beforeEach(async function () { await runFeatureFiltering( "Feature ID", "Taxon", @@ -345,7 +427,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( "text" ); }); - it("Properly updates topFeatures and botFeatures", function() { + it("Properly updates topFeatures and botFeatures", function () { chai.assert.sameMembers( ["Taxon1", "Taxon2", "Taxon3", "Taxon4", "Taxon5"], testing_utilities.getFeatureIDsFromObjectArray( @@ -359,7 +441,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( ) ); }); - it("Properly updates the sample plot balances", function() { + it("Properly updates the sample plot balances", function () { // same delta as in the compute balances tests var delta = 0.00001; // Sample1 @@ -399,7 +481,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( delta ); }); - it("Properly updates the rank plot classifications", function() { + it("Properly updates the rank plot classifications", function () { // we've selected the log-ratio of (all features) over // (taxon 3) var rpData = @@ -418,22 +500,22 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( } } }); - it('Properly updates the "feature text" headers', function() { + it('Properly updates the "feature text" headers', function () { testing_utilities.checkHeaders(5, 1, 5); testing_utilities.checkDataTable("topFeaturesDisplay", { Taxon1: [5, 6, 7, 0, 4, null, null], Taxon2: [1, 2, 3, 0, 4, null, null], Taxon3: [4, 5, 6, 0, 4, "Yeet", 100], Taxon4: [9, 8, 7, 0, 4, null, null], - Taxon5: [6, 5, 4, 0, 4, "null", "lol"] + Taxon5: [6, 5, 4, 0, 4, "null", "lol"], }); testing_utilities.checkDataTable("botFeaturesDisplay", { - Taxon3: [4, 5, 6, 0, 4, "Yeet", 100] + Taxon3: [4, 5, 6, 0, 4, "Yeet", 100], }); }); }); - describe("Multi-feature selections (text-based filtering, corner cases)", function() { - beforeEach(async function() { + describe("Multi-feature selections (text-based filtering, corner cases)", function () { + beforeEach(async function () { await resetRRVDisplay(); }); function assertWarningShown(numCommonFeatures) { @@ -453,11 +535,11 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( " feature(s)" ); } - describe("Empty search fields provided", function() { + describe("Empty search fields provided", function () { it("Clears feature classifications and sample balances"); }); - describe("Selecting the same feature(s) for both the numerator and denominator triggers a warning about 'Both'-classification features", function() { - it("Works when only one feature is common", async function() { + describe("Selecting the same feature(s) for both the numerator and denominator triggers a warning about 'Both'-classification features", function () { + it("Works when only one feature is common", async function () { // Filter numerator to all features (since they all have // feature IDs including the text "Taxon", and filter // denominator to just the "Taxon3" feature. @@ -473,7 +555,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( ); assertWarningShown(1); }); - it("Works when multiple features are common", async function() { + it("Works when multiple features are common", async function () { // Try again, but now select multiple overlapping features // for the denominator (select all features with an // "Intercept" differential of less than 9 -- this @@ -491,7 +573,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( }); }); }); - describe("Multi-feature selections (auto-selection)", function() { + describe("Multi-feature selections (auto-selection)", function () { /* Utility function that lets us essentially integration-test * the auto-selection functionality. Cool! */ @@ -502,10 +584,10 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( document.getElementById("autoSelectType").value = inputType; await document.getElementById("autoSelectButton").click(); } - beforeEach(async function() { + beforeEach(async function () { await resetRRVDisplay(); }); - it("Basic percentage-based filtering works", async function() { + it("Basic percentage-based filtering works", async function () { await callAutoSelect("25", "autoPercent"); // 25% of 5 features is 1, so we should see 1 feature on // the top and bottom (and the current ranking is @@ -571,7 +653,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( "Taxon2", "Taxon3", "Taxon4", - "Taxon5" + "Taxon5", ]; chai.assert.sameMembers( allFeatures, @@ -586,7 +668,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( ) ); }); - it("Basic literal-number-based filtering works", async function() { + it("Basic literal-number-based filtering works", async function () { await callAutoSelect("3", "autoLiteral"); // 3 features on the bottom and 3 on the top. Since there // are 5 features, we should see an overlap in the middle @@ -604,12 +686,50 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( ) ); }); - it("Invalid inputs result in empty feature selections", async function() { + it("Negative numbers correctly flip autoselection", async function () { + // These examples were just copied & flipped from the + // above tests + await callAutoSelect("-25", "autoPercent"); + chai.assert.sameMembers( + ["Taxon4"], + testing_utilities.getFeatureIDsFromObjectArray( + rrv.botFeatures + ) + ); + chai.assert.sameMembers( + ["Taxon2"], + testing_utilities.getFeatureIDsFromObjectArray( + rrv.topFeatures + ) + ); + await callAutoSelect("-3", "autoLiteral"); + chai.assert.sameMembers( + ["Taxon4", "Taxon5", "Taxon1"], + testing_utilities.getFeatureIDsFromObjectArray( + rrv.botFeatures + ) + ); + chai.assert.sameMembers( + ["Taxon2", "Taxon3", "Taxon1"], + testing_utilities.getFeatureIDsFromObjectArray( + rrv.topFeatures + ) + ); + }); + it("Invalid inputs result in empty feature selections", async function () { function assertEmpty(rrv) { chai.assert.empty(rrv.topFeatures); chai.assert.empty(rrv.botFeatures); } - var vals = ["-3", "123eee", "-0.01"]; + var vals = [ + "Infinity", + "-Infinity", + "Ten", + "A hundred lol", + "123eee", + "NaN", + "-NaN", + ]; for (var v = 0; v < vals.length; v++) { await callAutoSelect(vals[v], "autoLiteral"); assertEmpty(rrv); @@ -619,12 +739,12 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( }); }); }); - describe("Modifying rank plot field/size/color", function() { - beforeEach(async function() { + describe("Modifying rank plot field/size/color", function () { + beforeEach(async function () { await resetRRVDisplay(); }); - describe("Changing the ranking used on the rank plot", function() { - it("Updates rank plot field, title, and window sort transform", async function() { + describe("Changing the ranking used on the rank plot", function () { + it("Updates rank plot field, title, and window sort transform", async function () { document.getElementById("rankField").value = "Rank 1"; await document.getElementById("rankField").onchange(); // Check that the rank plot JSON was updated accordingly: @@ -656,12 +776,12 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( ); }); }); - describe("Changing the bar width", function() { + describe("Changing the bar width", function () { async function triggerBarSizeUpdate(newValue) { document.getElementById("barSizeSlider").value = newValue; await document.getElementById("barSizeSlider").onchange(); } - it("Changing the bar width to a constant size updates JSON and DOM properly", async function() { + it("Changing the bar width to a constant size updates JSON and DOM properly", async function () { await triggerBarSizeUpdate("3"); chai.assert.equal( 3, @@ -673,8 +793,8 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( .classList.contains("invisible") ); }); - describe("Changing the bar width to fit to the rank plot width", function() { - it("Works properly in basic case", async function() { + describe("Changing the bar width to fit to the rank plot width", function () { + it("Works properly in basic case", async function () { // Set bar size to 3 using the slider await triggerBarSizeUpdate("3"); chai.assert.equal( @@ -729,7 +849,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( .classList.contains("invisible") ); }); - it("Un-hides a warning element when the bar size is less than 1 pixel", async function() { + it("Un-hides a warning element when the bar size is less than 1 pixel", async function () { function isInvisible() { // Silly helper function to reduce repetitive code return document @@ -748,8 +868,8 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( }); }); }); - describe("Changing the rank plot color scheme", function() { - it("Updating works properly", async function() { + describe("Changing the rank plot color scheme", function () { + it("Updating works properly", async function () { document.getElementById("rankPlotColorScheme").value = "#5fa2c8,#daa520,#029e73"; await document @@ -775,13 +895,13 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( }); }); }); - describe("Modifying sample plot fields/scales/colorschemes", function() { - beforeEach(async function() { + describe("Modifying sample plot fields/scales/colorschemes", function () { + beforeEach(async function () { await resetRRVDisplay(); }); - describe("Changing the x-axis field used on the sample plot", function() { + describe("Changing the x-axis field used on the sample plot", function () { var xFieldEle = document.getElementById("xAxisField"); - it("Works properly in the basic case (no boxplots involved)", async function() { + it("Works properly in the basic case (no boxplots involved)", async function () { xFieldEle.value = "Metadata2"; // Kind of a lazy solution, but we assume that .onchange() // works well enough: this is derived from @@ -799,7 +919,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( rrv.samplePlotJSON.encoding.color.field ); }); - it('Also updates color field when in "boxplot" mode', async function() { + it('Also updates color field when in "boxplot" mode', async function () { // There's obviously more stuff we could test here re: // details of the boxplot functionality, but that's for // another test. @@ -816,14 +936,14 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( ); }); }); - describe("Changing the x-axis scale type used on the sample plot", function() { + describe("Changing the x-axis scale type used on the sample plot", function () { // TODO test filters, tooltips, etc. var xScaleEle = document.getElementById("xAxisScale"); it( "Works properly for basic case of (non-boxplot) categorical -> quantitative", testXAxisCategoricalToQuantitative ); - it("Works properly for basic case of quantitative -> (non-boxplot) categorical", async function() { + it("Works properly for basic case of quantitative -> (non-boxplot) categorical", async function () { await testXAxisCategoricalToQuantitative(); xScaleEle.value = "nominal"; await xScaleEle.onchange(); @@ -839,7 +959,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( ); }); }); - describe("Changing the color used on the sample plot", function() { + describe("Changing the color used on the sample plot", function () { var colorFieldEle = document.getElementById("colorField"); async function testColorFieldChange(field) { colorFieldEle.value = field; @@ -855,7 +975,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( ) ); } - it("Works properly: changes color and updates filter accordingly", async function() { + it("Works properly: changes color and updates filter accordingly", async function () { await testColorFieldChange("Metadata3"); await testColorFieldChange("Metadata2"); // Check that changing the color field overwrites old @@ -868,7 +988,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( ); }); }); - describe("Changing the color scale type used on the sample plot", function() { + describe("Changing the color scale type used on the sample plot", function () { var colorScaleEle = document.getElementById("colorScale"); // Analogous to testXAxisCategoricalToQuantitative() above for the @@ -904,16 +1024,16 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( } } // TODO test filters/TOOLTIPS updated properly - it("Works properly for categorical -> quantitative", async function() { + it("Works properly for categorical -> quantitative", async function () { await testChangeScaleType("quantitative"); }); - it("Works properly for quantitative -> categorical", async function() { + it("Works properly for quantitative -> categorical", async function () { await testChangeScaleType("quantitative"); await testChangeScaleType("nominal"); }); }); - describe("Changing the sample plot color schemes", function() { - it("Changing the categorical color scheme works properly", async function() { + describe("Changing the sample plot color schemes", function () { + it("Changing the categorical color scheme works properly", async function () { document.getElementById("catColorScheme").value = "accent"; await document.getElementById("catColorScheme").onchange(); chai.assert.equal( @@ -921,7 +1041,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( rrv.samplePlotJSON.config.range.category.scheme ); }); - it("Changing the quantitative color scheme works properly", async function() { + it("Changing the quantitative color scheme works properly", async function () { document.getElementById("quantColorScheme").value = "viridis"; await document @@ -932,7 +1052,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( rrv.samplePlotJSON.config.range.ramp.scheme ); }); - it("Invalid scale ranges cause an error", async function() { + it("Invalid scale ranges cause an error", async function () { // SO! Testing for errors in async functions doesn't // really work as expected in Chai, at least as of writing. // See https://github.com/chaijs/chai/issues/415 for @@ -955,45 +1075,15 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( }); }); }); - describe("Boxplot functionality", function() { - beforeEach(async function() { + describe("Boxplot functionality", function () { + beforeEach(async function () { await resetRRVDisplay(); }); - function testBoxplotEncodings(xField) { - chai.assert.equal("boxplot", rrv.samplePlotJSON.mark.type); - chai.assert.exists(rrv.samplePlotJSON.mark.median); - chai.assert.equal( - "#000000", - rrv.samplePlotJSON.mark.median.color - ); - // Also, the color field encoding should match the x-axis - // field encoding and be nominal - chai.assert.equal(xField, rrv.samplePlotJSON.encoding.x.field); - chai.assert.equal( - xField, - rrv.samplePlotJSON.encoding.color.field - ); - chai.assert.equal( - "nominal", - rrv.samplePlotJSON.encoding.color.type - ); - } - async function testSwitchToBoxplot(currXField) { - await document.getElementById("boxplotCheckbox").click(); - var xScaleEle = document.getElementById("xAxisScale"); - xScaleEle.value = "nominal"; - await xScaleEle.onchange(); - // Now the sample plot should be a boxplot - testBoxplotEncodings(currXField); - // In "boxplot mode", the color controls should be disabled - testing_utilities.assertEnabled("colorField", false); - testing_utilities.assertEnabled("colorScale", false); - } - describe("Changing to a boxplot...", function() { - it("...By checking the boxplot checkbox", async function() { + describe("Changing to a boxplot...", function () { + it("...By checking the boxplot checkbox", async function () { await testSwitchToBoxplot("Metadata1"); }); - it("...By changing the x-axis scale type to categorical", async function() { + it("...By changing the x-axis scale type to categorical", async function () { // change the x-axis scale from categorical to // quantitative. Then check the boxplot checkbox (which // won't change anything yet, since the x-axis scale is @@ -1008,7 +1098,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( testBoxplotEncodings("Sample ID"); }); }); - describe("Changing from a boxplot...", function() { + describe("Changing from a boxplot...", function () { async function testSamplePlotStateAfterBoxplot( currXField, currXScaleType, @@ -1018,6 +1108,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( chai.assert.notExists(rrv.samplePlotJSON.mark.median); testing_utilities.assertEnabled("colorField", true); testing_utilities.assertEnabled("colorScale", true); + testing_utilities.assertEnabled("borderCheckbox", true); // Fields should stay the same, and scales should stay // categorical chai.assert.equal( @@ -1058,7 +1149,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( rrv.samplePlotJSON.encoding.color.type ); } - it("...By unchecking the boxplot checkbox", async function() { + it("...By unchecking the boxplot checkbox", async function () { await testSwitchToBoxplot("Metadata1"); await document.getElementById("boxplotCheckbox").click(); await testSamplePlotStateAfterBoxplot( @@ -1067,7 +1158,7 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( "Sample ID" ); }); - it("...By changing the x-axis scale type to quantitative", async function() { + it("...By changing the x-axis scale type to quantitative", async function () { await testSwitchToBoxplot("Metadata1"); document.getElementById("xAxisScale").value = "quantitative"; @@ -1080,5 +1171,59 @@ define(["mocha", "chai", "testing_utilities", "dom_utils"], function( }); }); }); + describe("Changing borders on points in the sample plot", function () { + beforeEach(async function () { + await resetRRVDisplay(); + }); + function checkBordersAdded() { + chai.assert.equal("#000000", rrv.samplePlotJSON.mark.stroke); + chai.assert.equal(0.5, rrv.samplePlotJSON.mark.strokeWidth); + chai.assert.exists(rrv.samplePlotJSON.encoding.color.legend); + chai.assert.equal( + "#000000", + rrv.samplePlotJSON.encoding.color.legend.symbolStrokeColor + ); + chai.assert.equal( + 0.5, + rrv.samplePlotJSON.encoding.color.legend.symbolStrokeWidth + ); + } + function checkBordersRemoved() { + chai.assert.notExists(rrv.samplePlotJSON.mark.stroke); + chai.assert.notExists(rrv.samplePlotJSON.mark.strokeWidth); + chai.assert.notExists(rrv.samplePlotJSON.encoding.color.legend); + } + async function testAddBorders() { + await document.getElementById("borderCheckbox").click(); + checkBordersAdded(); + } + describe("Adding borders...", function () { + it("...By checking the border checkbox", async function () { + await testAddBorders(); + }); + it("...By unchecking the boxplot checkbox", async function () { + await testAddBorders(); + await testSwitchToBoxplot("Metadata1"); + checkBordersRemoved(); + // Switch *out* of boxplot mode + await document.getElementById("boxplotCheckbox").click(); + // Now, verify that the sample border checkbox being + // checked was "preserved", and that the borders are back + checkBordersAdded(); + }); + }); + describe("Removing borders...", function () { + it("...By unchecking the border checkbox", async function () { + await testAddBorders(); + await document.getElementById("borderCheckbox").click(); + checkBordersRemoved(); + }); + it("...By checking the boxplot checkbox", async function () { + await testAddBorders(); + await testSwitchToBoxplot("Metadata1"); + checkBordersRemoved(); + }); + }); + }); }); }); diff --git a/qurro/tests/web_tests/tests/test_rrvdisplay_compute_balance.js b/qurro/tests/web_tests/tests/test_rrvdisplay_compute_balance.js index 3e22341b..dc0fcc47 100644 --- a/qurro/tests/web_tests/tests/test_rrvdisplay_compute_balance.js +++ b/qurro/tests/web_tests/tests/test_rrvdisplay_compute_balance.js @@ -1,4 +1,4 @@ -define(["mocha", "chai", "testing_utilities"], function( +define(["mocha", "chai", "testing_utilities"], function ( mocha, chai, testing_utilities @@ -11,9 +11,9 @@ define(["mocha", "chai", "testing_utilities"], function( // prettier-ignore var countJSON = {"Taxon1": {"Sample2": 1.0, "Sample3": 2.0, "Sample5": 4.0, "Sample6": 5.0, "Sample7": 6.0}, "Taxon2": {"Sample1": 6.0, "Sample2": 5.0, "Sample3": 4.0, "Sample5": 2.0, "Sample6": 1.0}, "Taxon3": {"Sample1": 2.0, "Sample2": 3.0, "Sample3": 4.0, "Sample5": 4.0, "Sample6": 3.0, "Sample7": 2.0}, "Taxon4": {"Sample1": 1.0, "Sample2": 1.0, "Sample3": 1.0, "Sample5": 1.0, "Sample6": 1.0, "Sample7": 1.0}, "Taxon5": {"Sample3": 1.0, "Sample5": 2.0}}; - describe("Computing sample log-ratios of selected features in an RRVDisplay object", function() { + describe("Computing sample log-ratios of selected features in an RRVDisplay object", function () { var rrv; - before(async function() { + before(async function () { rrv = testing_utilities.getNewRRVDisplay( rankPlotJSON, samplePlotJSON, @@ -21,11 +21,11 @@ define(["mocha", "chai", "testing_utilities"], function( ); await rrv.makePlots(); }); - after(async function() { + after(async function () { await rrv.destroy(true, true, true); }); - describe("Single-feature selections", function() { - it("Computes the correct sample log-ratio", function() { + describe("Single-feature selections", function () { + it("Computes the correct sample log-ratio", function () { rrv.newFeatureHigh = { "Feature ID": "Taxon3" }; rrv.newFeatureLow = { "Feature ID": "Taxon4" }; chai.assert.equal( @@ -53,7 +53,7 @@ define(["mocha", "chai", "testing_utilities"], function( rrv.updateBalanceSingle({ "Sample ID": "Sample5" }) ); }); - it("Returns null when numerator and/or denominator is 0", function() { + it("Returns null when numerator and/or denominator is 0", function () { // In this first case, only the numerator is a 0. rrv.newFeatureHigh = { "Feature ID": "Taxon1" }; rrv.newFeatureLow = { "Feature ID": "Taxon2" }; @@ -68,24 +68,24 @@ define(["mocha", "chai", "testing_utilities"], function( ); }); - it("Throws an error if sample ID isn't present in data", function() { - chai.assert.throws(function() { + it("Throws an error if sample ID isn't present in data", function () { + chai.assert.throws(function () { rrv.updateBalanceSingle({ - "Sample ID": "lolthisisntreal" + "Sample ID": "lolthisisntreal", }); }); }); }); - describe("Multi-feature selections", function() { - it("Computes the correct sample log-ratio", function() { + describe("Multi-feature selections", function () { + it("Computes the correct sample log-ratio", function () { // Standard 2-taxon / 2-taxon case rrv.topFeatures = [ { "Feature ID": "Taxon1" }, - { "Feature ID": "Taxon3" } + { "Feature ID": "Taxon3" }, ]; rrv.botFeatures = [ { "Feature ID": "Taxon2" }, - { "Feature ID": "Taxon4" } + { "Feature ID": "Taxon4" }, ]; chai.assert.equal( Math.log(2 / 7), @@ -100,7 +100,7 @@ define(["mocha", "chai", "testing_utilities"], function( rrv.updateBalanceMulti({ "Sample ID": "Sample1" }) ); }); - it("Returns null when numerator and/or denominator feature lists are empty", function() { + it("Returns null when numerator and/or denominator feature lists are empty", function () { // Test what happens when numerator and/or denominator // feature lists are empty. If either or both of these // feature lists are empty, we should get a null balance @@ -125,18 +125,18 @@ define(["mocha", "chai", "testing_utilities"], function( rrv.updateBalanceMulti({ "Sample ID": "Sample1" }) ); }); - it("Throws an error if sample ID isn't present in data", function() { + it("Throws an error if sample ID isn't present in data", function () { // Same as in the updateBalanceSingle test -- verify that // a nonexistent sample ID causes an error - chai.assert.throws(function() { + chai.assert.throws(function () { rrv.updateBalanceMulti({ - "Sample ID": "lolthisisntreal" + "Sample ID": "lolthisisntreal", }); }); }); }); - describe("Summing feature abundances in a sample", function() { - it("Correctly sums feature abundances in a sample", function() { + describe("Summing feature abundances in a sample", function () { + it("Correctly sums feature abundances in a sample", function () { // Check case when number of features is just one chai.assert.equal( 6, @@ -160,7 +160,7 @@ define(["mocha", "chai", "testing_utilities"], function( [ { "Feature ID": "Taxon2" }, { "Feature ID": "Taxon4" }, - { "Feature ID": "Taxon1" } + { "Feature ID": "Taxon1" }, ] ) ); @@ -183,7 +183,7 @@ define(["mocha", "chai", "testing_utilities"], function( ) ); }); - it("Returns 0 when the input list of features is empty", function() { + it("Returns 0 when the input list of features is empty", function () { chai.assert.equal( 0, rrv.sumAbundancesForSampleFeatures( @@ -192,9 +192,9 @@ define(["mocha", "chai", "testing_utilities"], function( ) ); }); - it("Throws an error if sample ID isn't present in data", function() { + it("Throws an error if sample ID isn't present in data", function () { // Check that an invalid sample ID causes an error - chai.assert.throws(function() { + chai.assert.throws(function () { rrv.sumAbundancesForSampleFeatures( { "Sample ID": "lolthisisntreal" }, [] diff --git a/qurro/tests/web_tests/tests/test_rrvdisplay_destroy.js b/qurro/tests/web_tests/tests/test_rrvdisplay_destroy.js index 10d85bb2..77397f3a 100644 --- a/qurro/tests/web_tests/tests/test_rrvdisplay_destroy.js +++ b/qurro/tests/web_tests/tests/test_rrvdisplay_destroy.js @@ -1,4 +1,5 @@ -define(["dom_utils", "mocha", "chai", "testing_utilities"], function( +define(["vega", "dom_utils", "mocha", "chai", "testing_utilities"], function ( + vega, dom_utils, mocha, chai, @@ -12,9 +13,9 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( // prettier-ignore var countJSON = {"Taxon1": {"Sample2": 1.0, "Sample3": 2.0, "Sample5": 4.0, "Sample6": 5.0, "Sample7": 6.0}, "Taxon2": {"Sample1": 6.0, "Sample2": 5.0, "Sample3": 4.0, "Sample5": 2.0, "Sample6": 1.0}, "Taxon3": {"Sample1": 2.0, "Sample2": 3.0, "Sample3": 4.0, "Sample5": 4.0, "Sample6": 3.0, "Sample7": 2.0}, "Taxon4": {"Sample1": 1.0, "Sample2": 1.0, "Sample3": 1.0, "Sample5": 1.0, "Sample6": 1.0, "Sample7": 1.0}, "Taxon5": {"Sample3": 1.0, "Sample5": 2.0}}; - describe("The RRVDisplay destructor (destroy())", function() { + describe("The RRVDisplay destructor (destroy())", function () { var rrv; - beforeEach(async function() { + beforeEach(async function () { rrv = testing_utilities.getNewRRVDisplay( rankPlotJSON, samplePlotJSON, @@ -25,7 +26,7 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( // TODO: add tests that leaving certain args as false still lets // destroy() work partially? - it("Properly clears DOM element bindings", function() { + it("Properly clears DOM element bindings", function () { rrv.destroy(true, true, true); for (var i = 0; i < rrv.elementsWithOnClickBindings.length; i++) { chai.assert.isNull( @@ -40,14 +41,14 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( ); } }); - it("Properly clears the #rankPlot and #samplePlot divs", function() { + it("Properly clears the #rankPlot and #samplePlot divs", function () { rrv.destroy(true, true, true); chai.assert.isEmpty(document.getElementById("rankPlot").innerHTML); chai.assert.isEmpty( document.getElementById("samplePlot").innerHTML ); }); - it("Properly clears the ranking/metadata field s", function () { rrv.destroy(true, true, true); chai.assert.isEmpty(document.getElementById("rankField").innerHTML); chai.assert.isEmpty(document.getElementById("topSearch").innerHTML); @@ -59,11 +60,16 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( document.getElementById("colorField").innerHTML ); }); + it("Removes the 'qiimediscrete' (Classic QIIME Colors) color scheme", function () { + rrv.destroy(true, true, true); + chai.assert.isUndefined(vega.scheme("qiimediscrete")); + }); /* Warning: here be ugly, bulky code that I wrote in a hurry */ - it("Properly resets other UI elements to their defaults", async function() { + it("Properly resets other UI elements to their defaults", async function () { // before calling rrv.destroy(), change a few other things // this way we can check that these modifications are reverted on // calling destroy() + await document.getElementById("borderCheckbox").click(); await document.getElementById("boxplotCheckbox").click(); document.getElementById("topSearchType").value = "rank"; document.getElementById("botSearchType").value = "rank"; @@ -97,9 +103,18 @@ define(["dom_utils", "mocha", "chai", "testing_utilities"], function( rrv.destroy(true, true, true); + chai.assert.isFalse( + document.getElementById("borderCheckbox").checked + ); chai.assert.isFalse( document.getElementById("boxplotCheckbox").checked ); + // Check that boxplot mode "disabled" elements were enabled + chai.assert.isFalse(document.getElementById("colorField").disabled); + chai.assert.isFalse(document.getElementById("colorScale").disabled); + chai.assert.isFalse( + document.getElementById("borderCheckbox").disabled + ); chai.assert.equal( "text", document.getElementById("topSearchType").value diff --git a/qurro/tests/web_tests/tests/test_rrvdisplay_getinvalidsampleids.js b/qurro/tests/web_tests/tests/test_rrvdisplay_getinvalidsampleids.js index 00587ab9..63846c75 100644 --- a/qurro/tests/web_tests/tests/test_rrvdisplay_getinvalidsampleids.js +++ b/qurro/tests/web_tests/tests/test_rrvdisplay_getinvalidsampleids.js @@ -1,4 +1,4 @@ -define(["mocha", "chai", "testing_utilities"], function( +define(["mocha", "chai", "testing_utilities"], function ( mocha, chai, testing_utilities @@ -11,9 +11,9 @@ define(["mocha", "chai", "testing_utilities"], function( // prettier-ignore var countJSON = {"Taxon1": {"Sample2": 1.0, "Sample3": 2.0, "Sample5": 4.0, "Sample6": 5.0, "Sample7": 6.0}, "Taxon2": {"Sample1": 6.0, "Sample2": 5.0, "Sample3": 4.0, "Sample5": 2.0, "Sample6": 1.0}, "Taxon3": {"Sample1": 2.0, "Sample2": 3.0, "Sample3": 4.0, "Sample5": 4.0, "Sample6": 3.0, "Sample7": 2.0}, "Taxon4": {"Sample1": 1.0, "Sample2": 1.0, "Sample3": 1.0, "Sample5": 1.0, "Sample6": 1.0, "Sample7": 1.0}, "Taxon5": {"Sample3": 1.0, "Sample5": 2.0}}; - describe("Using RRVDisplay.getInvalidSampleIDs() to distinguish samples with invalid metadata field values", function() { + describe("Using RRVDisplay.getInvalidSampleIDs() to distinguish samples with invalid metadata field values", function () { var rrv, dataName; - before(async function() { + before(async function () { rrv = testing_utilities.getNewRRVDisplay( rankPlotJSON, samplePlotJSON, @@ -22,7 +22,7 @@ define(["mocha", "chai", "testing_utilities"], function( dataName = rrv.samplePlotJSON.data.name; await rrv.makePlots(); }); - after(async function() { + after(async function () { await rrv.destroy(true, true, true); }); function testOnMetadata1AndX(expectedInvalidSampleIDs) { @@ -62,23 +62,23 @@ define(["mocha", "chai", "testing_utilities"], function( "Sample3", "Sample5", "Sample6", - "Sample7" + "Sample7", ]; - describe("Works properly when all samples have a valid field", function() { - it("...When there's a quantitative encoding", function() { + describe("Works properly when all samples have a valid field", function () { + it("...When there's a quantitative encoding", function () { rrv.samplePlotJSON.encoding.x.type = "quantitative"; // We expect the list of invalid sample IDs to be [] -- // that is, every sample should be "valid." testOnMetadata1AndX([]); }); - describe("...When there's a nominal encoding", function() { - before(function() { + describe("...When there's a nominal encoding", function () { + before(function () { rrv.samplePlotJSON.encoding.x.type = "nominal"; }); - it('Works properly with "normal" numerical values', function() { + it('Works properly with "normal" numerical values', function () { testOnMetadata1AndX([]); }); - it('Accepts "special" values in strings like "NaN", "null", etc.', function() { + it('Accepts "special" values in strings like "NaN", "null", etc.', function () { // These are just non-empty strings, so they should be // acceptable. rrv.samplePlotJSON.datasets[dataName][0].Metadata1 = "NaN"; @@ -92,14 +92,14 @@ define(["mocha", "chai", "testing_utilities"], function( rrv.samplePlotJSON.datasets[dataName][5].Metadata1 = '""'; testOnMetadata1AndX([]); }); - it('Works ok with just-whitespace strings (" ")', function() { + it('Works ok with just-whitespace strings (" ")', function () { fillMetadata1Vals(" "); testOnMetadata1AndX([]); }); }); }); - describe("Filtering out samples with null field values", function() { - before(function() { + describe("Filtering out samples with null field values", function () { + before(function () { resetMetadata1Values(); // Manually set some samples' (1, 2, and 3) metadata vals // to null or "null". ("null" should be treated as a normal @@ -118,14 +118,14 @@ define(["mocha", "chai", "testing_utilities"], function( // So the "invalid" samples, then, should be 1, 2, and 3. var expectedSampleIDs = ["Sample1", "Sample2", "Sample3"]; - it("...When there's a quantitative encoding", function() { + it("...When there's a quantitative encoding", function () { // Both the actual null values and normal strings (like the // "null" value that we set to Sample3) should be filtered // out for a quantitative encoding. rrv.samplePlotJSON.encoding.x.type = "quantitative"; testOnMetadata1AndX(["Sample1", "Sample2", "Sample3"]); }); - it("...When there's a nominal encoding", function() { + it("...When there's a nominal encoding", function () { // "null" should be kept since it's just a normal string, // as tested above. The actual null values should be // filtered out -- even in a nominal encoding -- though. @@ -133,14 +133,14 @@ define(["mocha", "chai", "testing_utilities"], function( testOnMetadata1AndX(["Sample1", "Sample2"]); }); }); - describe("Filtering out non-numeric values if encoding is quantitative", function() { - before(function() { + describe("Filtering out non-numeric values if encoding is quantitative", function () { + before(function () { resetMetadata1Values(); rrv.samplePlotJSON.encoding.x.type = "quantitative"; }); after(resetMetadata1Values); - it("Works properly when only some samples' field values are non-numeric", function() { + it("Works properly when only some samples' field values are non-numeric", function () { rrv.samplePlotJSON.datasets[dataName][0].Metadata1 = "Missing: not provided"; rrv.samplePlotJSON.datasets[dataName][1].Metadata1 = "3.2a"; @@ -148,23 +148,23 @@ define(["mocha", "chai", "testing_utilities"], function( "2019-07-14"; testOnMetadata1AndX(["Sample1", "Sample2", "Sample3"]); }); - it("Works properly when all samples' field values are non-numeric", function() { + it("Works properly when all samples' field values are non-numeric", function () { fillMetadata1Vals("Missing: not provided"); testOnMetadata1AndX(allSampleIDs); }); - it("Properly filters out string Infinity values", function() { + it("Properly filters out string Infinity values", function () { fillMetadata1Vals("Infinity"); testOnMetadata1AndX(allSampleIDs); }); - it("Properly filters out string -Infinity values", function() { + it("Properly filters out string -Infinity values", function () { fillMetadata1Vals("-Infinity"); testOnMetadata1AndX(allSampleIDs); }); - it("Properly filters out string NaN values", function() { + it("Properly filters out string NaN values", function () { fillMetadata1Vals("NaN"); testOnMetadata1AndX(allSampleIDs); }); - it("Properly filters out string undefined values", function() { + it("Properly filters out string undefined values", function () { fillMetadata1Vals("undefined"); testOnMetadata1AndX(allSampleIDs); }); diff --git a/qurro/tests/web_tests/tests/test_rrvdisplay_getinvalidsampleids_samplestatstest.js b/qurro/tests/web_tests/tests/test_rrvdisplay_getinvalidsampleids_samplestatstest.js index ccf7fcbb..8c89fe02 100644 --- a/qurro/tests/web_tests/tests/test_rrvdisplay_getinvalidsampleids_samplestatstest.js +++ b/qurro/tests/web_tests/tests/test_rrvdisplay_getinvalidsampleids_samplestatstest.js @@ -1,4 +1,4 @@ -define(["mocha", "chai", "testing_utilities"], function( +define(["mocha", "chai", "testing_utilities"], function ( mocha, chai, testing_utilities @@ -11,7 +11,7 @@ define(["mocha", "chai", "testing_utilities"], function( // prettier-ignore var SSTcountJSON = {"Taxon1": {"Sample2": 1.0, "Sample3": 2.0, "Sample5": 4.0, "Sample6": 5.0, "Sample7": 6.0}, "Taxon2": {"Sample1": 6.0, "Sample2": 5.0, "Sample3": 4.0, "Sample5": 2.0, "Sample6": 1.0}, "Taxon3": {"Sample1": 2.0, "Sample2": 3.0, "Sample3": 4.0, "Sample5": 4.0, "Sample6": 3.0, "Sample7": 2.0}, "Taxon4": {"Sample1": 1.0, "Sample2": 1.0, "Sample3": 1.0, "Sample5": 1.0, "Sample6": 1.0, "Sample7": 1.0}, "Taxon5": {"Sample3": 1.0, "Sample5": 2.0}}; - describe('Sample metadata values in JSON, "samples shown" statistics, and sample plot filtering', function() { + describe('Sample metadata values in JSON, "samples shown" statistics, and sample plot filtering', function () { var rrv, dataName; async function resetRRVDisplay() { rrv = testing_utilities.getNewRRVDisplay( @@ -23,10 +23,10 @@ define(["mocha", "chai", "testing_utilities"], function( await rrv.makePlots(); } beforeEach(resetRRVDisplay); - afterEach(async function() { + afterEach(async function () { await rrv.destroy(true, true, true); }); - it("Sample metadata values are passed from python to JSON to JS correctly", function() { + it("Sample metadata values are passed from python to JSON to JS correctly", function () { var sampleArray = rrv.samplePlotJSON.datasets[dataName]; chai.assert.equal(6, sampleArray.length); @@ -63,7 +63,7 @@ define(["mocha", "chai", "testing_utilities"], function( chai.assert.equal("20", sampleArray[5].Metadata2); chai.assert.equal("21", sampleArray[5].Metadata3); }); - describe("Sample plot filters are properly set", function() { + describe("Sample plot filters are properly set", function () { /* Small helper function. encoding should be "xAxis" or "color", * and newValue should be whatever the new value you want to set * is. @@ -85,7 +85,7 @@ define(["mocha", "chai", "testing_utilities"], function( // NOTE: if we get around to removing the redundancy in the // filters generated when x-axis and color fields are equal, // these tests will need to be changed accordingly. - it("When both x-axis and color are categorical", async function() { + it("When both x-axis and color are categorical", async function () { // Just once, test the "starting" filters chai.assert.equal( "datum.qurro_balance != null && " + @@ -135,7 +135,7 @@ define(["mocha", "chai", "testing_utilities"], function( rrv.getInvalidSampleIDs("Metadata3", "color") ); }); - it("When x-axis is quantitative and color is categorical", async function() { + it("When x-axis is quantitative and color is categorical", async function () { await changeEncoding("xAxis", "quantitative", true); chai.assert.equal( "datum.qurro_balance != null && " + @@ -185,7 +185,7 @@ define(["mocha", "chai", "testing_utilities"], function( "Sample3", "Sample5", "Sample6", - "Sample7" + "Sample7", ], rrv.getInvalidSampleIDs("Sample ID", "x") ); @@ -193,7 +193,7 @@ define(["mocha", "chai", "testing_utilities"], function( rrv.getInvalidSampleIDs("Metadata2", "color") ); }); - it("When x-axis is categorical and color is quantitative", async function() { + it("When x-axis is categorical and color is quantitative", async function () { await changeEncoding("color", "quantitative", true); chai.assert.equal( "datum.qurro_balance != null && " + @@ -240,12 +240,12 @@ define(["mocha", "chai", "testing_utilities"], function( "Sample3", "Sample5", "Sample6", - "Sample7" + "Sample7", ], rrv.getInvalidSampleIDs("Sample ID", "color") ); }); - it("When both x-axis and color are quantitative", async function() { + it("When both x-axis and color are quantitative", async function () { await changeEncoding("color", "quantitative", true); chai.assert.equal( "datum.qurro_balance != null && " + diff --git a/qurro/tests/web_tests/tests/test_rrvdisplay_update_datatables.js b/qurro/tests/web_tests/tests/test_rrvdisplay_update_datatables.js index 949d1142..4aceafd3 100644 --- a/qurro/tests/web_tests/tests/test_rrvdisplay_update_datatables.js +++ b/qurro/tests/web_tests/tests/test_rrvdisplay_update_datatables.js @@ -1,4 +1,4 @@ -define(["mocha", "chai", "testing_utilities"], function( +define(["mocha", "chai", "testing_utilities"], function ( mocha, chai, testing_utilities @@ -11,9 +11,9 @@ define(["mocha", "chai", "testing_utilities"], function( // prettier-ignore var countJSON = {"Taxon1": {"Sample2": 1.0, "Sample3": 2.0, "Sample5": 4.0, "Sample6": 5.0, "Sample7": 6.0}, "Taxon2": {"Sample1": 6.0, "Sample2": 5.0, "Sample3": 4.0, "Sample5": 2.0, "Sample6": 1.0}, "Taxon3": {"Sample1": 2.0, "Sample2": 3.0, "Sample3": 4.0, "Sample5": 4.0, "Sample6": 3.0, "Sample7": 2.0}, "Taxon4": {"Sample1": 1.0, "Sample2": 1.0, "Sample3": 1.0, "Sample5": 1.0, "Sample6": 1.0, "Sample7": 1.0}, "Taxon5": {"Sample3": 1.0, "Sample5": 2.0}}; - describe("Updating the feature DataTables in RRVDisplay.updateFeaturesDisplays()", function() { + describe("Updating the feature DataTables in RRVDisplay.updateFeaturesDisplays()", function () { var rrv; - before(async function() { + before(async function () { rrv = testing_utilities.getNewRRVDisplay( rankPlotJSON, samplePlotJSON, @@ -21,7 +21,7 @@ define(["mocha", "chai", "testing_utilities"], function( ); await rrv.makePlots(); }); - after(async function() { + after(async function () { await rrv.destroy(true, true, true); }); @@ -57,17 +57,17 @@ define(["mocha", "chai", "testing_utilities"], function( await document.getElementById("multiFeatureButton").onclick(); } - it("Works for single-feature selections", function() { + it("Works for single-feature selections", function () { rrv.newFeatureHigh = testing_utilities.getFeatureRow(rrv, "Taxon3"); rrv.newFeatureLow = testing_utilities.getFeatureRow(rrv, "Taxon4"); rrv.updateFeaturesDisplays(true); // Check that tables are updated properly testing_utilities.checkDataTable("topFeaturesDisplay", { - Taxon3: [4, 5, 6, 0, 4, "Yeet", "100"] + Taxon3: [4, 5, 6, 0, 4, "Yeet", "100"], }); testing_utilities.checkDataTable("botFeaturesDisplay", { - Taxon4: [9, 8, 7, 0, 4, null, null] + Taxon4: [9, 8, 7, 0, 4, null, null], }); // Check that headers are updated accordingly testing_utilities.checkHeaders(1, 1, 5); @@ -80,14 +80,14 @@ define(["mocha", "chai", "testing_utilities"], function( // ...and check results again testing_utilities.checkDataTable("topFeaturesDisplay", { - Taxon1: [5, 6, 7, 0, 4, null, null] + Taxon1: [5, 6, 7, 0, 4, null, null], }); testing_utilities.checkDataTable("botFeaturesDisplay", { - Taxon2: [1, 2, 3, 0, 4, null, null] + Taxon2: [1, 2, 3, 0, 4, null, null], }); testing_utilities.checkHeaders(1, 1, 5); }); - it("Works for multi-feature selections", async function() { + it("Works for multi-feature selections", async function () { await runFeatureFiltering( "Feature ID", "Taxon", @@ -101,14 +101,14 @@ define(["mocha", "chai", "testing_utilities"], function( Taxon2: [1, 2, 3, 0, 4, null, null], Taxon3: [4, 5, 6, 0, 4, "Yeet", "100"], Taxon4: [9, 8, 7, 0, 4, null, null], - Taxon5: [6, 5, 4, 0, 4, "null", "lol"] + Taxon5: [6, 5, 4, 0, 4, "null", "lol"], }); testing_utilities.checkDataTable("botFeaturesDisplay", { - Taxon3: [4, 5, 6, 0, 4, "Yeet", "100"] + Taxon3: [4, 5, 6, 0, 4, "Yeet", "100"], }); testing_utilities.checkHeaders(5, 1, 5); }); - it('Clears the "feature text" DOM elements properly', function() { + it('Clears the "feature text" DOM elements properly', function () { // PART 1 // Populate the DOM elements rrv.newFeatureHigh = testing_utilities.getFeatureRow(rrv, "Taxon1"); @@ -137,7 +137,7 @@ define(["mocha", "chai", "testing_utilities"], function( testing_utilities.checkDataTable("botFeaturesDisplay", {}); testing_utilities.checkHeaders(0, 0, 5); }); - it("Works when both selected feature list(s) are empty", async function() { + it("Works when both selected feature list(s) are empty", async function () { await runFeatureFiltering( "Feature ID", "aoisdjfoisdjfoasidj", @@ -150,7 +150,7 @@ define(["mocha", "chai", "testing_utilities"], function( testing_utilities.checkDataTable("botFeaturesDisplay", {}); testing_utilities.checkHeaders(0, 0, 5); }); - it("Works when just one selected feature list is empty", async function() { + it("Works when just one selected feature list is empty", async function () { await runFeatureFiltering( "Feature ID", "Taxon3", @@ -160,7 +160,7 @@ define(["mocha", "chai", "testing_utilities"], function( "text" ); testing_utilities.checkDataTable("topFeaturesDisplay", { - Taxon3: [4, 5, 6, 0, 4, "Yeet", "100"] + Taxon3: [4, 5, 6, 0, 4, "Yeet", "100"], }); testing_utilities.checkDataTable("botFeaturesDisplay", {}); testing_utilities.checkHeaders(1, 0, 5); diff --git a/qurro/tests/web_tests/tests/test_rrvdisplay_update_feature_color.js b/qurro/tests/web_tests/tests/test_rrvdisplay_update_feature_color.js index a825f4f6..4bf40eec 100644 --- a/qurro/tests/web_tests/tests/test_rrvdisplay_update_feature_color.js +++ b/qurro/tests/web_tests/tests/test_rrvdisplay_update_feature_color.js @@ -1,4 +1,4 @@ -define(["mocha", "chai", "testing_utilities"], function( +define(["mocha", "chai", "testing_utilities"], function ( mocha, chai, testing_utilities @@ -11,9 +11,9 @@ define(["mocha", "chai", "testing_utilities"], function( // prettier-ignore var countJSON = {"Taxon1": {"Sample2": 1.0, "Sample3": 2.0, "Sample5": 4.0, "Sample6": 5.0, "Sample7": 6.0}, "Taxon2": {"Sample1": 6.0, "Sample2": 5.0, "Sample3": 4.0, "Sample5": 2.0, "Sample6": 1.0}, "Taxon3": {"Sample1": 2.0, "Sample2": 3.0, "Sample3": 4.0, "Sample5": 4.0, "Sample6": 3.0, "Sample7": 2.0}, "Taxon4": {"Sample1": 1.0, "Sample2": 1.0, "Sample3": 1.0, "Sample5": 1.0, "Sample6": 1.0, "Sample7": 1.0}, "Taxon5": {"Sample3": 1.0, "Sample5": 2.0}}; - describe("Updating features' colors in the rank plot based on the current selection", function() { + describe("Updating features' colors in the rank plot based on the current selection", function () { var rrv; - before(async function() { + before(async function () { rrv = testing_utilities.getNewRRVDisplay( rankPlotJSON, samplePlotJSON, @@ -21,10 +21,10 @@ define(["mocha", "chai", "testing_utilities"], function( ); await rrv.makePlots(); }); - after(async function() { + after(async function () { await rrv.destroy(true, true, true); }); - it("Works for single-feature selections", function() { + it("Works for single-feature selections", function () { rrv.newFeatureHigh = { "Feature ID": "FH" }; rrv.newFeatureLow = { "Feature ID": "FL" }; chai.assert.equal( @@ -47,15 +47,15 @@ define(["mocha", "chai", "testing_utilities"], function( ); }); - it("Works for multi-feature selections", function() { + it("Works for multi-feature selections", function () { rrv.topFeatures = [ { "Feature ID": "Feature1" }, { "Feature ID": "Feature2" }, - { "Feature ID": "Feature3" } + { "Feature ID": "Feature3" }, ]; rrv.botFeatures = [ { "Feature ID": "Feature3" }, - { "Feature ID": "Feature4" } + { "Feature ID": "Feature4" }, ]; chai.assert.equal( "Numerator", diff --git a/screenshots/redsea_data.png b/screenshots/redsea_data.png index 61322f65..93ab9bbc 100644 Binary files a/screenshots/redsea_data.png and b/screenshots/redsea_data.png differ diff --git a/setup.py b/setup.py index 02bb81d0..9f44ceb2 100644 --- a/setup.py +++ b/setup.py @@ -98,6 +98,5 @@ "console_scripts": ["qurro=qurro.scripts._plot:plot"], }, zip_safe=False, - # Fixes an Altair issue: see https://github.com/biocore/qurro/issues/74 - python_requires=">=3.5.3", + python_requires=">=3.6,<3.8", )