import * as d3 from 'd3';
import BaseDataLayer from './base';
import {merge} from '../../helpers/layouts';
import {applyStyles} from '../../helpers/common';
/**
* @memberof module:LocusZoom_DataLayers~arcs
*/
const default_layout = {
color: 'seagreen',
hitarea_width: '10px',
style: {
fill: 'none',
'stroke-width': '1px',
'stroke-opacity': '100%',
},
tooltip_positioning: 'top',
};
/**
* Arc Data Layer
* Implements a data layer that will render chromatin accessibility tracks.
* This layer draws arcs (one per datapoint) that connect two endpoints (x.field1 and x.field2) by means of an arc,
* with a height determined by y.field.
* @alias module:LocusZoom_DataLayers~arcs
* @see {@link module:LocusZoom_DataLayers~BaseDataLayer} for additional layout options
*/
class Arcs extends BaseDataLayer {
/**
* @param {String|module:LocusZoom_DataLayers~ScalableParameter[]} [layout.color='seagreen'] Specify how to choose the stroke color for each arc
* @param {number} [layout.hitarea_width='10px'] The width (in pixels) of hitareas. Arcs are only as wide as the stroke,
* so a hit area of 5px on each side can make it much easier to select an item for a tooltip.
* @param {string} [layout.style.fill='none'] The fill color under the area of the arc
* @param {string} [layout.style.stroke-width='1px']
* @param {string} [layout.style.stroke_opacity='100%']
* @param {'horizontal'|'vertical'|'top'|'bottom'|'left'|'right'} [layout.tooltip_positioning='top'] Where to draw the tooltip relative to the datum.
* @param {string} [layout.x_axis.field1] The field to use for one end of the arc; creates a point at (x1, 0)
* @param {string} [layout.x_axis.field2] The field to use for the other end of the arc; creates a point at (x2, 0)
* @param {string} [layout.y_axis.field] The height at the midpoint of the arc, (xmid, y)
*/
constructor(layout) {
layout = merge(layout, default_layout);
super(...arguments);
}
// Implement the main render function
render() {
const self = this;
const layout = self.layout;
const x_scale = self.parent['x_scale'];
const y_scale = self.parent[`y${layout.y_axis.axis}_scale`];
// Apply filters to only render a specified set of points
const track_data = this._applyFilters();
// Helper: Each individual data point describes a path composed of 3 points, with a spline to smooth the line
function _make_line(d) {
const x1 = d[layout.x_axis.field1];
const x2 = d[layout.x_axis.field2];
const xmid = (x1 + x2) / 2;
const coords = [
[x_scale(x1), y_scale(0)],
[x_scale(xmid), y_scale(d[layout.y_axis.field])],
[x_scale(x2), y_scale(0)],
];
// Smoothing options: https://bl.ocks.org/emmasaunders/f7178ed715a601c5b2c458a2c7093f78
const line = d3.line()
.x((d) => d[0])
.y((d) => d[1])
.curve(d3.curveNatural);
return line(coords);
}
// Draw real lines, and also invisible hitareas for easier mouse events
const hitareas = this.svg.group
.selectAll('path.lz-data_layer-arcs-hitarea')
.data(track_data, (d) => this.getElementId(d));
const selection = this.svg.group
.selectAll('path.lz-data_layer-arcs')
.data(track_data, (d) => this.getElementId(d));
this.svg.group
.call(applyStyles, layout.style);
hitareas
.enter()
.append('path')
.attr('class', 'lz-data_layer-arcs-hitarea')
.merge(hitareas)
.attr('id', (d) => this.getElementId(d))
.style('fill', 'none')
.style('stroke-width', layout.hitarea_width)
.style('stroke-opacity', 0)
.style('stroke', 'transparent')
.attr('d', (d) => _make_line(d));
// Add new points as necessary
selection
.enter()
.append('path')
.attr('class', 'lz-data_layer-arcs')
.merge(selection)
.attr('id', (d) => this.getElementId(d))
.attr('stroke', (d, i) => this.resolveScalableParameter(this.layout.color, d, i))
.attr('d', (d, i) => _make_line(d));
// Remove old elements as needed
selection.exit()
.remove();
hitareas.exit()
.remove();
// Apply mouse behaviors to arcs
this.svg.group
.call(this.applyBehaviors.bind(this));
return this;
}
_getTooltipPosition(tooltip) {
// Center the tooltip arrow at the apex of the arc. Sometimes, only part of an arc shows on the screen, so we
// clean up these values to ensure that the tooltip will appear within the window.
const panel = this.parent;
const layout = this.layout;
const x1 = tooltip.data[layout.x_axis.field1];
const x2 = tooltip.data[layout.x_axis.field2];
const y_scale = panel[`y${layout.y_axis.axis}_scale`];
return {
x_min: panel.x_scale(Math.min(x1, x2)),
x_max: panel.x_scale(Math.max(x1, x2)),
y_min: y_scale(tooltip.data[layout.y_axis.field]),
y_max: y_scale(0),
};
}
}
export {Arcs as default};