Skip to content

Commit

Permalink
23.7 fb variantid searchbox (#236)
Browse files Browse the repository at this point in the history
* FilterForm react-select

* React-select async callback + styling

* Restrict the AsyncSelect to allowableValues > 10

* Fix variantDetails field serialization in resolveVcfFields

* variableSamples searchbox UX coverage

* Fix isEmpty and add test coverage

* Fix No Filter displaying for is empty
  • Loading branch information
hextraza authored Oct 7, 2023
1 parent 2a7d9d4 commit e0e237c
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 32 deletions.
1 change: 1 addition & 0 deletions jbrowse/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 14 additions & 16 deletions jbrowse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,55 +13,53 @@
"test": "cross-env NODE_ENV=test jest",
"prepareCli": "rimraf ./buildCli && rimraf ./resources/external/jb-cli && npm install @jbrowse/[email protected] --prefix ./buildCli",
"jb-pkg": "npm run prepareCli && npx pkg --outdir=./resources/external/jb-cli ./buildCli/node_modules/@jbrowse/cli && rimraf ./buildCli"
},
"peerDependencies": {

},
"dependencies": {
"@gmod/vcf": "^5.0.10",
"@jbrowse/core": "^2.6.2",
"@jbrowse/plugin-variants": "^2.6.2",
"@jbrowse/plugin-svg": "^2.6.2",
"@jbrowse/plugin-linear-genome-view": "^2.6.2",
"@jbrowse/plugin-svg": "^2.6.2",
"@jbrowse/plugin-variants": "^2.6.2",
"@jbrowse/react-linear-genome-view": "^2.6.2",
"@labkey/api": "^1.22.0",
"@gmod/vcf": "^5.0.10",
"@labkey/components": "^2.355.0",
"@mui/x-data-grid": "^6.0.1",
"assert": "^2.0.0",
"browserify-zlib": "^0.2.0",
"buffer": "^6.0.3",
"child_process": "^1.0.2",
"fs": "^0.0.1-security",
"jquery": "^3.7.0",
"jspdf": "^2.5.1",
"jspdf-autotable": "^3.5.31",
"@mui/x-data-grid": "^6.0.1",
"node-polyfill-webpack-plugin": "2.0.1",
"@labkey/components": "^2.355.0",
"uuid": "^9.0.0",
"path-browserify": "^1.0.1",
"pkg": "^5.8.1",
"react-data-grid": "7.0.0-beta.10",
"react-google-charts": "^4.0.1",
"react-hot-loader": "^4.13.1",
"react-select": "^5.7.4",
"regenerator-runtime": "^0.13.11",
"stream-browserify": "^3.0.0",
"typescript": "^5.1.6",
"util": "^0.12.5",
"uuid": "^9.0.0",
"vm-browserify": "^1.1.2",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz",
"fs": "^0.0.1-security",
"child_process": "^1.0.2",
"react-hot-loader": "^4.13.1"
"xlsx": "https://cdn.sheetjs.com/xlsx-0.19.3/xlsx-0.19.3.tgz"
},
"devDependencies": {
"@labkey/build": "6.10.0",
"@types/jest": "^29.0.0",
"@types/jquery": "^3.0.0",
"@types/jexl": "^2.3.1",
"@types/jquery": "^3.0.0",
"@types/node": "^18.17.1",
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
"enzyme": "^3.11.0",
"jest": "^29.0.0",
"jest-cli": "^29.0.0",
"jest-mock": "^29.0.0",
"ts-jest": "^29.0.0",
"rimraf": "^3.0.2",
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0"
"ts-jest": "^29.0.0"
},
"jest": {
"globals": {
Expand Down
40 changes: 36 additions & 4 deletions jbrowse/src/client/JBrowse/VariantSearch/components/FilterForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState } from 'react';
import TextField from '@mui/material/TextField';
import Button from '@mui/material/Button';
import Select from '@mui/material/Select';
import AsyncSelect from 'react-select/async';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
Expand Down Expand Up @@ -131,6 +132,10 @@ const FilterForm = (props: FilterFormProps ) => {
filters.forEach((filter, index) => {
highlightedInputs[index] = { field: false, operator: false, value: false };

filter.field = filter.field ?? '';
filter.operator = filter.operator ?? '';
filter.value = filter.value ?? '';

if (filter.field === '') {
highlightedInputs[index].field = true;
}
Expand All @@ -139,7 +144,9 @@ const FilterForm = (props: FilterFormProps ) => {
highlightedInputs[index].operator = true;
}

if (filter.value === '') {
if (filter.operator === 'is empty' || filter.operator === 'is not empty') {
filter.value = '';
} else if (filter.value === '') {
highlightedInputs[index].value = true;
}
});
Expand Down Expand Up @@ -208,8 +215,8 @@ const FilterForm = (props: FilterFormProps ) => {
{filters.map((filter, index) => (
<FilterRow key={index} >
<FormControlMinWidth sx={ highlightedInputs[index]?.field ? highlightedSx : null }>
<InputLabel id="field-label">Field</InputLabel>
<Select
<InputLabel id="field-label">Field</InputLabel>
<Select
labelId="field-label"
value={filter.field}
onChange={(event) =>
Expand Down Expand Up @@ -266,9 +273,33 @@ const FilterForm = (props: FilterFormProps ) => {
{allowedGroupNames?.map((gn) => (
<MenuItem value={gn} key={gn}>{gn}</MenuItem>
))}

</Select>
</FormControlMinWidth>
) : fieldTypeInfo.find(obj => obj.name === filter.field)?.allowableValues?.length > 10 ? (
<FormControlMinWidth sx={ highlightedInputs[index]?.value ? highlightedSx : null } >
<AsyncSelect
id={`value-select-${index}`}
inputId={`value-select-${index}`}
aria-labelledby={`value-select-${index}`}
menuPortalTarget={document.body}
menuPosition={'fixed'}
isDisabled={filter.operator === "is empty" || filter.operator === "is not empty"}
menuShouldBlockScroll={true}
styles={{menuPortal: base => ({...base, zIndex: 9999})}}
isMulti={fieldTypeInfo.find(obj => obj.name === filter.field)?.isMultiValued}
loadOptions={(inputValue, callback) => {
const fieldInfo = fieldTypeInfo.find(obj => obj.name === filter.field);

callback(
(fieldInfo?.allowableValues || [])
.filter(value => value.toLowerCase().includes(inputValue.toLowerCase()))
.map(value => ({label: value, value}))
);
}}
onChange={(selected) => handleFilterChange(index, "value", selected?.length > 0 ? selected.map(s => s.value).join(',') : undefined)}
value={filter.value ? filter.value.split(',').map(value => ({label: value, value})) : undefined}
/>
</FormControlMinWidth>
) : fieldTypeInfo.find(obj => obj.name === filter.field)?.allowableValues?.length > 0 ? (
<FormControlMinWidth sx={ highlightedInputs[index]?.value ? highlightedSx : null } >
<InputLabel id="value-select-label">Value</InputLabel>
Expand Down Expand Up @@ -316,6 +347,7 @@ const FilterForm = (props: FilterFormProps ) => {
<Button
onClick={handleSubmit}
type="submit"
className="filter-form-select-button"
variant="contained"
color="primary"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,17 @@ const VariantTableWidget = observer(props => {
<div style={{ marginBottom: "10px", display: "flex", alignItems: "center" }}>
<div style={{ flex: 1 }}>
{filters.map((filter, index) => {
if ((filter as any).field && ((filter as any).operator === "is empty" || (filter as any).operator === "is not empty") && !(filter as any).value) {
return (
<Button
key={index}
onClick={() => setFilterModalOpen(true)}
style={{ border: "1px solid gray", margin: "5px" }}
>
{`${(filter as any).field} ${(filter as any).operator}`}
</Button>
);
}
if ((filter as any).field == "" || (filter as any).operator == "" || (filter as any).value == "" ) {
return (<Button
key={index}
Expand All @@ -476,8 +487,7 @@ const VariantTableWidget = observer(props => {
<Button
key={index}
onClick={() => setFilterModalOpen(true)}
style={{ border: "1px solid gray", margin: "5px" }}
>
style={{ border: "1px solid gray", margin: "5px" }} >
{`${(filter as any).field} ${(filter as any).operator} ${(filter as any).value}`}
</Button>
);
Expand Down
2 changes: 1 addition & 1 deletion jbrowse/src/client/JBrowse/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ export function searchStringToInitialFilters(knownFieldNames: string[]) : Filter
export function getOperatorsForField(fieldObj: FieldModel): string[] {
const stringOperators = ["equals", "does not equal", "contains", "does not contain", "starts with", "ends with", "is empty", "is not empty"];
const variableSamplesType = ["in set", "variable in", "not variable in", "variable in all of", "variable in any of", "not variable in any of", "not variable in one of", "is empty", "is not empty"];
const numericOperators = ["=", "!=", ">", ">=", "<", "<=", "is empty", "is not empty"];
const numericOperators = ["=", "!=", ">", ">=", "<", "<="];
const noneOperators = [];

// This can occur for the blank placeholder field:
Expand Down
1 change: 1 addition & 0 deletions jbrowse/src/org/labkey/jbrowse/JBrowseController.java
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,7 @@ public ApiResponse execute(ResolveVcfFieldsForm form, BindException errors)
if (form.isIncludeDefaultFields())
{
JBrowseFieldUtils.DEFAULT_FIELDS.forEach((key, val) -> ret.put(key, val.toJSON()));
ret.put(JBrowseFieldUtils.VARIABLE_SAMPLES, JBrowseFieldUtils.getVariableSamplesField(null).toJSON());
}

for (String key : form.getInfoKeys())
Expand Down
24 changes: 22 additions & 2 deletions jbrowse/src/org/labkey/jbrowse/JBrowseFieldUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import htsjdk.variant.vcf.VCFHeader;
import htsjdk.variant.vcf.VCFHeaderLineType;
import htsjdk.variant.vcf.VCFInfoHeaderLine;
import org.apache.logging.log4j.LogManager;
import org.jetbrains.annotations.Nullable;
import org.apache.logging.log4j.Logger;
import org.labkey.api.data.Container;
import org.labkey.api.jbrowse.JBrowseFieldDescriptor;
Expand All @@ -14,6 +14,7 @@
import org.labkey.jbrowse.model.JsonFile;

import java.io.File;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;

Expand All @@ -31,12 +32,12 @@ public class JBrowseFieldUtils
put("ref", new JBrowseFieldDescriptor("ref", "The reference allele", true, true, VCFHeaderLineType.String, 4).label("Ref Allele"));
put("alt", new JBrowseFieldDescriptor("alt", "The alternate allele", true, true, VCFHeaderLineType.String, 5).label("Alt Allele"));
put("genomicPosition", new JBrowseFieldDescriptor("genomicPosition", "", false, true, VCFHeaderLineType.Integer, 6).hidden(true).label("Genomic Position"));
put("variableSamples", new JBrowseFieldDescriptor(VARIABLE_SAMPLES, "All samples with this variant", true, true, VCFHeaderLineType.Character, 7).multiValued(true).label("Samples With Variant"));
}};

public static Map<String, JBrowseFieldDescriptor> getIndexedFields(JsonFile jsonFile, User u, Container c)
{
Map<String, JBrowseFieldDescriptor> ret = new LinkedHashMap<>(DEFAULT_FIELDS);
ret.put(VARIABLE_SAMPLES, getVariableSamplesField(jsonFile));

File vcf = jsonFile.getTrackFile();
if (!vcf.exists())
Expand Down Expand Up @@ -66,6 +67,25 @@ public static Map<String, JBrowseFieldDescriptor> getIndexedFields(JsonFile json
return ret;
}

public static JBrowseFieldDescriptor getVariableSamplesField(@Nullable JsonFile jsonFile) {
JBrowseFieldDescriptor field = new JBrowseFieldDescriptor(VARIABLE_SAMPLES, "All samples with this variant", true, true, VCFHeaderLineType.Character, 7).multiValued(true).label("Samples With Variant");
if (jsonFile != null) {
File vcf = jsonFile.getTrackFile();
if (vcf == null || !vcf.exists()) {
String msg = "Unable to find VCF file for track: " + jsonFile.getObjectId();
_log.error(msg + ", expected: " + (vcf == null ? "null" : vcf.getPath()));
return null;
}

try (VCFFileReader reader = new VCFFileReader(vcf)) {
field.allowableValues(reader.getHeader().getSampleNamesInOrder());
}
}

return field;
}


public static JBrowseSession getSession(String sessionId)
{
JBrowseSession session = JBrowseSession.getForId(sessionId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1505,6 +1505,13 @@ private void testVariantTableComparators() throws Exception {
waitAndClick(Locator.tagWithAttributeContaining("button", "aria-label", "Show filters"));
}

private void clearFilterDialog(String filter_text) {
waitForElement(Locator.tagWithText("button", filter_text)).click();
waitForElement(Locator.tagWithText("button", "Remove Filter")).click();
waitAndClick(Locator.tagWithText("button", "Search").index(1));
waitForElement(Locator.tagWithText("span", "2"));
}

private void testLuceneSearchUI(String sessionId)
{
beginAt("/" + getProjectName() + "/jbrowse-jbrowse.view?session=" + sessionId);
Expand All @@ -1530,16 +1537,63 @@ private void testLuceneSearchUI(String sessionId)

waitForElement(Locator.tagWithClass("input", "MuiInputBase-inputSizeSmall")).sendKeys("C");

// TODO: index isnt very stable. we need to differentiate the modal button from the grid button bar:
waitAndClick(Locator.tagWithText("button", "Search").index(1));
waitAndClick(Locator.tagWithClass("button", "filter-form-select-button"));

// indicates row is filtered:
waitForElementToDisappear(Locator.tagWithText("span", "2"));

// should re-open the dialog
waitForElement(Locator.tagWithText("button", "ref equals C")).click();
waitForElement(Locator.tagWithText("button", "Remove Filter")).click();
waitAndClick(Locator.tagWithText("button", "Search").index(1));
waitForElement(Locator.tagWithText("span", "2"));
clearFilterDialog("ref equals C");

// VariableSamples variable in m000001
waitAndClick(Locator.tagWithText("button", "Search"));
waitForElement(Locator.tagWithAttribute("div", "aria-labelledby", "field-label")).click();
waitForElement(Locator.tagWithText("li", "Samples With Variant")).click();
waitForElement(Locator.tagWithAttribute("div", "aria-labelledby", "operator-label")).click();
waitForElement(Locator.tagWithText("li", "variable in")).click();
waitForElement(Locator.tagWithId("input", "value-select-0")).sendKeys("m00001");
waitForElement(Locator.tagWithId("input", "value-select-0")).sendKeys(Keys.ENTER);
waitAndClick(Locator.tagWithClass("button", "filter-form-select-button"));
waitForElement(Locator.tagWithText("span", "0.553"));

clearFilterDialog("variableSamples variable in m00001");

// VariableSamples li usage + variable in all of
waitAndClick(Locator.tagWithText("button", "Search"));
waitForElement(Locator.tagWithAttribute("div", "aria-labelledby", "field-label")).click();
waitForElement(Locator.tagWithText("li", "Samples With Variant")).click();
waitForElement(Locator.tagWithAttribute("div", "aria-labelledby", "operator-label")).click();
waitForElement(Locator.tagWithText("li", "variable in all of")).click();
waitForElement(Locator.tagWithId("input", "value-select-0")).sendKeys("m000");
waitForElement(Locator.tagWithText("div", "m00005")).click();
waitForElement(Locator.tagWithId("input", "value-select-0")).sendKeys("m000");
waitForElement(Locator.tagWithText("div", "m00004")).click();
waitAndClick(Locator.tagWithClass("button", "filter-form-select-button"));
waitForElement(Locator.tagWithText("span", "0.3"));

clearFilterDialog("variableSamples variable in all of m00005,m00004");

// VariableSamples not variable in m05710
waitAndClick(Locator.tagWithText("button", "Search"));
waitForElement(Locator.tagWithAttribute("div", "aria-labelledby", "field-label")).click();
waitForElement(Locator.tagWithText("li", "Samples With Variant")).click();
waitForElement(Locator.tagWithAttribute("div", "aria-labelledby", "operator-label")).click();
waitForElement(Locator.tagWithText("li", "not variable in")).click();
waitForElement(Locator.tagWithId("input", "value-select-0")).sendKeys("m05710");
waitForElement(Locator.tagWithId("input", "value-select-0")).sendKeys(Keys.ENTER);
waitAndClick(Locator.tagWithClass("button", "filter-form-select-button"));
waitForElementToDisappear(Locator.tagWithText("span", "3.277E-4"));

clearFilterDialog("variableSamples not variable in m05710");

// samples with variant isEmpty
waitAndClick(Locator.tagWithText("button", "Search"));
waitForElement(Locator.tagWithAttribute("div", "aria-labelledby", "field-label")).click();
waitForElement(Locator.tagWithText("li", "Samples With Variant")).click();
waitForElement(Locator.tagWithAttribute("div", "aria-labelledby", "operator-label")).click();
waitForElement(Locator.tagWithText("li", "is empty")).click();
waitAndClick(Locator.tagWithClass("button", "filter-form-select-button"));
waitForElementToDisappear(Locator.tagWithText("span", "2"));

clearFilterDialog("variableSamples is empty");
}
}

0 comments on commit e0e237c

Please sign in to comment.