# Source: ext/lz-intervals-enrichment.js

    /**
     * Interval annotation track that groups annotations by enrichment value (a fixed y-axis) rather than by merged/split tracks.

     * This is not part of the core LocusZoom library, but can be included as a standalone file.

     * ### Features provided
     * * {@link module:LocusZoom_DataLayers~intervals_enrichment}
     * * {@link module:LocusZoom_Layouts~intervals_association_enrichment}
     * * {@link module:LocusZoom_Layouts~intervals_enrichment_panel}
     * * {@link module:LocusZoom_Layouts~intervals_enrichment_data_layer}
     * * {@link module:LocusZoom_Layouts~intervals_enrichment_tooltip}
     *
     *
     * ### 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):
     * ```javascript
     * <script src="https://cdn.jsdelivr.net/npm/locuszoom@INSERT_VERSION_HERE/dist/ext/lz-intervals-enrichment.min.js" type="application/javascript"></script>
     * ```
     *
     * To use with ES6 modules, the plugin must be loaded and registered explicitly before use:
     *
     * ```javascript
     * import LocusZoom from 'locuszoom';
     * import IntervalsTrack from 'locuszoom/esm/ext/lz-intervals-track';
     * LocusZoom.use(IntervalsTrack);
     * ```
     *
     * Then use the layouts made available by this extension. (see demos and documentation for guidance)
     * @module
     */

    // Coordinates (start, end) are cached to facilitate rendering
    const XCS = Symbol.for('lzXCS');
    const YCS = Symbol.for('lzYCS');
    const XCE = Symbol.for('lzXCE');
    const YCE = Symbol.for('lzYCE');

    function install(LocusZoom) {
        /**
         * @memberof module:LocusZoom_DataLayers~intervals_enrichment
         */
        const default_layout = {
            start_field: 'start',
            end_field: 'end',
            track_height: 10,
            track_vertical_spacing: 3,
            bounding_box_padding: 2,
            color: '#B8B8B8',
            fill_opacity: 0.5,
            tooltip_positioning: 'vertical',
        };

        const BaseLayer = LocusZoom.DataLayers.get('BaseDataLayer');

        /**
         * Intervals-by-enrichment Data Layer
         *
         * Implements a data layer that groups interval annotations by enrichment value (a fixed y-axis)
         * @alias module:LocusZoom_DataLayers~intervals_enrichment
         * @see {@link module:LocusZoom_DataLayers~BaseDataLayer} for additional layout options
         */
        class LzIntervalsEnrichment extends BaseLayer {
            /**
             * @param {string} [layout.start_field='start'] The field that defines interval start position
             * @param {string} [layout.end_field='end'] The field that defines interval end position
             * @param {number} [layout.track_height=10] The height of each interval rectangle, in px
             * @param {number} [layout.track_vertical_spacing=3]
             * @param {number} [layout.bounding_box_padding=2]
             * @param {string|module:LocusZoom_DataLayers~ScalableParameter[]} [layout.color='#B8B8B8'] The color of each datum rectangle
             * @param {number|module:LocusZoom_DataLayers~ScalableParameter[]} [layout.fill_opacity=0.5] The opacity of
             *   each rectangle. The default is semi-transparent, because low-significance tracks may overlap very closely.
             * @param {string} [layout.tooltip_positioning='vertical']
             */
            constructor(layout) {
                LocusZoom.Layouts.merge(layout, default_layout);
                super(...arguments);
            }

            // Helper function to sum layout values to derive total height for a single interval track
            getTrackHeight() {
                return this.layout.track_height
                    + this.layout.track_vertical_spacing
                    + (2 * this.layout.bounding_box_padding);
            }

            render() {
                // Determine the appropriate layout for tracks. Store the previous categories (y axis ticks) to decide
                //   whether the axis needs to be re-rendered.

                // Apply filters to only render a specified set of points. Hidden fields will still be given space to render, but not shown.
                const track_data = this._applyFilters(this.data);

                const {start_field, end_field, bounding_box_padding, track_height} = this.layout;
                const y_field = this.layout.y_axis.field;
                const y_axis_name = `y${this.layout.y_axis.axis}_scale`;
                const { x_scale, [y_axis_name]: y_scale } = this.parent;

                // Calculate coordinates for each point
                track_data.forEach((item) => {
                    item[XCS] = x_scale(item[start_field]);
                    item[XCE] = x_scale(item[end_field]);
                    item[YCS] = y_scale(item[y_field]) - this.getTrackHeight() / 2 + bounding_box_padding;
                    item[YCE] = item[YCS] + track_height;
                });

                track_data.sort((a, b) => {
                    // Simplistic layout algorithm that adds wide rectangles to the DOM first, so that small rectangles
                    //  in the same space are clickable (SVG element order determines z-index)
                    const aspan = a[XCE] - a[XCS];
                    const bspan = b[XCE] - b[XCS];
                    return bspan - aspan;
                });

                const selection = this.svg.group.selectAll('rect')
                    .data(track_data);

                selection.enter()
                    .append('rect')
                    .merge(selection)
                    .attr('id', (d) => this.getElementId(d))
                    .attr('x', (d) => d[XCS])
                    .attr('y', (d) => d[YCS])
                    .attr('width', (d) => d[XCE] - d[XCS])
                    .attr('height', this.layout.track_height)
                    .attr('fill', (d, i) => this.resolveScalableParameter(this.layout.color, d, i))
                    .attr('fill-opacity', (d, i) => this.resolveScalableParameter(this.layout.fill_opacity, d, i));

                selection.exit()
                    .remove();

                this.svg.group
                    .call(this.applyBehaviors.bind(this));
            }

            _getTooltipPosition(tooltip) {
                return {
                    x_min: tooltip.data[XCS],
                    x_max: tooltip.data[XCE],
                    y_min: tooltip.data[YCS],
                    y_max: tooltip.data[YCE],
                };
            }
        }

        /**
         * (**extension**) A basic tooltip with information to be shown over an intervals-by-enrichment datum
         * @alias module:LocusZoom_Layouts~intervals_enrichment_tooltip
         * @type tooltip
         * @see {@link module:ext/lz-intervals-enrichment} for required extension and installation instructions
         */
        const intervals_tooltip_layout = {
            namespace: { 'intervals': 'intervals' },
            closable: true,
            show: { or: ['highlighted', 'selected'] },
            hide: { and: ['unhighlighted', 'unselected'] },
            html: `<b>Tissue</b>: {{intervals:tissueId|htmlescape}}<br>
                   <b>Range</b>: {{intervals:chromosome|htmlescape}}: {{intervals:start|htmlescape}}-{{intervals:end|htmlescape}}<br>
                   <b>-log<sub>10</sub> p</b>: {{intervals:pValue|neglog10|scinotation|htmlescape}}<br>
                   <b>Enrichment (n-fold)</b>: {{intervals:fold|scinotation|htmlescape}}`,
        };

        /**
         * (**extension**) A data layer with some preconfigured options for intervals-by-enrichment display, in
         *  which intervals are ranked by priority from enrichment analysis.
         *
         * @alias module:LocusZoom_Layouts~intervals_enrichment_data_layer
         * @type data_layer
         * @see {@link module:ext/lz-intervals-enrichment} for required extension and installation instructions
         */
        const intervals_layer_layout = {
            id: 'intervals_enrichment',
            type: 'intervals_enrichment',
            tag: 'intervals_enrichment',
            namespace: { 'intervals': 'intervals' },
            match: { send: 'intervals:tissueId' },
            id_field: 'intervals:start', // not a good ID field for overlapping intervals
            start_field: 'intervals:start',
            end_field: 'intervals:end',
            filters: [
                { field: 'intervals:ancestry', operator: '=', value: 'EU' },
                { field: 'intervals:pValue', operator: '<=', value: 0.05 },
                { field: 'intervals:fold', operator: '>', value: 2.0 },
            ],
            y_axis: {
                axis: 1,
                field: 'intervals:fold', // is this used for other than extent generation?
                floor: 0,
                upper_buffer: 0.10,
                min_extent: [0, 10],
            },
            fill_opacity: 0.5, // Many intervals overlap: show all, even if the ones below can't be clicked
            color: [
                {
                    field: 'intervals:tissueId',
                    scale_function: 'stable_choice',
                    parameters: {
                        values: ['#1f77b4', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', '#d62728', '#ff9896', '#9467bd', '#c5b0d5', '#8c564b', '#c49c94', '#e377c2', '#f7b6d2', '#7f7f7f', '#c7c7c7', '#bcbd22', '#dbdb8d', '#17becf', '#9edae5'],
                    },
                },
            ],
            behaviors: {
                onmouseover: [
                    { action: 'set', status: 'highlighted' },
                ],
                onmouseout: [
                    { action: 'unset', status: 'highlighted' },
                ],
                onclick: [
                    { action: 'toggle', status: 'selected', exclusive: true },
                ],
                onshiftclick: [
                    { action: 'toggle', status: 'selected' },
                ],
            },
            tooltip: intervals_tooltip_layout,
        };

        // This is tied to a rather specific demo, so it's not added to the reusable registry
        // Highlights areas of a scatter plot that match the HuGeAMP-provided enrichment analysis data
        // Relies on matching behavior/ interaction (not visible initially)
        const intervals_highlight_layout = {
            id: 'interval_matches',
            type: 'highlight_regions',
            namespace: { intervals: 'intervals' },
            match: { receive: 'intervals:tissueId' },
            start_field: 'intervals:start',
            end_field: 'intervals:end',
            merge_field: 'intervals:tissueId',
            filters: [
                { field: 'lz_is_match', operator: '=', value: true },
                { field: 'intervals:ancestry', operator: '=', value: 'EU' },
                { field: 'intervals:pValue', operator: '<=', value: 0.05 },
                { field: 'intervals:fold', operator: '>', value: 2.0 },
            ],
            color: [{
                field: 'intervals:tissueId',
                scale_function: 'stable_choice',
                parameters: {
                    values: ['#1f77b4', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', '#d62728', '#ff9896', '#9467bd', '#c5b0d5', '#8c564b', '#c49c94', '#e377c2', '#f7b6d2', '#7f7f7f', '#c7c7c7', '#bcbd22', '#dbdb8d', '#17becf', '#9edae5'],
                },
            }],
            fill_opacity: 0.1,
        };

        /**
         * (**extension**) A panel containing an intervals-by-enrichment data layer
         * @alias module:LocusZoom_Layouts~intervals_enrichment_panel
         * @type panel
         * @see {@link module:ext/lz-intervals-enrichment} for required extension and installation instructions
         */
        const intervals_panel_layout = {
            id: 'intervals_enrichment',
            tag: 'intervals_enrichment',
            min_height: 250,
            height: 250,
            margin: { top: 35, right: 50, bottom: 40, left: 70 },
            inner_border: 'rgb(210, 210, 210)',
            axes: {
                x: {
                    label: 'Chromosome {{chr}} (Mb)',
                    label_offset: 34,
                    tick_format: 'region',
                    extent: 'state',
                },
                y1: {
                    label: 'enrichment (n-fold)',
                    label_offset: 40,
                },
            },
            interaction: {
                drag_background_to_pan: true,
                drag_x_ticks_to_scale: true,
                drag_y1_ticks_to_scale: true,
                scroll_to_zoom: true,
                x_linked: true,
            },
            data_layers: [intervals_layer_layout],
        };

        /**
         * (**extension**) A plot layout that shows association summary statistics, genes, and intervals-by-enrichment data.
         *  This layout provides interactive matching: clicking an interval marking causes area of the scatter plot to be
         *  highlighted for any annotations that match the specified category.
         *  It is intended to work with data in the HuGeAMP format.
         * @alias module:LocusZoom_Layouts~intervals_association_enrichment
         * @type plot
         * @see {@link module:ext/lz-intervals-enrichment} for required extension and installation instructions
         */
        const intervals_plot_layout = {
            state: {},
            width: 800,
            responsive_resize: true,
            min_region_scale: 20000,
            max_region_scale: 1000000,
            toolbar: LocusZoom.Layouts.get('toolbar', 'standard_association'),
            panels: [
                function () {
                    const base = LocusZoom.Layouts.get('panel', 'association');
                    base.data_layers.unshift(intervals_highlight_layout);
                    return base;
                }(),
                intervals_panel_layout,
                LocusZoom.Layouts.get('panel', 'genes'),
            ],
        };

        LocusZoom.DataLayers.add('intervals_enrichment', LzIntervalsEnrichment);

        LocusZoom.Layouts.add('tooltip', 'intervals_enrichment', intervals_tooltip_layout);
        LocusZoom.Layouts.add('data_layer', 'intervals_enrichment', intervals_layer_layout);
        LocusZoom.Layouts.add('panel', 'intervals_enrichment', intervals_panel_layout);
        LocusZoom.Layouts.add('plot', 'intervals_association_enrichment', intervals_plot_layout);
    }

    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;