# 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;