From 170d01e541dee3f36b88ea9106bf762eae950a29 Mon Sep 17 00:00:00 2001 From: Andy Boughton Date: Mon, 13 Dec 2021 15:43:15 -0500 Subject: [PATCH 1/3] Always show catalog and credset annos: most people use, and without configuration, the combined layout is far easier to read --- src/App.vue | 4 - src/components/ExportData.vue | 9 +- src/components/GwasToolbar.vue | 40 +---- src/components/PlotPanes.vue | 2 - src/util/lz-helpers.js | 289 +++++++++++++++++++++++---------- 5 files changed, 206 insertions(+), 138 deletions(-) diff --git a/src/App.vue b/src/App.vue index d893f76..097c072 100644 --- a/src/App.vue +++ b/src/App.vue @@ -38,8 +38,6 @@ export default { // State to be tracked across all components genome_build: 'GRCh37', known_tracks: [], - // Control specific display options - has_credible_sets: true, }; }, computed: { @@ -295,7 +293,6 @@ export default {
[] }, - has_credible_sets: { type: Boolean, default: true }, table_data: { type: Array, default: () => [] }, }, data() { @@ -78,13 +77,9 @@ export default { formatterParams: { precision: 3 }, sorter: 'number', }, + { title: 'Cred. set', field: 'credset:is_member', formatter: 'tickCross' }, + { title: 'Posterior probability', field: 'credset:posterior_prob', formatter: formatSciNotation }, ]; - if (this.has_credible_sets) { - base.push( - { title: 'Cred. set', field: 'credset:is_member', formatter: 'tickCross' }, - { title: 'Posterior probability', field: 'credset:posterior_prob', formatter: formatSciNotation }, - ); - } return base; }, }, diff --git a/src/components/GwasToolbar.vue b/src/components/GwasToolbar.vue index 4e418a4..36cfd1b 100644 --- a/src/components/GwasToolbar.vue +++ b/src/components/GwasToolbar.vue @@ -26,10 +26,6 @@ export default { type: String, default: 'GRCh37', }, - has_credible_sets: { - type: Boolean, - default: true, - }, // Limit how many studies can be added (due to browser performance) max_studies: { type: Number, @@ -53,9 +49,6 @@ export default { message: '', message_class: '', - // Allow the user to customize the plot and select featured annotations. - has_gwas_catalog: true, - // Controls for "batch view" mode batch_mode_active: false, batch_mode_regions: [], @@ -75,14 +68,6 @@ export default { this.$emit('update:genome_build', newValue); }, }, - i_has_credible_sets: { - get: function() { - return this.has_credible_sets; - }, - set: function(newValue) { - this.$emit('update:has_credible_sets', newValue); - }, - }, }, methods: { reset() { @@ -116,11 +101,11 @@ export default { return; } - const { genome_build, has_gwas_catalog, has_credible_sets } = this; + const { genome_build } = this; const track_sources = createStudySources(data_type, reader, filename, parser_func); - const panel_layouts = createStudyLayouts(data_type, filename, display_name, {has_gwas_catalog, has_credible_sets}); + const panel_layouts = createStudyLayouts(data_type, filename, display_name); const new_plot_state = { genome_build, ...metadata }; this.$emit('add-tabix-track', data_type, filename, display_name, track_sources, panel_layouts, new_plot_state); @@ -176,27 +161,6 @@ export default { variant="info" class="float-right">
- Annotations
-
- - -
-
- - -
Genome Build
diff --git a/src/util/lz-helpers.js b/src/util/lz-helpers.js index ee995f0..3b56408 100644 --- a/src/util/lz-helpers.js +++ b/src/util/lz-helpers.js @@ -26,6 +26,195 @@ class TabixAssociationLZ extends TabixUrlSource { LocusZoom.Adapters.add('TabixAssociationLZ', TabixAssociationLZ); + +const localzoom_assoc_layer = function () { + const assoc_tooltip = LocusZoom.Layouts.get('tooltip', 'standard_association_with_label'); + assoc_tooltip.html += `{{#if assoc:beta|is_numeric}}
β: {{assoc:beta|scinotation|htmlescape}}{{/if}} +{{#if assoc:stderr_beta|is_numeric}}
SE (β): {{assoc:stderr_beta|scinotation|htmlescape}}{{/if}} +{{#if assoc:alt_allele_freq|is_numeric}}
Alt. freq: {{assoc:alt_allele_freq|scinotation|htmlescape}} {{/if}} +{{#if assoc:rsid}}
rsID: {{assoc:rsid|htmlescape}}{{/if}} +{{#if credset:posterior_prob}}
Posterior probability: {{credset:posterior_prob|scinotation}}{{/if}}
+ +{{#if catalog:rsid}}
See hits in GWAS catalog{{/if}}`; + + return LocusZoom.Layouts.get('data_layer', 'association_pvalues', { + namespace: { catalog: 'catalog', credset: 'credset' }, + data_operations: [ + // This combines many pieces of data into a single cohesive set of records + { + type: 'fetch', + from: ['assoc', 'catalog', 'ld(assoc)', 'credset(assoc)'], + }, + { + type: 'left_match', + name: 'credset_plus_ld', + requires: ['credset', 'ld'], + params: ['assoc:position', 'ld:position2'], + }, + { + type: 'assoc_to_gwas_catalog', + name: 'assoc_catalog', + requires: ['credset_plus_ld', 'catalog'], + params: ['assoc:position', 'catalog:pos', 'catalog:log_pvalue'], + }, + ], + tooltip: assoc_tooltip, + label: { + text: '{{#if assoc:rsid}}{{assoc:rsid}}{{#else}}{{assoc:variant}}{{/if}}', + spacing: 12, + lines: { style: { 'stroke-width': '2px', stroke: '#333333', 'stroke-dasharray': '2px 2px' } }, + filters: [ + { field: 'lz_show_label', operator: '=', value: true }, + ], + style: { 'font-weight': 'bold' }, + }, + }); +}(); + + +// Register reusable rendering options for LocalZoom association tracks (a set of combined custom features) +LocusZoom.Layouts.add('data_layer', 'localzoom_assoc', localzoom_assoc_layer); + +const localzoom_assoc_panel = function () { + // LocalZoom renders an association track with several annotations that are not combined anywhere else; we define a custom track + const base = LocusZoom.Layouts.get('panel', 'association', { + height: 300, + data_layers: [ + LocusZoom.Layouts.get('data_layer', 'significance'), + LocusZoom.Layouts.get('data_layer', 'recomb_rate'), + LocusZoom.Layouts.get('data_layer', 'localzoom_assoc'), + ], + }); + // Add display options for catalog + credsets + base.toolbar.widgets.push({ + type: 'display_options', + custom_event_name: 'widget_association_display_options_choice', + position: 'right', + color: 'blue', + // Below: special config specific to this widget + button_html: 'Display options...', + button_title: 'Control how plot items are displayed', + layer_name: 'associationpvalues', + default_config_display_name: 'Default view', + options: [ + { + // First dropdown menu item + display_name: '95% credible set (boolean)', // Human readable representation of field name + display: { // Specify layout directives that control display of the plot for this option + point_shape: 'circle', + point_size: 40, + color: { + field: 'credset:is_member', + scale_function: 'if', + parameters: { + field_value: true, + then: '#00CC00', + else: '#CCCCCC', + }, + }, + legend: [ // Tells the legend how to represent this display option + { + shape: 'circle', + color: '#00CC00', + size: 40, + label: 'In credible set', + class: 'lz-data_layer-scatter', + }, + { + shape: 'circle', + color: '#CCCCCC', + size: 40, + label: 'Not in credible set', + class: 'lz-data_layer-scatter', + }, + ], + }, + }, + { + display_name: '95% credible set (gradient by contribution)', + display: { + point_shape: 'circle', + point_size: 40, + color: [ + { + field: 'credset:contrib_fraction', + scale_function: 'if', + parameters: { + field_value: 0, + then: '#777777', + }, + }, + { + scale_function: 'interpolate', + field: 'credset:contrib_fraction', + parameters: { + breaks: [0, 1], + values: ['#fafe87', '#9c0000'], + }, + }, + ], + legend: [ + { + shape: 'circle', + color: '#777777', + size: 40, + label: 'No contribution', + class: 'lz-data_layer-scatter', + }, + { + shape: 'circle', + color: '#fafe87', + size: 40, + label: 'Some contribution', + class: 'lz-data_layer-scatter', + }, + { + shape: 'circle', + color: '#9c0000', + size: 40, + label: 'Most contribution', + class: 'lz-data_layer-scatter', + }, + ], + }, + }, + { + display_name: 'Label catalog traits', + display: { // Specify layout directives that control display of the plot for this option + label: { + text: '{{catalog:trait}}', + spacing: 6, + lines: { + style: { + 'stroke-width': '2px', + 'stroke': '#333333', + 'stroke-dasharray': '2px 2px', + }, + }, + filters: [ + // Only label points if they are significant for some trait in the catalog, AND in high LD + // with the top hit of interest + { field: 'catalog:trait', operator: '!=', value: null }, + { field: 'catalog:log_pvalue', operator: '>', value: 7.301 }, + { field: 'ld:correlation', operator: '>', value: 0.4 }, + ], + style: { + 'font-size': '12px', + 'font-weight': 'bold', + 'fill': '#333333', + }, + }, + }, + }, + ], + }); + // LocalZoom field names are slightly more verbose than the PortalDev API; rename this field so everything works + LocusZoom.Layouts.renameField(base, 'assoc:se', 'assoc:stderr_beta', false); + return base; +}(); + +LocusZoom.Layouts.add('panel', 'localzoom_assoc', localzoom_assoc_panel); + /** * A source name cannot contain special characters (this would break the layout) * @@ -42,96 +231,23 @@ function sourceName(display_name) { * of the features selected. * @param {string} track_id Internal track ID, expected to be unique across the entire plot. * @param {string} display_name - * @param annotations * @return {*[]} */ function createGwasStudyLayout( track_id, display_name, - annotations = { has_credible_sets: false, has_gwas_catalog: true }, ) { - const new_panels = []; - - // Other namespaces won't be overridden; they will be reused as is. - const namespace = { - assoc: `assoc_${track_id}`, - }; - - 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}}', - spacing: 12, - lines: { style: { 'stroke-width': '2px', stroke: '#333333', 'stroke-dasharray': '2px 2px' } }, - filters: [ - { field: 'lz_show_label', operator: '=', value: true }, - ], - style: { 'font-weight': 'bold' }, - }; - assoc_layer.tooltip = LocusZoom.Layouts.get('tooltip', 'standard_association_with_label'); - const assoc_tooltip = assoc_layer.tooltip; - - assoc_tooltip.html += `{{#if assoc:beta|is_numeric}}
β: {{assoc:beta|scinotation|htmlescape}}{{/if}} -{{#if assoc:stderr_beta|is_numeric}}
SE (β): {{assoc:stderr_beta|scinotation|htmlescape}}{{/if}} -{{#if assoc:alt_allele_freq|is_numeric}}
Alt. freq: {{assoc:alt_allele_freq|scinotation|htmlescape}} {{/if}} -{{#if assoc:rsid}}
rsID: {{assoc:rsid|htmlescape}}{{/if}}`; - const dash_extra = []; // Build Display options widget & add to toolbar iff features selected - if (Object.values(annotations).some((item) => !!item)) { - dash_extra.push({ - type: 'display_options', - custom_event_name: 'widget_association_display_options_choice', - position: 'right', - color: 'blue', - // Below: special config specific to this widget - button_html: 'Display options...', - button_title: 'Control how plot items are displayed', - layer_name: 'associationpvalues', - default_config_display_name: 'Default view', - options: [], - }); - } - if (annotations.has_credible_sets) { - // Grab the options object from a pre-existing layout - const basis = LocusZoom.Layouts.get('panel', 'association_credible_set', { namespace }); - dash_extra[0].options.push(...basis.toolbar.widgets.pop().options); - assoc_tooltip.html += '{{#if credset:posterior_prob}}
Posterior probability: {{credset:posterior_prob|scinotation}}{{/if}}
'; - // Tell the layer to fetch the extra data - assoc_layer.namespace.credset = `credset_${track_id}`; - LocusZoom.Layouts.mutate_attrs(assoc_layer, '$..data_operations[?(@.type === "fetch")].from', (old) => old.concat('credset(assoc)')); - } - if (annotations.has_gwas_catalog) { - assoc_layer.data_operations.push({ - type: 'assoc_to_gwas_catalog', - name: 'assoc_catalog', - requires: ['assoc_plus_ld', 'catalog'], - params: ['assoc:position', 'catalog:pos', 'catalog:log_pvalue'], - }); - - // Grab the options object from a pre-existing layout, and add it to the dropdown menu - const basis = LocusZoom.Layouts.get('panel', 'association_catalog'); - dash_extra[0].options.push(...basis.toolbar.widgets.pop().options); - assoc_tooltip.html += '{{#if catalog:rsid}}
See hits in GWAS catalog{{/if}}'; + // Override the dataset-specific namespaces, so that each layer knows how to find data + const namespace = { assoc: `assoc_${track_id}`, credset: `credset_${track_id}` }; - // Tell this layer how to fetch the extra data required - assoc_layer.namespace.catalog = 'catalog'; - LocusZoom.Layouts.mutate_attrs(assoc_layer, '$..data_operations[?(@.type === "fetch")].from', (old) => old.concat('catalog')); - } - assoc_panel.toolbar.widgets.push(...dash_extra); - - // After all custom options added, run mods through Layouts.get once more to apply namespacing - new_panels.push(assoc_panel); - if (annotations.has_gwas_catalog) { - new_panels.push(LocusZoom.Layouts.get('panel', 'annotation_catalog', { + return [ + LocusZoom.Layouts.get('panel', 'localzoom_assoc', { + id: `association_${track_id}`, + namespace, + title: { text: display_name }, + }), + LocusZoom.Layouts.get('panel', 'annotation_catalog', { id: `catalog_${track_id}`, namespace, title: { @@ -139,19 +255,18 @@ function createGwasStudyLayout( style: { 'font-size': '14px' }, x: 50, }, - })); - } - return new_panels; + }), + ]; } /** * Create an appropriate layout based on datatype */ -function createStudyLayouts (data_type, filename, display_name, annotations) { +function createStudyLayouts (data_type, filename, display_name) { const track_id = `${data_type}_${sourceName(filename)}`; if (data_type === DATA_TYPES.GWAS) { - return createGwasStudyLayout(track_id, display_name, annotations); + return createGwasStudyLayout(track_id, display_name); } else if (data_type === DATA_TYPES.BED) { return [ LocusZoom.Layouts.get('panel', 'bed_intervals', { From 95297dde78e2cb2bb248249cfcec8a2efe115a8a Mon Sep 17 00:00:00 2001 From: Andy Boughton Date: Mon, 13 Dec 2021 19:52:46 -0500 Subject: [PATCH 2/3] Fix bugs with setting LDrefvar --- package-lock.json | 29 +++++++++++++++++++---------- package.json | 2 +- src/components/GwasToolbar.vue | 3 +-- src/util/lz-helpers.js | 19 ++++++++++--------- 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 78ba7b6..dd90fd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1872,7 +1872,8 @@ "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "dev": true }, "ansi-styles": { "version": "3.2.1", @@ -7965,8 +7966,8 @@ } }, "locuszoom": { - "version": "git+https://github.com/statgen/locuszoom.git#71720dea275062ea4f2a6384c843c2cbf2f3f1a6", - "from": "git+https://github.com/statgen/locuszoom.git#71720de", + "version": "git+https://github.com/statgen/locuszoom.git#33316cde4b0270aa7b8c027a68b8f65b1bd843f5", + "from": "git+https://github.com/statgen/locuszoom.git#33316cd", "requires": { "d3": "^5.16.0", "gwas-credible-sets": "^0.1.0", @@ -12630,6 +12631,11 @@ "uri-js": "^4.2.2" } }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, "chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -12784,6 +12790,11 @@ "through": "^2.3.6" }, "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, "strip-ansi": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", @@ -12847,13 +12858,6 @@ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "requires": { "ansi-regex": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - } } }, "table": { @@ -12867,6 +12871,11 @@ "string-width": "^3.0.0" }, "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", diff --git a/package.json b/package.json index f536cc8..8e35e4e 100644 --- a/package.json +++ b/package.json @@ -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#71720de", + "locuszoom": "git+https://github.com/statgen/locuszoom.git#33316cd", "lodash": "^4.17.11", "tabulator-tables": "^5.0.7", "vue": "^2.6.14", diff --git a/src/components/GwasToolbar.vue b/src/components/GwasToolbar.vue index 36cfd1b..2457612 100644 --- a/src/components/GwasToolbar.vue +++ b/src/components/GwasToolbar.vue @@ -157,11 +157,10 @@ export default {
- Genome Build
Date: Mon, 13 Dec 2021 20:04:17 -0500 Subject: [PATCH 3/3] Bump for version 0.9.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e35e4e..c3dd083 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "localzoom", - "version": "0.9.3", + "version": "0.9.4", "license": "MIT", "engines": { "node": ">=10.13.0"