/**
* 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;