Source: ext/lz-credible-sets.js

/**
 * Custom code used to power credible sets demonstration example. This is not part of the core LocusZoom library,
 * but can be included as a standalone file.
 *
 * ### Features provided
 * * {@link module:LocusZoom_Adapters~CredibleSetLZ}
 * * {@link module:LocusZoom_Layouts~association_credible_set_tooltip}
 * * {@link module:LocusZoom_Layouts~annotation_credible_set_tooltip}
 * * {@link module:LocusZoom_Layouts~association_credible_set}
 * * {@link module:LocusZoom_Layouts~annotation_credible_set_layer}
 * * {@link module:LocusZoom_Layouts~annotation_credible_set}
 * * {@link module:LocusZoom_Layouts~association_credible_set}
 * * {@link module:LocusZoom_Layouts~association_credible_set_plot}
 *
 * ### Loading and usage
 * The page must incorporate and load all libraries before this file can be used, including:
 *  - LocusZoom
 *
 * To use in an environment without special JS build tooling, simply load the extension file as JS from a CDN (after any dependencies):
 * ```
 * <script src="https://cdn.jsdelivr.net/npm/locuszoom@INSERT_VERSION_HERE/dist/ext/lz-credible-sets.min.js" type="application/javascript"></script>
 * ```
 *
 * To use with ES6 modules, the plugin must be loaded and registered explicitly before use:
 * ```
 * import LocusZoom from 'locuszoom';
 * import credibleSets from 'locuszoom/esm/ext/lz-credible-sets';
 * LocusZoom.use(credibleSets);
 * ```
 @module
*/

import {marking, scoring} from 'gwas-credible-sets';

function install (LocusZoom) {
    const BaseUMAdapter = LocusZoom.Adapters.get('BaseUMAdapter');

    /**
     * (**extension**) Custom data adapter that calculates the 95% credible set based on provided association data.
     * This source must be requested as the second step in a chain, after a previous step that returns fields required
     *  for the calculation. (usually, it follows a request for GWAS summary statistics)
     * @alias module:LocusZoom_Adapters~CredibleSetLZ
     * @see {@link module:ext/lz-credible-sets} for required extension and installation instructions
     */
    class CredibleSetLZ extends BaseUMAdapter {
        /**
         * @param {Number} [config.params.threshold=0.95] The credible set threshold (eg 95%). Will continue selecting SNPs
         *  until the posterior probabilities add up to at least this fraction of the total.
         * @param {Number} [config.params.significance_threshold=7.301] Do not perform a credible set calculation for this
         *  region unless AT LEAST ONE SNP (as -log10p) exceeds the line of GWAS signficance. Otherwise we are declaring a
         *  credible set when there is no evidence of anything being significant at all. If one snp is significant, it will
         *  create a credible set for the entire region; the resulting set may include things below the line of significance.
         */
        constructor(config) {
            super(...arguments);
            // Set defaults. Default sig threshold is the line of GWAS significance. (as -log10p)
            this._config = Object.assign(
                { threshold: 0.95, significance_threshold: 7.301 },
                this._config,
            );
            this._prefix_namespace = false;
        }

        _getCacheKey (state) {
            const threshold = state.credible_set_threshold || this._config.threshold;
            return [threshold, state.chr, state.start, state.end].join('_');
        }

        _buildRequestOptions(options, assoc_data) {
            const base = super._buildRequestOptions(...arguments);
            base._assoc_data = assoc_data;
            return base;
        }

        _performRequest(options) {
            const {_assoc_data} = options;
            if (!_assoc_data.length) {
                // No credible set can be calculated because there is no association data for this region
                return Promise.resolve([]);
            }

            const assoc_logp_name = this._findPrefixedKey(_assoc_data[0], 'log_pvalue');

            const threshold = this._config.threshold;

            // Calculate raw bayes factors and posterior probabilities based on information returned from the API
            const nlogpvals = _assoc_data.map((item) => item[assoc_logp_name]);

            if (!nlogpvals.some((val) => val >= this._config.significance_threshold)) {
                // If NO points have evidence of significance, define the credible set to be empty
                //  (rather than make a credible set that we don't think is meaningful)
                return Promise.resolve(_assoc_data);
            }

            try {
                const scores = scoring.bayesFactors(nlogpvals);
                const posteriorProbabilities = scoring.normalizeProbabilities(scores);

                // Use scores to mark the credible set in various ways (depending on your visualization preferences,
                //   some of these may not be needed)
                const credibleSet = marking.findCredibleSet(posteriorProbabilities, threshold);
                const credSetScaled = marking.rescaleCredibleSet(credibleSet);
                const credSetBool = marking.markBoolean(credibleSet);

                // Annotate each response record based on credible set membership. This has the effect of joining
                //   credset results to assoc data directly within the adapter (no separate join needed)
                for (let i = 0; i < _assoc_data.length; i++) {
                    _assoc_data[i][`${options._provider_name}:posterior_prob`] = posteriorProbabilities[i];
                    _assoc_data[i][`${options._provider_name}:contrib_fraction`] = credSetScaled[i];
                    _assoc_data[i][`${options._provider_name}:is_member`] = credSetBool[i];
                }
            } catch (e) {
                // If the calculation cannot be completed, return the data without annotation fields
                console.error(e);
            }
            return Promise.resolve(_assoc_data);
        }
    }

    LocusZoom.Adapters.add('CredibleSetLZ', CredibleSetLZ);

    // Add related layouts to the central global registry
    /**
     * (**extension**) Tooltip layout that appends credible set posterior probability to the default association tooltip (for SNPs in the credible set)
     * @alias module:LocusZoom_Layouts~association_credible_set_tooltip
     * @type tooltip
     * @see {@link module:ext/lz-credible-sets} for required extension and installation instructions
     */
    const association_credible_set_tooltip = function () {
        // Extend a known tooltip with an extra row of info showing posterior probabilities
        const l = LocusZoom.Layouts.get('tooltip', 'standard_association');
        l.html += '{{#if credset:posterior_prob}}<br>Posterior probability: <strong>{{credset:posterior_prob|scinotation|htmlescape}}</strong>{{/if}}';
        return l;
    }();

    LocusZoom.Layouts.add('tooltip', 'association_credible_set', association_credible_set_tooltip);

    /**
     * (**extension**) A tooltip layout for annotation (rug) tracks that provides information about credible set members
     * @alias module:LocusZoom_Layouts~annotation_credible_set_tooltip
     * @type tooltip
     * @see {@link module:ext/lz-credible-sets} for required extension and installation instructions
     */
    const annotation_credible_set_tooltip = {
        closable: true,
        show: { or: ['highlighted', 'selected'] },
        hide: { and: ['unhighlighted', 'unselected'] },
        html: '<strong>{{assoc:variant|htmlescape}}</strong><br>'
            + 'P Value: <strong>{{assoc:log_pvalue|logtoscinotation|htmlescape}}</strong><br>' +
            '{{#if credset:posterior_prob}}<br>Posterior probability: <strong>{{credset:posterior_prob|scinotation|htmlescape}}</strong>{{/if}}',
    };
    LocusZoom.Layouts.add('tooltip', 'annotation_credible_set', annotation_credible_set_tooltip);

    /**
     * (**extension**) A data layer layout that shows GWAS summary statistics overlaid with credible set membership information
     * @alias module:LocusZoom_Layouts~association_credible_set_layer
     * @type data_layer
     * @see {@link module:ext/lz-credible-sets} for required extension and installation instructions
     */

    const association_credible_set_layer = function () {
        const base = LocusZoom.Layouts.get('data_layer', 'association_pvalues', {
            id: 'associationcredibleset',
            namespace: { 'assoc': 'assoc', 'credset': 'credset', 'ld': 'ld' },
            data_operations: [
                {
                    type: 'fetch',
                    from: ['assoc', 'ld(assoc)', 'credset(assoc)'],
                },
                {
                    type: 'left_match',
                    name: 'credset_plus_ld',
                    requires: ['credset', 'ld'],  // The credible sets demo wasn't fully moved over to the new data operations system, and as such it is a bit weird
                    params: ['assoc:position', 'ld:position2'],  // FIXME: old LZ used position, because it was less sensitive to format. We'd like to match assoc:variant = ld:variant2, but not every assoc source provides variant data in the way we need. This would need to be fixed via special formatting adjustment later.
                },
            ],
            fill_opacity: 0.7,
            tooltip: LocusZoom.Layouts.get('tooltip', 'association_credible_set'),
            match: { send: 'assoc:variant', receive: 'assoc:variant' },
        });
        base.color.unshift({
            field: 'lz_is_match',  // Special field name whose presence triggers custom rendering
            scale_function: 'if',
            parameters: {
                field_value: true,
                then: '#FFf000',
            },
        });
        return base;
    }();
    LocusZoom.Layouts.add('data_layer', 'association_credible_set', association_credible_set_layer);

    /**
     * (**extension**) A data layer layout that shows a vertical mark whenever a SNP is a member of the credible set
     * @alias module:LocusZoom_Layouts~annotation_credible_set_layer
     * @type data_layer
     * @see {@link module:ext/lz-credible-sets} for required extension and installation instructions
     */
    const annotation_credible_set_layer = {
        namespace: { 'assoc': 'assoc', 'credset': 'credset' },
        data_operations: [{
            type: 'fetch',
            from: ['assoc', 'credset(assoc)'],
        }],
        id: 'annotationcredibleset',
        type: 'annotation_track',
        id_field: 'assoc:variant',
        x_axis: {
            field: 'assoc:position',
        },
        color: [
            {
                field: 'lz_is_match',  // Special field name whose presence triggers custom rendering
                scale_function: 'if',
                parameters: {
                    field_value: true,
                    then: '#001cee',
                },
            },
            '#00CC00',
        ],
        match: { send: 'assoc:variant', receive: 'assoc:variant' },
        filters: [
            // Specify which points to show on the track. Any selection must satisfy ALL filters
            { field: 'credset:is_member', operator: '=', value: true },
        ],
        behaviors: {
            onmouseover: [
                { action: 'set', status: 'highlighted' },
            ],
            onmouseout: [
                { action: 'unset', status: 'highlighted' },
            ],
            onclick: [
                { action: 'toggle', status: 'selected', exclusive: true },
            ],
            onshiftclick: [
                { action: 'toggle', status: 'selected' },
            ],
        },
        tooltip: LocusZoom.Layouts.get('tooltip', 'annotation_credible_set'),
        tooltip_positioning: 'top',
    };
    LocusZoom.Layouts.add('data_layer', 'annotation_credible_set', annotation_credible_set_layer);

    /**
     * (**extension**) A panel layout that shows a vertical mark whenever a SNP is a member of the credible set
     * @alias module:LocusZoom_Layouts~annotation_credible_set
     * @type panel
     * @see {@link module:ext/lz-credible-sets} for required extension and installation instructions
     */
    const annotation_credible_set = {
        id: 'annotationcredibleset',
        title: { text: 'SNPs in 95% credible set', x: 50, style: { 'font-size': '14px' } },
        min_height: 50,
        height: 50,
        margin: { top: 25, right: 50, bottom: 10, left: 70 },
        inner_border: 'rgb(210, 210, 210)',
        toolbar: LocusZoom.Layouts.get('toolbar', 'standard_panel'),
        axes: {
            x: { extent: 'state', render: false },
        },
        interaction: {
            drag_background_to_pan: true,
            scroll_to_zoom: true,
            x_linked: true,
        },
        data_layers: [
            LocusZoom.Layouts.get('data_layer', 'annotation_credible_set'),
        ],
    };
    LocusZoom.Layouts.add('panel', 'annotation_credible_set', annotation_credible_set);

    /**
     * (**extension**) A panel layout that shows GWAS summary statistics in a standard LocusZoom view, overlaid with credible set membership information
     * @alias module:LocusZoom_Layouts~association_credible_set
     * @type panel
     * @see {@link module:ext/lz-credible-sets} for required extension and installation instructions
     */
    const association_credible_set_panel = function () {
        const l = LocusZoom.Layouts.get('panel', 'association', {
            id: 'associationcrediblesets',
            data_layers: [
                LocusZoom.Layouts.get('data_layer', 'significance'),
                LocusZoom.Layouts.get('data_layer', 'recomb_rate'),
                LocusZoom.Layouts.get('data_layer', 'association_credible_set'),
            ],
        });
        // Add "display options" button to control how credible set coloring is overlaid on the standard association plot
        l.toolbar.widgets.push(
            {
                type: 'display_options',
                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: 'associationcredibleset',
                default_config_display_name: 'Linkage Disequilibrium (default)', // display name for the default plot color option (allow user to revert to plot defaults)

                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',
                                },
                            ],
                        },
                    },
                    {
                        // Second option. The same plot- or even the same field- can be colored in more than one way.
                        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',
                                },
                            ],
                        },
                    },
                ],
            },
        );
        return l;
    }();
    LocusZoom.Layouts.add('panel', 'association_credible_set', association_credible_set_panel);

    /**
     * (**extension**) A standard LocusZoom plot layout, with additional credible set information.
     * @alias module:LocusZoom_Layouts~association_credible_set_plot
     * @type plot
     * @see {@link module:ext/lz-credible-sets} for required extension and installation instructions
     */
    const association_credible_set_plot = {
        state: {},
        width: 800,
        height: 450,
        responsive_resize: true,
        min_region_scale: 20000,
        max_region_scale: 1000000,
        toolbar: LocusZoom.Layouts.get('toolbar', 'standard_association'),
        panels: [
            LocusZoom.Layouts.get('panel', 'association_credible_set'),
            LocusZoom.Layouts.get('panel', 'annotation_credible_set'),
            LocusZoom.Layouts.get('panel', 'genes'),
        ],
    };
    LocusZoom.Layouts.add('plot', 'association_credible_set', association_credible_set_plot);
}


if (typeof LocusZoom !== 'undefined') {
    // Auto-register the plugin when included as a script tag. ES6 module users must register via LocusZoom.use()
    // eslint-disable-next-line no-undef
    LocusZoom.use(install);
}


export default install;