Skip to content

Commit

Permalink
Merge branch 'release/0.9.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
abought committed Nov 11, 2021
2 parents 42f86ad + f200251 commit fe2be48
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 96 deletions.
17 changes: 8 additions & 9 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "localzoom",
"version": "0.8.1",
"version": "0.9.0",
"license": "MIT",
"engines": {
"node": ">=10.13.0"
Expand All @@ -20,7 +20,7 @@
"@sentry/browser": "^4.5.2",
"bootstrap": "^4.4.1",
"bootstrap-vue": "^2.21.2",
"locuszoom": "git+https://github.com/statgen/locuszoom.git#780cfd8",
"locuszoom": "git+https://github.com/statgen/locuszoom.git#53ba107",
"lodash": "^4.17.11",
"tabulator-tables": "^4.9.0",
"vue": "^2.6.14",
Expand Down
247 changes: 194 additions & 53 deletions src/App.vue

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/components/ExportData.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import LocusZoom from 'locuszoom';
import { sourceName } from '../util/lz-helpers';
import TabulatorTable from './TabulatorTable.vue';
import { DATA_TYPES } from '../util/constants';
function formatSciNotation(cell, params) {
// Tabulator cell formatter using sci notation
Expand Down Expand Up @@ -39,7 +40,7 @@ export default {
},
gwas_tracks() {
return this.known_tracks.filter(({data_type}) => data_type === 'gwas');
return this.known_tracks.filter(({data_type}) => data_type === DATA_TYPES.GWAS);
},
table_config() {
Expand Down
2 changes: 1 addition & 1 deletion src/components/GwasToolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ export default {
class="col-sm-6">
<div v-if="study_count < max_studies">
<tabix-adder
:allow_ld="false"
:allow_ld="study_count > 0"
@ready="receiveTabixReader"
@fail="showMessage"
/>
Expand Down
9 changes: 5 additions & 4 deletions src/components/RegionPicker.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<template>
<div class="form-inline">
<form class="form-inline" @submit.prevent="selectRegion">
<vue-bootstrap-typeahead
:data="search_results"
v-model="region"
:serializer="s => s.term"
:min-matching-chars="3"
placeholder="chr:start-end, rs, or gene"/>
<button
<input
class="btn btn-primary"
@click="selectRegion">Go to region</button>
</div>
type="submit"
value="Go to region">
</form>
</template>

<script>
Expand Down
29 changes: 17 additions & 12 deletions src/components/TabixAdder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { makeBed12Parser, makeGWASParser, makePlinkLdParser } from 'locuszoom/es
import { blobReader, urlReader } from 'tabix-reader';
import GwasParserOptions from './GwasParserOptions.vue';
import { DATA_TYPES } from '../util/constants';
import { positionToStartRange } from '../util/entity-helpers';
Expand All @@ -21,14 +22,17 @@ export default {
return {
tabix_mode: 'file',
display_name: '',
data_type: 'gwas',
data_type: DATA_TYPES.GWAS,
tabix_gz_url: '',
// Options required to pass props to the GWAS modal
filename: '',
show_gwas_modal: false,
tabix_reader: null,
};
},
beforeCreate() {
this.DATA_TYPES = DATA_TYPES;
},
methods: {
reset() {
// Note: Deliberately don't reset datatype, for workflows of "add more of the same"
Expand Down Expand Up @@ -105,7 +109,7 @@ export default {
suggestRegion(data_type, reader, parser) {
// Note; the GWAS modal does this for GWAS data internally.
return new Promise((resolve, reject) => {
if (data_type === 'bed') {
if (data_type === DATA_TYPES.BED) {
// 1. Find headers (has tabs, does not begin with "browser", "track", or comment mark)
// 2. Get first data row and return the coordinates for that row. This method assumes the LZ parser with field names according to UCSC BED spec
const header_prefixes = /^(browser | track |#)/;
Expand Down Expand Up @@ -141,10 +145,9 @@ export default {
});
} else {
// Not implemented for other datatypes
return {};
resolve({});
}
});
},
createReader(event) {
Expand All @@ -165,28 +168,30 @@ export default {
}
reader_promise.then(([reader, filename]) => {
if (data_type === 'gwas') {
if (data_type === DATA_TYPES.GWAS) {
// GWAS files are very messy, and so knowing where to find the file is not enough.
// After receiving the reader, we need to ask the user how to parse the file. (via UI)
if (filename.includes('.bed')) {
if (filename.includes('.bed') || filename.includes('.ld')) {
// Check in case the user does something unwise. (2500 unique BED lines would be bad!)
throw new Error('Selected datatype GWAS does not match file extension .bed');
throw new Error('Wrong file extension for selected data type "GWAS"');
}
this.filename = filename;
this.tabix_reader = reader;
this.show_gwas_modal = true;
} else {
// All other data types are quasi-standardized, and hence we can declare this
// new track immediately after verifying that a valid reader exists
let parser;
if (data_type === 'bed') {
if (data_type === DATA_TYPES.BED) {
parser = makeBed12Parser({normalize: true});
if (!filename.includes('.bed')) {
throw new Error('BED interval file names must include extension .bed');
}
} else if (data_type === 'plink_ld') {
} else if (data_type === DATA_TYPES.PLINK_LD) {
parser = makePlinkLdParser({normalize: true});
if (!filename.includes('.ld')) {
throw new Error('User PLINK LD file names must include extension .ld');
}
} else {
throw new Error('Unrecognized datatype');
}
Expand All @@ -208,7 +213,7 @@ export default {
const { tabix_reader, filename, display_name } = this;
// Receive GWAS options and declare a new track for this datatype
const parser = makeGWASParser(parser_config);
this.sendTrackOptions('gwas', tabix_reader, parser, filename, display_name, region_config);
this.sendTrackOptions(DATA_TYPES.GWAS, tabix_reader, parser, filename, display_name, region_config);
this.reset();
},
Expand Down Expand Up @@ -290,7 +295,7 @@ export default {
name="data-type"
value="plink_ld"
>
PLINK 1.9 LD
PLINK 1.9 LD (overlay on GWAS; see guide above)
</b-form-radio>
</b-form-group>

Expand Down
8 changes: 8 additions & 0 deletions src/util/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ const REGEX_REGION = /^(?:chr)?(\w+)\s*:\s*(\d+)-(\d+)$/;

const DEFAULT_REGION_SIZE = 500000;

// Enum that controls recognized data types, so we can rename/ adjust as needed.
const DATA_TYPES = Object.freeze({
BED: 'bed',
GWAS: 'gwas',
PLINK_LD: 'plink_ld',
});

export {
DATA_TYPES,
DEFAULT_REGION_SIZE,
REGEX_REGION, REGEX_POSITION,
PORTAL_API_BASE_URL, LD_SERVER_BASE_URL,
Expand Down
91 changes: 77 additions & 14 deletions src/util/lz-helpers.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import escape from 'lodash/escape';
import LocusZoom from 'locuszoom';
import credibleSets from 'locuszoom/esm/ext/lz-credible-sets';
import tabixSource from 'locuszoom/esm/ext/lz-tabix-source';
import intervalTracks from 'locuszoom/esm/ext/lz-intervals-track';
import lzParsers from 'locuszoom/esm/ext/lz-parsers';

import { PORTAL_API_BASE_URL, LD_SERVER_BASE_URL } from './constants';
import { PORTAL_API_BASE_URL, LD_SERVER_BASE_URL, DATA_TYPES } from './constants';

LocusZoom.use(credibleSets);
LocusZoom.use(tabixSource);
Expand Down Expand Up @@ -56,12 +57,16 @@ function createGwasStudyLayout(
assoc: `assoc_${track_id}`,
};

const assoc_panel = LocusZoom.Layouts.get('panel', 'association', {
let assoc_panel = LocusZoom.Layouts.get('panel', 'association', {
id: `association_${track_id}`,
title: { text: display_name },
height: 275,
namespace,
});

// The PortalDev API uses a different name for the SE field. LocalZoom is a bit more descriptive.
assoc_panel = LocusZoom.Layouts.renameField(assoc_panel, 'assoc:se', 'assoc:stderr_beta', false);

const assoc_layer = assoc_panel.data_layers[2]; // layer 1 = recomb rate
assoc_layer.label = {
text: '{{#if assoc:rsid}}{{assoc:rsid}}{{#else}}{{assoc:variant}}{{/if}}',
Expand Down Expand Up @@ -145,18 +150,19 @@ function createGwasStudyLayout(
function createStudyLayouts (data_type, filename, display_name, annotations) {
const track_id = `${data_type}_${sourceName(filename)}`;

if (data_type === 'gwas') {
if (data_type === DATA_TYPES.GWAS) {
return createGwasStudyLayout(track_id, display_name, annotations);
} else if (data_type === 'bed') {
} else if (data_type === DATA_TYPES.BED) {
return [
LocusZoom.Layouts.get('panel', 'bed_intervals', {
id: track_id,
namespace: { intervals: track_id },
title: { text: display_name },
}),
];
} else if (data_type === 'plink_ld') {
throw new Error('Not yet implemented');
} else if (data_type === DATA_TYPES.PLINK_LD) {
// PLINK LD is overlaid onto the plot, but not shown as its own panel
return [];
} else {
throw new Error('Unrecognized datatype');
}
Expand Down Expand Up @@ -187,14 +193,18 @@ function createGwasTabixSources(track_id, tabix_reader, parser_func) {
function createStudySources(data_type, tabix_reader, filename, parser_func) {
// todo rename to GET from CREATE, for consistency
const track_id = `${data_type}_${sourceName(filename)}`;
if (data_type === 'gwas') {
if (data_type === DATA_TYPES.GWAS) {
return createGwasTabixSources(track_id, tabix_reader, parser_func);
} else if (data_type === 'bed') {
} else if (data_type === DATA_TYPES.BED) {
return [
[track_id, ['TabixUrlSource', {reader: tabix_reader, parser_func }]],
];
} else if (data_type === 'plink_ld') {
throw new Error('Not yet implemented');
} else if (data_type === DATA_TYPES.PLINK_LD) {
return [
// We are overriding LD in-place, not naming the source instance dynamically based on filename
// Replacing the source completely removes what could otherwise be a big unused cache object, and makes it simpler to add new tracks with a stock layout
['ld', ['UserTabixLD', {reader: tabix_reader, parser_func }]],
];
} else {
throw new Error('Unrecognized datatype');
}
Expand All @@ -203,9 +213,11 @@ function createStudySources(data_type, tabix_reader, filename, parser_func) {

function addPanels(plot, data_sources, panel_options, source_options) {
source_options.forEach(([name, options]) => {
if (!data_sources.has(name)) {
data_sources.add(name, options);
}
// If the same name is encountered twice, the new item will override.
// Elsewhere in this app, we also check if an item is duplicated and warn the user, so this shouldn't result in files being replaced.
// Relaxing the constraint internally allows us to override things like LD with a new source of exactly the same name
// TODO: Make more selective after lz-plot is rewritten to be more generic
data_sources.add(name, options, true);
});
panel_options.forEach((panel_layout) => {
panel_layout.y_index = -1; // Make sure genes track is always the last one
Expand Down Expand Up @@ -243,12 +255,63 @@ function getBasicLayout(initial_state = {}, study_panels = [], mods = {}) {
return LocusZoom.Layouts.get('plot', 'standard_association', extra);
}

/**
* When user LD is received, update the plot (irreversibly) to activate custom LD features
* 1. Remove the 1000G "ld pop" toolbar button (because those pops aren't relevant for user provided LD)
* 2. Add a new toolbar button describing the study
* 3. Tell the plot to use the new LD datasource when rendering anything that uses ld
* @param {LocusZoom.Plot} plot A reference to the LZ plot instance
* @param {String} display_name Display name
*/
function activateUserLD(plot, display_name) {
const widgets = plot.toolbar.widgets;
// Remove the old ld_population toolbar widget if present
const ld_pop_index = widgets.findIndex((item) => item.layout.tag === 'ld_population');
if (ld_pop_index !== -1) {
const ld_widget = widgets[ld_pop_index];
ld_widget.destroy();
widgets.splice(ld_pop_index, 1);
plot.toolbar.update();
}

// Add an LD description if not present
const ld_desc_index = widgets.findIndex((item) => item.layout.tag === 'user_ld_description');
let widget;
if (ld_desc_index !== -1) {
widget = widgets[ld_desc_index];
} else {
const config = {
tag: 'user_ld_description',
type: 'menu',
color: 'blue',
position: 'right',
button_html: 'USER LD',
menu_html: '',
};
// Awkward API wart- dynamic toolbar needs to modify toolbar in two ways
plot.layout.toolbar.widgets.push(config);
widget = plot.toolbar.addWidget(config);
}
// Update the UI help button EVERY time user LD is added, eg let user swap out LD to a different file when they switch regions
// (I'd RATHER they provide LD into all one file for regions, while still being a small file. But let's expect the most kludgy workflows to win)
const caption = `This plot is being rendered with user-provided LD. The filename is: <b>${escape(display_name)}</b>`;
LocusZoom.Layouts.mutate_attrs(plot.layout, '$..widgets[?(@.tag === "user_ld_description")].menu_html', caption);
widget.show();

// Since the LD is swapped out, we need to tell the plot to re-parse data config (LZ caches datasource options until told not to)
// Also, Update any UI captions / toolbar widgets
plot.mutateLayout();
plot.toolbar.update();
// Re-render with the new data
plot.applyState();
}

export {
// Basic definitions
getBasicSources, getBasicLayout,
createStudySources, createStudyLayouts,
// Plot manipulation
sourceName, addPanels,
sourceName, activateUserLD, addPanels,
// Constants
stateUrlMapping,
};

0 comments on commit fe2be48

Please sign in to comment.