Skip to content

Commit a208193

Browse files
hextrazaSebastian Benjaminbbimber
authored
Custom server-side sorting (#264)
* Support custom sorting fields for JBrowse --------- Co-authored-by: Sebastian Benjamin <sebastiancbenjamin@gmail.com> Co-authored-by: bbimber <bbimber@gmail.com>
1 parent 0b0f1d0 commit a208193

File tree

5 files changed

+190
-27
lines changed

5 files changed

+190
-27
lines changed

jbrowse/src/client/JBrowse/VariantSearch/components/VariantTableWidget.tsx

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import {
55
GridColumnVisibilityModel,
66
GridPaginationModel,
77
GridRenderCellParams,
8+
GridSortDirection,
9+
GridSortModel,
810
GridToolbarColumnsButton,
911
GridToolbarContainer,
1012
GridToolbarDensitySelector,
1113
GridToolbarExport
1214
} from '@mui/x-data-grid';
1315
import SearchIcon from '@mui/icons-material/Search';
14-
import React, { useEffect, useState, useMemo } from 'react';
16+
import LinkIcon from '@mui/icons-material/Link';
17+
import React, { useEffect, useState } from 'react';
1518
import { getConf } from '@jbrowse/core/configuration';
1619
import { AppBar, Box, Button, Dialog, Paper, Popover, Toolbar, Tooltip, Typography } from '@mui/material';
1720
import { FilterFormModal } from './FilterFormModal';
@@ -67,22 +70,25 @@ const VariantTableWidget = observer(props => {
6770
session.hideWidget(widget)
6871
}
6972

70-
function handleQuery(passedFilters, pushToHistory, pageQueryModel = pageSizeModel) {
73+
function handleQuery(passedFilters, pushToHistory, pageQueryModel = pageSizeModel, sortQueryModel = sortModel) {
7174
const { page = pageSizeModel.page, pageSize = pageSizeModel.pageSize } = pageQueryModel;
75+
const { field = "genomicPosition", sort = false } = sortQueryModel[0] ?? {};
7276

7377
const encodedSearchString = createEncodedFilterString(passedFilters, false);
7478
const currentUrl = new URL(window.location.href);
7579
currentUrl.searchParams.set("searchString", encodedSearchString);
7680
currentUrl.searchParams.set("page", page.toString());
7781
currentUrl.searchParams.set("pageSize", pageSize.toString());
82+
currentUrl.searchParams.set("sortField", field.toString());
83+
currentUrl.searchParams.set("sortDirection", sort.toString());
7884

7985
if (pushToHistory) {
8086
window.history.pushState(null, "", currentUrl.toString());
8187
}
8288

8389
setFilters(passedFilters);
8490
setDataLoaded(false)
85-
fetchLuceneQuery(passedFilters, sessionId, trackGUID, page, pageSize, (json)=>{handleSearch(json)}, (error) => {setDataLoaded(true); setError(error)});
91+
fetchLuceneQuery(passedFilters, sessionId, trackGUID, page, pageSize, field, sort, (json)=>{handleSearch(json)}, (error) => {setDataLoaded(true); setError(error)});
8692
}
8793

8894
const TableCellWithPopover = (props: { value: any }) => {
@@ -190,7 +196,29 @@ const VariantTableWidget = observer(props => {
190196
Search
191197
</Button>
192198
<GridToolbarDensitySelector />
193-
<GridToolbarExport />
199+
<GridToolbarExport csvOptions={{
200+
delimiter: ';',
201+
}} />
202+
203+
<Button
204+
startIcon={<LinkIcon />}
205+
size="small"
206+
color="primary"
207+
onClick={() => {
208+
navigator.clipboard.writeText(window.location.href)
209+
.then(() => {
210+
// Popup message for successful copy
211+
alert('URL copied to clipboard.');
212+
})
213+
.catch(err => {
214+
// Error handling
215+
console.error('Failed to copy the URL: ', err);
216+
alert('Failed to copy the URL.');
217+
});
218+
}}
219+
>
220+
Share
221+
</Button>
194222
</GridToolbarContainer>
195223
);
196224
}
@@ -234,11 +262,15 @@ const VariantTableWidget = observer(props => {
234262
// False until initial data load or an error:
235263
const [dataLoaded, setDataLoaded] = useState(false)
236264

237-
const urlParams = new URLSearchParams(window.location.search);
238-
const page = parseInt(urlParams.get('page') || '0');
239-
const pageSize = parseInt(urlParams.get('pageSize') || '50');
265+
const urlParams = new URLSearchParams(window.location.search)
266+
const page = parseInt(urlParams.get('page') || '0')
267+
const pageSize = parseInt(urlParams.get('pageSize') || '50')
240268
const [pageSizeModel, setPageSizeModel] = React.useState<GridPaginationModel>({ page, pageSize });
241269

270+
const sortField = urlParams.get('sortField') || 'genomicPosition'
271+
const sortDirection = urlParams.get('sortDirection') || 'desc'
272+
const [sortModel, setSortModel] = React.useState<GridSortModel>([{ field: sortField, sort: sortDirection as GridSortDirection }])
273+
242274
const colVisURLComponent = urlParams.get("colVisModel") || "{}"
243275
const colVisModel = JSON.parse(decodeURIComponent(colVisURLComponent))
244276
const [columnVisibilityModel, setColumnVisibilityModel] = useState<GridColumnVisibilityModel>(colVisModel);
@@ -419,6 +451,11 @@ const VariantTableWidget = observer(props => {
419451
currentUrl.searchParams.set("colVisModel", encodeURIComponent(JSON.stringify(trueValuesModel)));
420452
window.history.pushState(null, "", currentUrl.toString());
421453
}}
454+
sortingMode="server"
455+
onSortModelChange={(newModel) => {
456+
setSortModel(newModel)
457+
handleQuery(filters, true, { page: 0, pageSize: pageSizeModel.pageSize }, newModel);
458+
}}
422459
/>
423460
)
424461

@@ -440,7 +477,7 @@ const VariantTableWidget = observer(props => {
440477
fieldTypeInfo: fieldTypeInfo,
441478
allowedGroupNames: allowedGroupNames,
442479
promotedFilters: promotedFilters,
443-
handleQuery: (filters) => handleQuery(filters, true)
480+
handleQuery: (filters) => handleQuery(filters, true, { page: 0, pageSize: pageSizeModel.pageSize}, sortModel)
444481
}}
445482
/>
446483
);

jbrowse/src/client/JBrowse/utils.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ function generateLuceneString(field, operator, value) {
419419
return luceneQueryString;
420420
}
421421

422-
export async function fetchLuceneQuery(filters, sessionId, trackGUID, offset, pageSize, successCallback, failureCallback) {
422+
export async function fetchLuceneQuery(filters, sessionId, trackGUID, offset, pageSize, sortField, sortReverseString, successCallback, failureCallback) {
423423
if (!offset) {
424424
offset = 0
425425
}
@@ -439,6 +439,13 @@ export async function fetchLuceneQuery(filters, sessionId, trackGUID, offset, pa
439439
return
440440
}
441441

442+
let sortReverse;
443+
if(sortReverseString == "asc") {
444+
sortReverse = true
445+
} else {
446+
sortReverse = false
447+
}
448+
442449
return Ajax.request({
443450
url: ActionURL.buildURL('jbrowse', 'luceneQuery.api'),
444451
method: 'GET',
@@ -449,7 +456,15 @@ export async function fetchLuceneQuery(filters, sessionId, trackGUID, offset, pa
449456
failure: function(res) {
450457
failureCallback("There was an error: " + res.status + "\n Status Body: " + res.responseText + "\n Session ID:" + sessionId)
451458
},
452-
params: {"searchString": createEncodedFilterString(filters, true), "sessionId": sessionId, "trackId": trackGUID, "offset": offset, "pageSize": pageSize},
459+
params: {
460+
"searchString": createEncodedFilterString(filters, true),
461+
"sessionId": sessionId,
462+
"trackId": trackGUID,
463+
"offset": offset,
464+
"pageSize": pageSize,
465+
"sortField": sortField ?? "genomicPosition",
466+
"sortReverse": sortReverse
467+
},
453468
});
454469
}
455470

jbrowse/src/org/labkey/jbrowse/JBrowseController.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -910,7 +910,7 @@ public ApiResponse execute(LuceneQueryForm form, BindException errors)
910910

911911
try
912912
{
913-
return new ApiSimpleResponse(searcher.doSearch(getUser(), PageFlowUtil.decode(form.getSearchString()), form.getPageSize(), form.getOffset()));
913+
return new ApiSimpleResponse(searcher.doSearch(getUser(), PageFlowUtil.decode(form.getSearchString()), form.getPageSize(), form.getOffset(), form.getSortField(), form.getSortReverse()));
914914
}
915915
catch (Exception e)
916916
{
@@ -947,6 +947,10 @@ public static class LuceneQueryForm
947947

948948
private int _offset = 0;
949949

950+
private String _sortField = "genomicPosition";
951+
952+
private boolean _sortReverse = false;
953+
950954
public String getSearchString()
951955
{
952956
return _searchString;
@@ -987,6 +991,14 @@ public void setOffset(int offset)
987991
_offset = offset;
988992
}
989993

994+
public String getSortField() { return _sortField; }
995+
996+
public void setSortField(String sortField) { _sortField = sortField; }
997+
998+
public boolean getSortReverse() { return _sortReverse; }
999+
1000+
public void setSortReverse(boolean sortReverse) { _sortReverse = sortReverse; }
1001+
9901002
public String getTrackId()
9911003
{
9921004
return _trackId;

jbrowse/src/org/labkey/jbrowse/JBrowseLuceneSearch.java

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.apache.lucene.search.TopFieldDocs;
2323
import org.apache.lucene.store.Directory;
2424
import org.apache.lucene.store.FSDirectory;
25+
import org.apache.lucene.util.NumericUtils;
2526
import org.jetbrains.annotations.Nullable;
2627
import org.json.JSONObject;
2728
import org.labkey.api.data.Container;
@@ -50,6 +51,7 @@
5051
import java.util.StringTokenizer;
5152
import java.util.regex.Matcher;
5253
import java.util.regex.Pattern;
54+
import java.util.stream.Collectors;
5355

5456
import static org.labkey.jbrowse.JBrowseFieldUtils.VARIABLE_SAMPLES;
5557
import static org.labkey.jbrowse.JBrowseFieldUtils.getSession;
@@ -61,6 +63,8 @@ public class JBrowseLuceneSearch
6163
private final JsonFile _jsonFile;
6264
private final User _user;
6365
private final String[] specialStartPatterns = {"*:* -", "+", "-"};
66+
private static final String ALL_DOCS = "all";
67+
private static final String GENOMIC_POSITION = "genomicPosition";
6468

6569
private JBrowseLuceneSearch(final JBrowseSession session, final JsonFile jsonFile, User u)
6670
{
@@ -130,7 +134,7 @@ public String extractFieldName(String queryString) {
130134
return parts.length > 0 ? parts[0].trim() : null;
131135
}
132136

133-
public JSONObject doSearch(User u, String searchString, final int pageSize, final int offset) throws IOException, ParseException
137+
public JSONObject doSearch(User u, String searchString, final int pageSize, final int offset, String sortField, boolean sortReverse) throws IOException, ParseException
134138
{
135139
searchString = tryUrlDecode(searchString);
136140
File indexPath = _jsonFile.getExpectedLocationOfLuceneIndex(true);
@@ -146,7 +150,7 @@ public JSONObject doSearch(User u, String searchString, final int pageSize, fina
146150
IndexSearcher indexSearcher = new IndexSearcher(indexReader);
147151

148152
List<String> stringQueryParserFields = new ArrayList<>();
149-
List<String> numericQueryParserFields = new ArrayList<>();
153+
Map<String, SortField.Type> numericQueryParserFields = new HashMap<>();
150154
PointsConfig intPointsConfig = new PointsConfig(new DecimalFormat(), Integer.class);
151155
PointsConfig doublePointsConfig = new PointsConfig(new DecimalFormat(), Double.class);
152156
Map<String, PointsConfig> pointsConfigMap = new HashMap<>();
@@ -161,11 +165,11 @@ public JSONObject doSearch(User u, String searchString, final int pageSize, fina
161165
{
162166
case Flag, String, Character -> stringQueryParserFields.add(field);
163167
case Float -> {
164-
numericQueryParserFields.add(field);
168+
numericQueryParserFields.put(field, SortField.Type.DOUBLE);
165169
pointsConfigMap.put(field, doublePointsConfig);
166170
}
167171
case Integer -> {
168-
numericQueryParserFields.add(field);
172+
numericQueryParserFields.put(field, SortField.Type.INT);
169173
pointsConfigMap.put(field, intPointsConfig);
170174
}
171175
}
@@ -182,14 +186,14 @@ public JSONObject doSearch(User u, String searchString, final int pageSize, fina
182186

183187
BooleanQuery.Builder booleanQueryBuilder = new BooleanQuery.Builder();
184188

185-
if (searchString.equals("all")) {
189+
if (searchString.equals(ALL_DOCS)) {
186190
booleanQueryBuilder.add(new MatchAllDocsQuery(), BooleanClause.Occur.MUST);
187191
}
188192

189193
// Split input into tokens, 1 token per query separated by &
190194
StringTokenizer tokenizer = new StringTokenizer(searchString, "&");
191195

192-
while (tokenizer.hasMoreTokens() && !searchString.equals("all"))
196+
while (tokenizer.hasMoreTokens() && !searchString.equals(ALL_DOCS))
193197
{
194198
String queryString = tokenizer.nextToken();
195199
Query query = null;
@@ -205,7 +209,7 @@ public JSONObject doSearch(User u, String searchString, final int pageSize, fina
205209
{
206210
query = queryParser.parse(queryString);
207211
}
208-
else if (numericQueryParserFields.contains(fieldName))
212+
else if (numericQueryParserFields.containsKey(fieldName))
209213
{
210214
try
211215
{
@@ -226,16 +230,28 @@ else if (numericQueryParserFields.contains(fieldName))
226230

227231
BooleanQuery query = booleanQueryBuilder.build();
228232

233+
// By default, sort in INDEXORDER, which is by genomicPosition
234+
Sort sort = Sort.INDEXORDER;
235+
236+
// If the sort field is not genomicPosition, use the provided sorting data
237+
if (!sortField.equals(GENOMIC_POSITION)) {
238+
SortField.Type fieldType;
239+
240+
if (stringQueryParserFields.contains(sortField)) {
241+
fieldType = SortField.Type.STRING;
242+
} else if (numericQueryParserFields.containsKey(sortField)) {
243+
fieldType = numericQueryParserFields.get(sortField);
244+
} else {
245+
throw new IllegalArgumentException("Could not find type for sort field: " + sortField);
246+
}
247+
248+
sort = new Sort(new SortField(sortField, fieldType, sortReverse));
249+
}
250+
229251
// Get chunks of size {pageSize}. Default to 1 chunk -- add to the offset to get more.
230252
// We then iterate over the range of documents we want based on the offset. This does grow in memory
231253
// linearly with the number of documents, but my understanding is that these are just score,id pairs
232254
// rather than full documents, so mem usage *should* still be pretty low.
233-
//TopDocs topDocs = indexSearcher.search(query, pageSize * (offset + 1));
234-
235-
// Define sort field
236-
SortField sortField = new SortField("pos", SortField.Type.INT, false);
237-
Sort sort = new Sort(sortField);
238-
239255
// Perform the search with sorting
240256
TopFieldDocs topDocs = indexSearcher.search(query, pageSize * (offset + 1), sort);
241257

@@ -253,10 +269,8 @@ else if (numericQueryParserFields.contains(fieldName))
253269
String fieldName = field.name();
254270
String[] fieldValues = doc.getValues(fieldName);
255271
if (fieldValues.length > 1) {
256-
// If there is more than one value, put the array of values into the JSON object.
257272
elem.put(fieldName, fieldValues);
258273
} else {
259-
// If there is only one value, just put this single value into the JSON object.
260274
elem.put(fieldName, fieldValues[0]);
261275
}
262276
}

0 commit comments

Comments
 (0)