client/client/modules/render/tracks/vcf/internal/renderer/sample/index.js (694 lines of code) (raw):
import * as PIXI from 'pixi.js-legacy';
import {CachedTrackRenderer} from '../../../../../core';
import {ColorProcessor, NumberFormatter} from '../../../../../utilities';
import findPositionForLabel from './utilities/find-position-for-label';
import splitChromosomeByDeletions from './utilities/split-chromosome-by-deletions';
function generateCorrectXFn (viewport) {
return x => Math.max(
-viewport.canvasSize,
Math.min(
2 * viewport.canvasSize,
x
)
);
}
class VCFSampleRenderer extends CachedTrackRenderer {
get height() {
return this._height;
}
set height(value) {
this._height = value;
}
constructor(config, track) {
super(track);
this._config = config;
this._colors = track && track.config && track.config.colors
? track.config.colors
: {};
this.chromosomeGraphics = new PIXI.Graphics();
this.graphics = new PIXI.Graphics();
this.lettersContainer = new PIXI.Container();
this.lettersBackground = new PIXI.Graphics();
this.hoveredGraphics = new PIXI.Graphics();
this.dataContainer.addChild(this.chromosomeGraphics);
this.dataContainer.addChild(this.graphics);
this.dataContainer.addChild(this.hoveredGraphics);
this.dataContainer.addChild(this.lettersBackground);
this.dataContainer.addChild(this.lettersContainer);
this.freeLabelsZones = [];
this.bubbles = [];
this.visualInfo = [];
this.hoveredItem = undefined;
this.initializeCentralLine();
}
renderDeletion (deletion, options = {}) {
const {
viewport = this._track ? this._track.viewport : undefined,
y1,
y2,
graphics = this.graphics,
hovered = false
} = options;
if (!viewport) {
return;
}
const {
startIndex,
endIndex,
highlightColor
} = deletion;
const color = this._colors.del || 0x000000;
const correctX = generateCorrectXFn(viewport);
const x1 = Math.floor(correctX(viewport.project.brushBP2pixel(startIndex - 0.5)));
const x2 = Math.ceil(correctX(viewport.project.brushBP2pixel(endIndex + 0.5)));
if (highlightColor) {
const height = Math.floor((y2 - y1) * 0.5);
graphics
.beginFill(highlightColor, 1)
.lineStyle(0, 0, 0)
.drawRect(
Math.min(x1, x2),
Math.floor((y1 + y2) / 2.0 - height / 2.0),
Math.abs(x1 - x2),
height
)
.endFill();
}
graphics
.lineStyle(hovered ? 2 : 1, color, 1)
.moveTo(x1, y1)
.lineTo(x1, y2)
.moveTo(x1, (y1 + y2) / 2.0)
.lineTo(x2, (y1 + y2) / 2.0)
.moveTo(x2, y1)
.lineTo(x2, y2);
return {
variant: deletion,
position: {
x1, x2
}
};
}
renderInsertion (insertion, options) {
const {
viewport = this._track ? this._track.viewport : undefined,
y1,
y2,
graphics = this.graphics,
hovered = false
} = options;
if (!viewport) {
return;
}
const {
startIndex,
highlightColor
} = insertion;
const color = this._colors.ins || 0x000000;
const correctX = generateCorrectXFn(viewport);
const x = Math.floor(correctX(viewport.project.brushBP2pixel(startIndex - 0.5)));
const offset = Math.max(viewport.factor / 5.0, 3);
const x1 = Math.floor(x - offset);
const x2 = Math.ceil(x + offset);
if (highlightColor) {
const width = offset;
graphics
.beginFill(highlightColor, 1)
.lineStyle(0, 0, 0)
.drawRect(
Math.floor(x - width / 2.0),
Math.floor(y1 + 2),
width,
(y2 - y1 - 4)
)
.endFill();
}
graphics
.lineStyle(hovered ? 2 : 1, color, 1)
.moveTo(x, Math.round(y1 + 1))
.lineTo(x, Math.round(y2 - 1))
.moveTo(x1, Math.round(y1 + 1))
.lineTo(x2, Math.round(y1 + 1))
.moveTo(x1, Math.round(y2 - 1))
.lineTo(x2, Math.round(y2 - 1));
return {
variant: insertion,
position: {
x1,
x2
}
};
}
renderSNV (snv, options) {
const {
viewport = this._track ? this._track.viewport : undefined,
y1,
y2,
graphics = this.graphics,
hovered = false,
renderLabels = true
} = options;
if (!viewport) {
return;
}
const {
alternativeAlleles = [],
startIndex,
highlightColor
} = snv;
if (alternativeAlleles.length > 0) {
const renderNucleotides = renderLabels &&
viewport.factor > this._config.collapsed.nucleotide.threshold;
const correctX = generateCorrectXFn(viewport);
const [allele] = alternativeAlleles;
const letters = allele.split();
for (let l = 0; l < letters.length; l += 1) {
const letter = letters[l];
const color = this._colors[letter];
if (color) {
const position = startIndex + l;
const x1 = Math.floor(correctX(viewport.project.brushBP2pixel(position - 0.5)));
const x2 = Math.ceil(correctX(viewport.project.brushBP2pixel(position + 0.5)));
if (highlightColor) {
graphics.lineStyle(1, highlightColor, 1);
} else {
graphics.lineStyle(0, 0, 0);
}
graphics
.beginFill(
hovered ? ColorProcessor.darkenColor(color) : color,
1
)
.drawRect(
x1,
y1,
x2 - x1,
y2 - y1
)
.endFill();
if (renderNucleotides && this.labelsManager) {
const nucleotide = this.labelsManager.getLabel(
letter,
this._config.collapsed.nucleotide.font
);
if (nucleotide) {
nucleotide.x = (x1 + x2) / 2.0 - nucleotide.width / 2.0;
nucleotide.y = (y1 + y2) / 2.0 - nucleotide.height / 2.0;
this.lettersContainer.addChild(nucleotide);
}
}
}
}
return {
variant: snv,
position: {
x1: Math.floor(
correctX(
viewport.project.brushBP2pixel(
startIndex - 0.5
)
)
),
x2: Math.floor(
correctX(
viewport.project.brushBP2pixel(startIndex + letters.length - 0.5)
)
)
}
};
}
return undefined;
}
renderInversion (inversion, options = {}) {
const {
viewport = this._track ? this._track.viewport : undefined,
y1,
y2,
graphics = this.graphics,
hovered = false
} = options;
if (!viewport) {
return;
}
const {
startIndex,
endIndex,
interChromosome,
highlightColor
} = inversion;
if (!interChromosome) {
const color = this._colors.ins || 0x000000;
const correctX = generateCorrectXFn(viewport);
const x1 = Math.floor(correctX(viewport.project.brushBP2pixel(startIndex - 0.5)));
const x2 = Math.ceil(correctX(viewport.project.brushBP2pixel(endIndex + 0.5)));
const x1ArrowDirection = Math.sign(x2 - x1);
const x2ArrowDirection = -x1ArrowDirection;
const yCenter = Math.round((y1 + y2) / 2.0);
const arrowSize = Math.max(
2,
Math.abs(y2 - y1) / 2.0 - 1
);
if (highlightColor) {
const height = Math.floor((y2 - y1) * 0.5);
graphics
.beginFill(highlightColor, 1)
.lineStyle(0, 0, 0)
.drawRect(
Math.min(x1, x2),
Math.floor((y1 + y2) / 2.0 - height / 2.0),
Math.abs(x1 - x2),
height
)
.endFill();
}
graphics
.lineStyle(hovered ? 2 : 1, color, 1)
.moveTo(x1, yCenter)
.lineTo(
Math.round(x1 + x1ArrowDirection * arrowSize),
Math.round(yCenter - arrowSize)
)
.moveTo(x1, yCenter)
.lineTo(
Math.round(x1 + x1ArrowDirection * arrowSize),
Math.round(yCenter + arrowSize)
)
.moveTo(x1, yCenter)
.lineTo(x2, yCenter)
.moveTo(x2, yCenter)
.lineTo(
Math.round(x2 + x2ArrowDirection * arrowSize),
Math.round(yCenter - arrowSize)
)
.moveTo(x2, yCenter)
.lineTo(
Math.round(x2 + x2ArrowDirection * arrowSize),
Math.round(yCenter + arrowSize)
);
return {
variant: inversion,
position: {
x1,
x2
}
};
}
return this.renderStructural(inversion, options);
}
renderStructural (variation, options) {
const {
viewport = this._track ? this._track.viewport : undefined,
y1,
y2,
graphics = this.graphics,
hovered = false,
renderLabels = true
} = options;
if (!viewport) {
return;
}
const config = this._config;
const {
startIndex,
interChromosome,
alternativeAllelesInfo = [],
highlightColor
} = variation;
const {
mate
} = alternativeAllelesInfo[0] || {};
let direction = 1;
let mateInfo;
let endPosition;
let visualInfo;
if (interChromosome && mate) {
const {
attachedAt,
chromosome,
position
} = mate;
direction = /^left$/i.test(attachedAt) ? -1 : 1;
if (chromosome && position) {
mateInfo = `${chromosome} ${NumberFormatter.formattedText(position)}`;
if (direction > 0) {
mateInfo = '\u2192'.concat(' ', mateInfo);
} else {
mateInfo = mateInfo.concat(' ', '\u2190');
}
}
} else if (!interChromosome && mate) {
const {
attachedAt,
position
} = mate;
direction = /^left$/i.test(attachedAt) ? -1 : 1;
if (position && position !== startIndex) {
direction = Math.sign(position - startIndex);
endPosition = position;
}
}
const color = 0x000000;
const correctX = generateCorrectXFn(viewport);
const x = Math.floor(correctX(viewport.project.brushBP2pixel(startIndex - direction * 0.5)));
graphics
.lineStyle(hovered ? 2 : 1, color, 1)
.moveTo(x, y1)
.lineTo(x, y2);
if (renderLabels) {
let labelText = variation.type;
if (mateInfo) {
if (direction > 0) {
labelText = labelText.concat(' ', mateInfo);
} else {
labelText = mateInfo.concat(' ', labelText);
}
}
const label = this.labelsManager.getLabel(
labelText,
config.collapsed.variation.label.font
);
const initialPosition = x +
(
label.width / 2.0 + config.collapsed.variation.label.margin + 1
) * direction -
label.width / 2.0;
const bestPosition = findPositionForLabel(
this.freeLabelsZones,
{
x: initialPosition,
width: label.width
}
);
if (bestPosition) {
label.y = Math.floor((y1 + y2) / 2.0 - label.height / 2.0);
label.x = Math.round(bestPosition);
this.lettersContainer.addChild(label);
this.lettersBackground
.lineStyle(0, 0xffffff, 0)
.beginFill(
highlightColor || (
this._colors.base !== undefined ? this._colors.base : 0xffffff
),
highlightColor ? 1 : 0.8
)
.drawRect(
label.x - config.collapsed.variation.label.margin,
label.y,
label.width + 2.0 * config.collapsed.variation.label.margin,
label.height
)
.endFill();
visualInfo = {
x1: label.x,
x2: label.x + label.width
};
if (bestPosition !== initialPosition) {
const {
radius = 1,
color: stroke = color
} = config.collapsed.variation.cutout || {};
const xx = Math.round(
bestPosition -
(label.width / 2.0 + radius + 1) * direction +
label.width / 2.0
);
this.lettersBackground
.lineStyle(1, stroke, 1)
.beginFill(stroke, 1)
.drawCircle(
xx,
Math.round((y1 + y2) / 2.0),
radius
)
.endFill()
.moveTo(x, y2 - radius)
.lineTo(xx, y2 - radius)
.lineTo(xx, Math.round((y1 + y2) / 2.0));
}
}
}
if (!mateInfo && endPosition) {
const fromX = x;
const toX = Math.round(correctX(viewport.project.brushBP2pixel(endPosition)));
if (highlightColor) {
graphics
.beginFill(highlightColor, 1)
.lineStyle(0, 0, 0)
.drawRect(
Math.min(fromX, toX),
Math.floor((y1 + y2) / 2.0 - config.collapsed.variation.highlight.lineHeight / 2.0),
Math.abs(fromX - toX),
config.collapsed.variation.highlight.lineHeight
)
.endFill();
}
graphics
.lineStyle(hovered ? 2 : 1, color, 1)
.moveTo(toX, y1)
.lineTo(toX, y2)
.lineStyle(hovered ? 2 : 1, config.collapsed.variation.line.stroke, 1)
.moveTo(toX, Math.round((y1 + y2) / 2.0) - 0.5)
.lineTo(fromX, Math.round((y1 + y2) / 2.0) - 0.5);
visualInfo = {
x1: Math.min(fromX, toX),
x2: Math.max(fromX, toX)
};
}
if (visualInfo) {
return {
variant: variation,
position: visualInfo
};
}
return undefined;
}
renderStatistics (variation, options) {
const {
viewport = this._track ? this._track.viewport : undefined,
y1,
y2,
graphics = this.graphics,
hovered = false,
renderLabels = true
} = options;
if (!viewport) {
return;
}
const config = this._config;
const {
startIndex,
endIndex,
highlightColor,
variationsCount
} = variation;
// todo: highlight!
if (renderLabels) {
const label = this.labelsManager.getLabel(`${variationsCount}`, config.collapsed.bubble.font);
const correctX = generateCorrectXFn(viewport);
const initialPosition = Math.floor(
correctX(
viewport.project.brushBP2pixel((startIndex + endIndex) / 2.0) -
label.width / 2.0
)
);
label.x = Math.round(
findPositionForLabel(
this.freeLabelsZones,
{
x: initialPosition,
width: label.width
}
)
);
label.y = Math.floor((y1 + y2) / 2.0 - label.height / 2.0);
const yRadius = (y1 + y2) / 2.0;
const xRadius = Math.max(
label.width / 2.0 + config.collapsed.bubble.padding,
yRadius
);
this.bubbles.push({
variation,
xCenter: label.x + label.width / 2.0,
yCenter: (y1 + y2) / 2.0,
xRadius,
yRadius
});
this.lettersContainer.addChild(label);
}
const [bubble] = this.bubbles.filter(o => o.variation === variation);
if (bubble) {
let {xRadius} = bubble;
const {yRadius} = bubble;
if (hovered) {
xRadius += 1;
}
graphics
.beginFill(config.collapsed.bubble.fill, 1)
.lineStyle(1, config.collapsed.bubble.stroke, 1)
.drawRoundedRect(
Math.floor(bubble.xCenter - xRadius),
Math.ceil(bubble.yCenter - yRadius),
xRadius * 2.0,
yRadius * 2.0,
bubble.yRadius + 1
)
.endFill();
return {
variant: variation,
position: {
x1: Math.floor(bubble.xCenter - xRadius),
x2: Math.ceil(bubble.xCenter + xRadius)
}
};
}
return undefined;
}
renderVariation (variation, options) {
if (variation.isStatistics) {
return this.renderStatistics(variation, options);
}
if (
/^del$/i.test(variation.type) &&
(!variation.structural || !variation.interChromosome)
) {
return this.renderDeletion(variation, options);
}
if (/^ins$/i.test(variation.type)) {
return this.renderInsertion(variation, options);
}
if (/^(snv|snp)$/i.test(variation.type)) {
return this.renderSNV(variation, options);
}
if (/^inv$/i.test(variation.type)) {
return this.renderInversion(variation, options);
}
return this.renderStructural(variation, options);
}
rebuildContainer(viewport, cache) {
super.rebuildContainer(viewport, cache);
this.clear();
const {data: variantsData = {}} = cache || {};
const {variants: data = []} = variantsData;
const config = this._config;
const chromosomeParts = splitChromosomeByDeletions(
data.filter(variation => !variation.isStatistics && /^del$/i.test(variation.type))
);
if (this._colors.base) {
this.chromosomeGraphics
.lineStyle(0, 0xcccccc, 0)
.beginFill(this._colors.base, 1);
} else {
this.chromosomeGraphics
.lineStyle(1, 0xcccccc, 1)
.beginFill(0xffffff, 1);
}
const correctX = generateCorrectXFn(viewport);
const y1 = Math.floor(
config.collapsedSampleHeight / 2.0 - config.collapsed.bar.height / 2.0
);
const y2 = Math.floor(
config.collapsedSampleHeight / 2.0 + config.collapsed.bar.height / 2.0
);
for (const chromosomePart of chromosomeParts) {
const {
startIndex,
endIndex
} = chromosomePart;
const x1 = Math.floor(
correctX(
viewport.project.brushBP2pixel(startIndex - 0.5)
)
);
const x2 = Math.ceil(
correctX(
viewport.project.brushBP2pixel(endIndex - 0.5)
)
);
this.chromosomeGraphics
.drawRect(
x1,
y1,
x2 - x1,
y2 - y1
);
}
this.chromosomeGraphics.endFill();
const sorted = data.slice();
const variantLength = v => Math.abs(v.startIndex - v.endIndex);
const variantTypeWeight = v => /^(del|inv)$/i.test(v.type) ? 1 : 0;
const statisticsFirst = v => v.isStatistics ? 0 : 1;
sorted
.sort((a, b) => variantTypeWeight(a) - variantTypeWeight(b))
.sort((a, b) => variantLength(b) - variantLength(a))
.sort((a, b) => statisticsFirst(a) - statisticsFirst(b));
sorted.forEach(variation => {
const variantVisualInfo = this.renderVariation(
variation,
{viewport, y1, y2}
);
if (variantVisualInfo) {
this.visualInfo.push(variantVisualInfo);
}
});
const getVisualSizePx = o => Math.abs(o.position.x2 - o.position.x1);
this.visualInfo.sort((a, b) => getVisualSizePx(a) - getVisualSizePx(b));
}
clear() {
this.hoveredItem = undefined;
this.chromosomeGraphics.clear();
this.graphics.clear();
this.hoveredGraphics.clear();
this.lettersBackground.clear();
this.lettersContainer.removeChildren();
this.freeLabelsZones = [{start: -Infinity, end: Infinity}];
this.bubbles = [];
this.visualInfo = [];
}
getElementUnderPosition (position) {
if (!position) {
return undefined;
}
const {x} = position;
const [element] = (this.visualInfo || [])
.filter(o => o.position && (o.position.x1 <= x && o.position.x2 >= x));
return element;
}
onClick(position) {
const element = this.getElementUnderPosition(position);
return element ? element.variant : undefined;
}
onMove(position) {
const element = this.getElementUnderPosition(position);
if (element !== this.hoveredItem) {
this.hoveredGraphics.clear();
this.graphics.alpha = 1.0;
this.hoveredItem = element;
if (element) {
const config = this._config;
this.graphics.alpha = config.collapsed.hoveredAlpha || 1;
const y1 = Math.floor(
config.collapsedSampleHeight / 2.0 - config.collapsed.bar.height / 2.0
);
const y2 = Math.floor(
config.collapsedSampleHeight / 2.0 + config.collapsed.bar.height / 2.0
);
this.renderVariation(
element.variant,
{
y1,
y2,
hovered: true,
graphics: this.hoveredGraphics,
renderLabels: false
}
);
}
}
return element;
}
animate() {
return false;
}
}
export {VCFSampleCoverageRenderer} from './coverage';
export {VCFSamplesScroll} from './scroll';
export {VCFSampleRenderer};