Source: components/data_layer/annotation_track.js

import BaseDataLayer from './base';
import {merge} from '../../helpers/layouts';

/**
 * @memberof module:LocusZoom_DataLayers~annotation_track
 */
const default_layout = {
    color: '#000000',
    filters: null,
    tooltip_positioning: 'vertical',
    hitarea_width: 8,
};

/**
 * Create a single continuous 2D track that provides information about each datapoint
 *
 * For example, this can be used to mark items by membership in a group, alongside information in other panels
 * @alias module:LocusZoom_DataLayers~annotation_track
 * @see {@link module:LocusZoom_DataLayers~BaseDataLayer} for additional layout options
 */
class AnnotationTrack extends BaseDataLayer {
    /**
     * @param {String|module:LocusZoom_DataLayers~ScalableParameter[]} [layout.color] Specify how to choose the fill color for each tick mark
     * @param {number} [layout.hitarea_width=8] The width (in pixels) of hitareas. Annotation marks are typically 1 px wide,
     *   so a hit area of 4px on each side can make it much easier to select an item for a tooltip. Hitareas will not interfere
     *   with selecting adjacent points.
     * @param {'horizontal'|'vertical'|'top'|'bottom'|'left'|'right'} [layout.tooltip_positioning='vertical'] Where to draw the tooltip relative to the datum.
     */
    constructor(layout) {
        if (!Array.isArray(layout.filters)) {
            throw new Error('Annotation track must specify array of filters for selecting points to annotate');
        }
        merge(layout, default_layout);
        super(...arguments);
    }

    initialize() {
        super.initialize();
        this._hitareas_group = this.svg.group.append('g')
            .attr('class', `lz-data_layer-${this.layout.type}-hit_areas`);

        this._visible_lines_group = this.svg.group.append('g')
            .attr('class', `lz-data_layer-${this.layout.type}-visible_lines`);
    }

    render() {
        // Apply filters to only render a specified set of points
        const track_data = this._applyFilters();

        const hit_areas_selection = this._hitareas_group.selectAll(`rect.lz-data_layer-${this.layout.type}`)
            .data(track_data, (d) => d[this.layout.id_field]);


        const _getX = (d, i) => {
            // Helper for hitarea position calcs: ensures that a hitarea never overlaps the space allocated
            // for a real data element. Helps to avoid mouse jitter when selecting tooltips in crowded areas.
            const x_center = this.parent['x_scale'](d[this.layout.x_axis.field]);
            let x_left = x_center - this.layout.hitarea_width / 2;
            if (i >= 1) {
                // This assumes that the data are in sorted order.
                const left_node = track_data[i - 1];
                const left_node_x_center = this.parent['x_scale'](left_node[this.layout.x_axis.field]);
                x_left = Math.max(x_left, (x_center + left_node_x_center) / 2);
            }
            return [x_left, x_center];
        };

        // Draw hitareas under real data elements, so that real data elements always take precedence
        hit_areas_selection.enter()
            .append('rect')
            .attr('class', `lz-data_layer-${this.layout.type}`)
            // Update the set of elements to reflect new data
            .merge(hit_areas_selection)
            .attr('id', (d) => this.getElementId(d))
            .attr('height', this.parent.layout.height)
            .attr('opacity', 0)
            .attr('x', (d, i) => {
                const crds = _getX(d, i);
                return crds[0];
            })
            .attr('width', (d, i) => {
                const crds = _getX(d, i);
                return (crds[1] - crds[0]) + this.layout.hitarea_width / 2;
            });

        const width = 1;
        const selection = this._visible_lines_group.selectAll(`rect.lz-data_layer-${this.layout.type}`)
            .data(track_data, (d) => d[this.layout.id_field]);
        // Draw rectangles (visual and tooltip positioning)
        selection.enter()
            .append('rect')
            .attr('class', `lz-data_layer-${this.layout.type}`)
            .merge(selection)
            .attr('id', (d) => this.getElementId(d))
            .attr('x', (d) => this.parent['x_scale'](d[this.layout.x_axis.field]) - width / 2)
            .attr('width', width)
            .attr('height', this.parent.layout.height)
            .attr('fill', (d, i) => this.resolveScalableParameter(this.layout.color, d, i));

        // Remove unused elements
        selection.exit()
            .remove();

        // Set up tooltips and mouse interaction
        this.svg.group
            .call(this.applyBehaviors.bind(this));

        // Remove unused elements
        hit_areas_selection.exit()
            .remove();
    }

    /**
     * Render tooltip at the center of each tick mark
     * @param tooltip
     * @return {{y_min: number, x_max: *, y_max: *, x_min: number}}
     * @private
     */
    _getTooltipPosition(tooltip) {
        const panel = this.parent;
        const data_layer_height = panel.layout.height - (panel.layout.margin.top + panel.layout.margin.bottom);
        const stroke_width = 1; // as defined in the default stylesheet

        const x_center = panel.x_scale(tooltip.data[this.layout.x_axis.field]);
        const y_center = data_layer_height / 2;
        return {
            x_min: x_center - stroke_width,
            x_max: x_center + stroke_width,
            y_min: y_center - panel.layout.margin.top,
            y_max: y_center + panel.layout.margin.bottom,
        };
    }
}

export {AnnotationTrack as default};