Revision 58d6304a8688ac75ce1144d67ee25fc6669e60ed authored by Alexander Veit on 27 May 2022, 19:52:17 UTC, committed by Alexander Veit on 27 May 2022, 19:52:17 UTC
HorizontalLine1DPixiTrack.js
import { format } from 'd3-format';
import { scaleLinear } from 'd3-scale';
import HorizontalTiled1DPixiTrack from './HorizontalTiled1DPixiTrack';
import { colorToHex } from './utils';
class HorizontalLine1DPixiTrack extends HorizontalTiled1DPixiTrack {
stopHover() {
this.pMouseOver.clear();
this.animate();
}
getMouseOverHtml(trackX) {
// if we're not supposed to show the tooltip, don't show it
// we return here so that the mark isn't drawn in the code
// below
if (!this.tilesetInfo || !this.options.showTooltip || !this.valueScale)
return '';
const value = this.getDataAtPos(trackX);
let textValue = '';
if (value) textValue = format('.3f')(value);
const graphics = this.pMouseOver;
const colorHex = 0;
const yPos = this.valueScale(value);
graphics.clear();
graphics.beginFill(colorHex, 0.5);
graphics.lineStyle(1, colorHex, 1);
const markerWidth = 4;
graphics.drawRect(
trackX - markerWidth / 2,
yPos - markerWidth / 2,
markerWidth,
markerWidth,
);
this.animate();
return `${textValue}`;
}
/**
* Create whatever is needed to draw this tile.
*/
initTile(tile) {
super.initTile(tile);
if (!tile.tileData || !tile.tileData.dense) {
console.warn('emptyTile:', tile);
return;
}
tile.xValues = new Array(tile.tileData.dense.length);
tile.yValues = new Array(tile.tileData.dense.length);
if (this.isValueScaleLocked()) {
// If valueScales are locked get min and max values of the locked group
// for initialization. This prevents a flickering that is caused by
// rendering the track multiple times with possibly different valueScales
const glge = this.getLockGroupExtrema();
if (glge !== null) {
this.minValue(glge[0]);
this.maxValue(glge[1]);
}
}
this.drawTile(tile);
}
rerender(options, force) {
super.rerender(options, force);
this.options = options;
super.draw();
this.visibleAndFetchedTiles().forEach((tile) => {
this.renderTile(tile);
});
}
renderTile(tile) {
// this function is just so that we follow the same pattern as
// HeatmapTiledPixiTrack.js
this.drawTile(tile);
this.drawAxis(this.valueScale);
}
drawTile(tile) {
super.drawTile(tile);
if (!tile.graphics) {
return;
}
if (!tile.tileData || !tile.tileData.dense) {
return;
}
const graphics = tile.graphics;
const { tileX, tileWidth } = this.getTilePosAndDimensions(
tile.tileData.zoomLevel,
tile.tileData.tilePos,
);
const tileValues = tile.tileData.dense;
if (tileValues.length === 0) {
return;
}
const [vs, offsetValue] = this.makeValueScale(
this.minValue(),
this.medianVisibleValue,
this.maxValue(),
);
this.valueScale = vs;
graphics.clear();
this.drawAxis(this.valueScale);
if (
this.options.valueScaling === 'log' &&
this.valueScale.domain()[1] < 0
) {
console.warn(
'Negative values present when using a log scale',
this.valueScale.domain(),
);
return;
}
const stroke = colorToHex(
this.options.lineStrokeColor ? this.options.lineStrokeColor : 'blue',
);
// this scale should go from an index in the data array to
// a position in the genome coordinates
if (!this.tilesetInfo.tile_size && !this.tilesetInfo.bins_per_dimension) {
console.warn(
'No tileset_info.tile_size or tileset_info.bins_per_dimension',
this.tilesetInfo,
);
}
const tileSize =
this.tilesetInfo.tile_size || this.tilesetInfo.bins_per_dimension;
const tileXScale = scaleLinear()
.domain([0, tileSize])
.range([tileX, tileX + tileWidth]);
const strokeWidth = this.options.lineStrokeWidth
? this.options.lineStrokeWidth
: 1;
graphics.lineStyle(strokeWidth, stroke, 1);
tile.segments = [];
let currentSegment = [];
for (let i = 0; i < tileValues.length; i++) {
const xPos = this._xScale(tileXScale(i));
const yPos = this.valueScale(tileValues[i] + offsetValue);
if (
(this.options.valueScaling === 'log' && tileValues[i] === 0) ||
Number.isNaN(yPos)
) {
if (currentSegment.length > 1) {
tile.segments.push(currentSegment);
}
// Just ignore 1-element segments.
currentSegment = [];
continue;
}
if (tileXScale(i) > this.tilesetInfo.max_pos[0]) {
// Data is in the last tile and extends beyond the coordinate system.
break;
}
currentSegment.push([xPos, yPos]);
}
if (currentSegment.length > 1) {
tile.segments.push(currentSegment);
}
for (const segment of tile.segments) {
const first = segment[0];
const rest = segment.slice(1);
graphics.moveTo(first[0], first[1]);
for (const point of rest) {
graphics.lineTo(point[0], point[1]);
}
}
}
setPosition(newPosition) {
super.setPosition(newPosition);
this.pMain.position.y = this.position[1];
this.pMain.position.x = this.position[0];
this.pMouseOver.position.y = this.position[1];
this.pMouseOver.position.x = this.position[0];
}
zoomed(newXScale, newYScale) {
this.xScale(newXScale);
this.yScale(newYScale);
this.refreshTiles();
this.draw();
const isValueScaleLocked = this.isValueScaleLocked();
if (
this.continuousScaling &&
this.minValue() !== undefined &&
this.maxValue() !== undefined
) {
if (
this.valueScaleMin === null &&
this.valueScaleMax === null &&
!isValueScaleLocked
) {
const newMin = this.minVisibleValue();
const newMax = this.maxVisibleValue();
const epsilon = 1e-6;
if (
newMin !== null &&
newMax !== null &&
(Math.abs(this.minValue() - newMin) > epsilon ||
Math.abs(this.maxValue() - newMax) > epsilon)
) {
this.minValue(newMin);
this.maxValue(newMax);
this.scheduleRerender();
}
}
if (isValueScaleLocked) {
this.onValueScaleChanged();
}
}
}
superSVG() {
/*
* Bypass this track's exportSVG and call its parent's directly.
*/
return super.exportSVG();
}
/**
* Export an SVG representation of this track
*
* @returns {Array} The two returned DOM nodes are both SVG
* elements [base,track]. Base is a parent which contains track as a
* child. Track is clipped with a clipping rectangle contained in base.
*
*/
exportSVG() {
let track = null;
let base = null;
if (super.exportSVG) {
[base, track] = super.exportSVG();
} else {
base = document.createElement('g');
track = base;
}
base.setAttribute('class', 'exported-line-track');
const output = document.createElement('g');
track.appendChild(output);
output.setAttribute(
'transform',
`translate(${this.position[0]},${this.position[1]})`,
);
const stroke = this.options.lineStrokeColor
? this.options.lineStrokeColor
: 'blue';
this.visibleAndFetchedTiles().forEach((tile) => {
const g = document.createElement('path');
g.setAttribute('fill', 'transparent');
g.setAttribute('stroke', stroke);
let d = '';
for (const segment of tile.segments) {
const first = segment[0];
const rest = segment.slice(1);
d += `M${first[0]} ${first[1]}`;
for (const point of rest) {
d += `L${point[0]} ${point[1]}`;
}
}
g.setAttribute('d', d);
output.appendChild(g);
});
const gAxis = document.createElement('g');
gAxis.setAttribute('id', 'axis');
// append the axis to base so that it's not clipped
base.appendChild(gAxis);
gAxis.setAttribute(
'transform',
`translate(${this.axis.pAxis.position.x}, ${this.axis.pAxis.position.y})`,
);
// add the axis to the export
if (
this.options.axisPositionHorizontal === 'left' ||
this.options.axisPositionVertical === 'top'
) {
// left axis are shown at the beginning of the plot
const gDrawnAxis = this.axis.exportAxisLeftSVG(
this.valueScale,
this.dimensions[1],
);
gAxis.appendChild(gDrawnAxis);
} else if (
this.options.axisPositionHorizontal === 'right' ||
this.options.axisPositionVertical === 'bottom'
) {
const gDrawnAxis = this.axis.exportAxisRightSVG(
this.valueScale,
this.dimensions[1],
);
gAxis.appendChild(gDrawnAxis);
}
return [base, track];
}
tileToLocalId(tile) {
if (
this.options.aggregationMode &&
this.options.aggregationMode !== 'mean'
) {
return `${tile.join('.')}.${this.options.aggregationMode}`;
}
return `${tile.join('.')}`;
}
tileToRemoteId(tile) {
return this.tileToLocalId(tile);
}
}
export default HorizontalLine1DPixiTrack;
Computing file changes ...