higlass
Version:
HiGlass Hi-C / genomic / large data viewer
1,037 lines (868 loc) • 27.7 kB
JavaScript
// @ts-nocheck
import boxIntersect from 'box-intersect';
import classifyPoint from 'robust-point-in-polygon';
// Configs
import { GLOBALS } from './configs';
// Components
import HorizontalTiled1DPixiTrack from './HorizontalTiled1DPixiTrack';
// Services
import { tileProxy } from './services';
import trackUtils from './track-utils';
// Utils
import { colorToHex } from './utils';
// these are default values that are overwritten by the track's options
const FONT_SIZE = 11;
const FONT_FAMILY = 'Arial';
const GENE_LABEL_POS = 'outside';
const GENE_RECT_HEIGHT = 10;
const GENE_STRAND_SPACING = 4;
const MAX_TEXTS = 20;
const WHITE_HEX = colorToHex('#ffffff');
const EXON_LINE_HEIGHT = 2;
const EXON_HEIGHT = (2 * GENE_RECT_HEIGHT) / 3;
const GENE_MINI_TRIANGLE_HEIGHT = (2 * EXON_HEIGHT) / 3;
const MAX_GENE_ENTRIES = 50;
const MAX_FILLER_ENTRIES = 5000;
const DEFAULT_PLUS_STRAND_COLOR = 'blue';
const DEFAULT_MINUS_STRAND_COLOR = 'red';
/**
* Event handler for when gene annotations are clicked on.
*/
const geneClickFunc = (event, track, payload) => {
// fill rectangles are just indicators and are not meant to be
// clicked on
if (payload.type === 'filler') return;
track.pubSub.publish('app.click', {
type: 'gene-annotation',
event,
payload,
});
};
/**
* Fillers are annotations indicating that the region they cover
* contains hidden [gene] annotations. If a filler overlaps a gene
* then it's not necessary because we already see the gene.
*
* This function goes through a set of genes and fillers and flags
* the fillers that are completely contained within a gene so that
* we don't display them.
*
* @param {array} genes A list of gene annotations
* @param {array} fillers A list of filler annotations
*/
const flagOverlappingFillers = (genes, fillers) => {
const regions = genes.concat(fillers);
const boxes = regions.map((x) => [x.xStart, 1, x.xEnd, 1]);
boxIntersect(boxes, (i, j) => {
let filler = null;
let gene = null;
if (regions[i].type === 'filler') {
filler = regions[i];
} else {
gene = regions[i];
}
if (regions[j].type === 'filler') {
if (filler) return; // two fillers, don't care if they overlap
filler = regions[j];
} else {
if (!filler) return;
gene = regions[j];
}
if (filler.xStart >= gene.xStart && filler.xEnd <= gene.xEnd) {
filler.hide = true;
}
});
};
/**
* Initialize a tile. Pulled out from the track so that it
* can be modified without having to modify the track
* object (e.g. in an Observable notebooke)
*
* @param {HorizontalGeneAnnotationsTrack} track The track object
* @param {Object} tile The tile to render
* @param {Object} options The track's options
*/
function externalInitTile(track, tile, options) {
const {
flipText,
fontSize,
fontFamily,
plusStrandColor,
minusStrandColor,
maxGeneEntries,
maxFillerEntries,
maxTexts,
} = options;
// create texts
tile.texts = {};
tile.rectGraphics = new GLOBALS.PIXI.Graphics();
tile.textBgGraphics = new GLOBALS.PIXI.Graphics();
tile.textGraphics = new GLOBALS.PIXI.Graphics();
tile.rectMaskGraphics = new GLOBALS.PIXI.Graphics();
tile.graphics.addChild(tile.rectGraphics);
tile.graphics.addChild(tile.textBgGraphics);
tile.graphics.addChild(tile.textGraphics);
tile.graphics.addChild(tile.rectMaskGraphics);
tile.rectGraphics.mask = tile.rectMaskGraphics;
if (!tile.tileData.sort) return;
tile.tileData.sort((a, b) => b.importance - a.importance);
const geneEntries = tile.tileData
.filter((td) => td.type !== 'filler')
.slice(0, maxGeneEntries);
const fillerEntries = tile.tileData
.filter((td) => td.type === 'filler')
.slice(0, maxFillerEntries);
tile.tileData = geneEntries.concat(fillerEntries);
tile.tileData.forEach((td, i) => {
if (td.type === 'filler') {
return;
}
const geneInfo = td.fields;
const geneName = geneInfo[3];
const geneId = track.geneId(geneInfo, td.type);
const strand = td.strand || geneInfo[5];
td.strand = td.strand || strand;
let fill = plusStrandColor || DEFAULT_PLUS_STRAND_COLOR;
if (strand === '-') {
fill = minusStrandColor || DEFAULT_MINUS_STRAND_COLOR;
}
tile.textWidths = {};
tile.textHeights = {};
// don't draw texts for the latter entries in the tile
if (i >= maxTexts) return;
const text = new GLOBALS.PIXI.Text(geneName, {
fontSize: `${fontSize}px`,
fontFamily,
fill: colorToHex(fill),
});
text.interactive = true;
if (flipText) text.scale.x = -1;
text.anchor.x = 0.5;
text.anchor.y = 1;
tile.texts[geneId] = text; // index by geneName
tile.texts[geneId].strand = strand;
tile.textGraphics.addChild(text);
});
tile.initialized = true;
}
/** Draw generic rectangles... currently used for filler annotations */
function renderRects(
rects,
track,
tile,
someGraphics, // unused in this function
xScale,
color,
alpha,
centerY,
height,
) {
const topY = centerY - height / 2;
const FILLER_PADDING = 0;
rects.forEach((td) => {
const graphics = new GLOBALS.PIXI.Graphics();
tile.rectGraphics.addChild(graphics);
graphics.beginFill(color, 0.1);
graphics.lineStyle(0, color);
const poly = [
xScale(td.xStart) - FILLER_PADDING,
topY,
xScale(td.xEnd) + FILLER_PADDING,
topY,
xScale(td.xEnd) + FILLER_PADDING,
topY + height,
xScale(td.xStart) - FILLER_PADDING,
topY + height,
xScale(td.xStart) - FILLER_PADDING,
topY,
];
graphics.interactive = true;
graphics.buttonMode = true;
graphics.mouseup = (event) => geneClickFunc(event, track, td);
graphics.drawPolygon(poly);
tile.allRects.push([poly, td.strand, td]);
});
}
/** Draw the exons within a gene */
function drawExons(
track,
graphics,
txStart,
txEnd,
exonStarts,
exonEnds,
chrOffset,
centerY,
height,
strand,
) {
const topY = centerY - height / 2;
const exonOffsetStarts = exonStarts.split(',').map((x) => +x + chrOffset);
const exonOffsetEnds = exonEnds.split(',').map((x) => +x + chrOffset);
const xStartPos = track._xScale(txStart);
const xEndPos = track._xScale(txEnd);
const width = xEndPos - xStartPos;
const yMiddle = centerY;
const polys = [];
// draw the middle line
let poly = [
xStartPos,
yMiddle - EXON_LINE_HEIGHT / 2,
xStartPos + width,
yMiddle - EXON_LINE_HEIGHT / 2,
xStartPos + width,
yMiddle + EXON_LINE_HEIGHT / 2,
xStartPos,
yMiddle + EXON_LINE_HEIGHT / 2,
];
graphics.drawPolygon(poly);
polys.push(poly);
// the distance between the mini-triangles
const triangleInterval = 2 * height;
// the first triangle (arrowhead) will be drawn in renderGeneSymbols
for (
let j = Math.max(track.position[0], xStartPos) + triangleInterval;
j < Math.min(track.position[0] + track.dimensions[0], xStartPos + width);
j += triangleInterval
) {
if (strand === '+') {
poly = [
j,
yMiddle - GENE_MINI_TRIANGLE_HEIGHT / 2,
j + GENE_MINI_TRIANGLE_HEIGHT / 2,
yMiddle,
j,
yMiddle + GENE_MINI_TRIANGLE_HEIGHT / 2,
];
} else {
poly = [
j,
yMiddle - GENE_MINI_TRIANGLE_HEIGHT / 2,
j - GENE_MINI_TRIANGLE_HEIGHT / 2,
yMiddle,
j,
yMiddle + GENE_MINI_TRIANGLE_HEIGHT / 2,
];
}
polys.push(poly);
graphics.drawPolygon(poly);
}
// draw the actual exons
for (let j = 0; j < exonOffsetStarts.length; j++) {
const exonStart = exonOffsetStarts[j];
const exonEnd = exonOffsetEnds[j];
const xStart = track._xScale(exonStart);
const localWidth = Math.max(
1,
track._xScale(exonEnd) - track._xScale(exonStart),
);
// we're not going to draw rectangles over the arrowhead
// at the start of the gene
let minX = xStartPos;
let maxX = xEndPos;
const pointerWidth = track.geneRectHeight / 2;
let localPoly = null;
if (strand === '+') {
maxX = xEndPos - pointerWidth;
localPoly = [
Math.min(xStart, maxX),
topY,
Math.min(xStart + localWidth, maxX),
topY,
Math.min(xStart + localWidth, maxX),
topY + height,
Math.min(xStart, maxX),
topY + height,
Math.min(xStart, maxX),
topY,
];
} else {
minX = xStartPos + pointerWidth;
localPoly = [
Math.max(xStart, minX),
topY,
Math.max(xStart + localWidth, minX),
topY,
Math.max(xStart + localWidth, minX),
topY + height,
Math.max(xStart, minX),
topY + height,
Math.max(xStart, minX),
topY,
];
}
polys.push(localPoly);
graphics.drawPolygon(localPoly);
}
return polys;
}
/** Draw the arrowheads at the ends of genes */
function renderGeneSymbols(
genes,
track,
tile,
oldGraphics,
xScale,
color,
alpha,
centerY,
height,
) {
const topY = centerY - height / 2;
genes.forEach((gene) => {
const xStart = track._xScale(gene.xStart);
const xEnd = track._xScale(gene.xEnd);
const graphics = new GLOBALS.PIXI.Graphics();
tile.rectGraphics.addChild(graphics);
graphics.beginFill(color, alpha);
graphics.interactive = true;
graphics.buttonMode = true;
graphics.mouseup = (evt) => geneClickFunc(evt, track, gene);
const pointerWidth = track.geneRectHeight / 2;
let poly = [];
if (gene.strand === '+' || gene.fields[5] === '+') {
const pointerStart = Math.max(xStart, xEnd - pointerWidth);
const pointerEnd = pointerStart + pointerWidth;
poly = [
pointerStart,
topY,
pointerEnd,
topY + track.geneRectHeight / 2,
pointerStart,
topY + track.geneRectHeight,
];
} else {
const pointerStart = Math.min(xEnd, xStart + pointerWidth);
const pointerEnd = pointerStart - pointerWidth;
poly = [
pointerStart,
topY,
pointerEnd,
topY + track.geneRectHeight / 2,
pointerStart,
topY + track.geneRectHeight,
];
}
graphics.drawPolygon(poly);
tile.allRects.push([poly, gene.strand, gene]);
});
}
function renderGeneExons(
genes,
track,
tile,
rectGraphics,
xScale,
color,
alpha,
centerY,
height,
) {
genes.forEach((gene) => {
const geneInfo = gene.fields;
const chrOffset = +gene.chrOffset;
const exonStarts = geneInfo[12];
const exonEnds = geneInfo[13];
const graphics = new GLOBALS.PIXI.Graphics();
tile.rectGraphics.addChild(graphics);
graphics.beginFill(color, alpha);
graphics.interactive = true;
graphics.buttonMode = true;
graphics.mouseup = (evt) => geneClickFunc(evt, track, gene);
tile.allRects = tile.allRects.concat(
drawExons(
track,
graphics,
gene.xStart,
gene.xEnd,
exonStarts,
exonEnds,
chrOffset, // not used for now because we have just one chromosome
centerY,
height,
gene.strand || gene.fields[5],
).map((x) => [x, gene.strand, gene]),
);
});
}
function renderGenes(
genes,
track,
tile,
graphics,
xScale,
color,
alpha,
centerY,
height,
) {
renderGeneSymbols(
genes,
track,
tile,
graphics,
xScale,
color,
alpha,
centerY,
height,
);
renderGeneExons(
genes,
track,
tile,
graphics,
xScale,
color,
alpha,
centerY,
height,
);
}
/** Create a preventing this track from drawing outside of its
* visible area
*/
function renderMask(track, tile) {
const { tileX, tileWidth } = trackUtils.getTilePosAndDimensions(
track.tilesetInfo,
tile.tileId,
);
tile.rectMaskGraphics.clear();
const randomColor = Math.floor(Math.random() * 16 ** 6);
tile.rectMaskGraphics.beginFill(randomColor, 0.3);
const x = track._xScale(tileX);
const y = 0;
const width = track._xScale(tileX + tileWidth) - track._xScale(tileX);
const height = track.dimensions[1];
tile.rectMaskGraphics.drawRect(x, y, width, height);
}
class HorizontalGeneAnnotationsTrack extends HorizontalTiled1DPixiTrack {
/**
* Create a new track for Gene Annotations
*
* Arguments:
* ----------
* context: Object related to the environment of this track
* (e.g. renderer object, pubSubs)
* options: Options from the viewconf
*/
constructor(context, options) {
super(context, options);
const { animate } = context;
this.animate = animate;
this.options = options;
this.fontSize = +this.options.fontSize || FONT_SIZE;
this.geneLabelPos = this.options.geneLabelPosition || GENE_LABEL_POS;
this.geneRectHeight =
+this.options.geneAnnotationHeight || GENE_RECT_HEIGHT;
// Don't ask me why but rectangles and triangles seem to be drawn 2px larger
// than they should be
this.geneRectHeight -= 2;
this.geneStrandSpacing =
+this.options.geneStrandSpacing || GENE_STRAND_SPACING;
this.geneStrandHSpacing = this.geneStrandSpacing / 2;
this.geneRectHHeight = this.geneRectHeight / 2;
}
initTile(tile) {
externalInitTile(this, tile, {
flipText: this.flipText,
fontSize: this.fontSize,
fontFamily: FONT_FAMILY,
plusStrandColor: this.options.plusStrandColor,
minusStrandColor: this.options.minusStrandColor,
maxGeneEntries: MAX_GENE_ENTRIES,
maxFillerEntries: MAX_FILLER_ENTRIES,
maxTexts: MAX_TEXTS,
});
this.renderTile(tile);
}
/** cleanup */
destroyTile(tile) {
tile.rectGraphics.destroy();
tile.rectMaskGraphics.destroy();
tile.textGraphics.destroy();
tile.textBgGraphics.destroy();
tile.graphics.destroy();
}
/*
* Redraw the track because the options
* changed
*/
rerender(options, force) {
const strOptions = JSON.stringify(options);
if (!force && strOptions === this.prevOptions) return;
super.rerender(options, force);
this.fontSize = +this.options.fontSize || FONT_SIZE;
this.geneLabelPos = this.options.geneLabelPosition || GENE_LABEL_POS;
this.geneRectHeight =
+this.options.geneAnnotationHeight || GENE_RECT_HEIGHT;
this.geneStrandHSpacing = this.geneStrandSpacing / 2;
this.geneRectHHeight = this.geneRectHeight / 2;
this.prevOptions = strOptions;
this.visibleAndFetchedTiles().forEach((tile) => {
this.renderTile(tile);
});
}
drawTile() {}
geneId(geneInfo, type) {
return `${type}_${geneInfo[0]}_${geneInfo[1]}_${geneInfo[2]}_${geneInfo[3]}`;
}
renderTile(tile) {
if (!tile.initialized) return;
tile.allRects = [];
// store the scale at while the tile was drawn at so that
// we only resize it when redrawing
tile.drawnAtScale = this._xScale.copy();
tile.rectGraphics.removeChildren();
tile.rectGraphics.clear();
tile.textBgGraphics.clear();
const fill = {};
const FILLER_RECT_ALPHA = 0.3;
const GENE_ALPHA = 0.3;
fill['+'] = colorToHex(
this.options.plusStrandColor || DEFAULT_PLUS_STRAND_COLOR,
);
fill['-'] = colorToHex(
this.options.minusStrandColor || DEFAULT_MINUS_STRAND_COLOR,
);
let plusFillerRects = tile.tileData.filter(
(td) => td.type === 'filler' && td.strand === '+',
);
let minusFillerRects = tile.tileData.filter(
(td) => td.type === 'filler' && td.strand === '-',
);
const plusGenes = tile.tileData.filter(
(td) =>
td.type !== 'filler' && (td.strand === '+' || td.fields[5] === '+'),
);
const minusGenes = tile.tileData.filter(
(td) =>
td.type !== 'filler' && (td.strand === '-' || td.fields[5] === '-'),
);
flagOverlappingFillers(plusGenes, plusFillerRects);
flagOverlappingFillers(minusGenes, minusFillerRects);
// remove the fillers that are contained within a gene
plusFillerRects = plusFillerRects.filter((x) => !x.hide);
minusFillerRects = minusFillerRects.filter((x) => !x.hide);
const yMiddle = this.dimensions[1] / 2;
// const fillerGeneSpacing = (this.options.fillerHeight - this.geneRectHeight) / 2;
const plusStrandCenterY =
yMiddle - this.geneRectHeight / 2 - this.geneStrandSpacing / 2;
const minusStrandCenterY =
yMiddle + this.geneRectHeight / 2 + this.geneStrandSpacing / 2;
const plusRenderContext = [
this,
tile,
tile.rectGraphics,
this._xScale,
fill['+'],
FILLER_RECT_ALPHA,
plusStrandCenterY,
this.geneRectHeight,
];
const minusRenderContext = [
this,
tile,
tile.rectGraphics,
this._xScale,
fill['-'],
FILLER_RECT_ALPHA,
minusStrandCenterY,
this.geneRectHeight,
];
renderRects(plusFillerRects, ...plusRenderContext);
renderRects(minusFillerRects, ...minusRenderContext);
plusRenderContext[5] = GENE_ALPHA;
minusRenderContext[5] = GENE_ALPHA;
renderGenes(plusGenes, ...plusRenderContext);
renderGenes(minusGenes, ...minusRenderContext);
renderMask(this, tile);
trackUtils.stretchRects(this, [
(x) => x.rectGraphics,
(x) => x.rectMaskGraphics,
]);
for (const text of Object.values(tile.texts)) {
text.style = {
fontSize: `${this.fontSize}px`,
FONT_FAMILY,
fill: colorToHex(
text.strand === '-'
? this.options.minusStrandColor || DEFAULT_MINUS_STRAND_COLOR
: this.options.plusStrandColor || DEFAULT_PLUS_STRAND_COLOR,
),
};
}
}
calculateZoomLevel() {
// offset by 2 because 1D tiles are more dense than 2D tiles
// 1024 points per tile vs 256 for 2D tiles
const xZoomLevel = tileProxy.calculateZoomLevel(
this._xScale,
this.tilesetInfo.min_pos[0],
this.tilesetInfo.max_pos[0],
);
let zoomLevel = Math.min(xZoomLevel, this.maxZoom);
zoomLevel = Math.max(zoomLevel, 0);
return zoomLevel;
}
draw() {
super.draw();
this.allTexts = [];
this.allBoxes = [];
const allTiles = [];
this.geneAreaHeight = this.geneRectHeight;
const fontSizeHalf = this.fontSize / 2;
trackUtils.stretchRects(this, [
(x) => x.rectGraphics,
(x) => x.rectMaskGraphics,
]);
Object.values(this.fetchedTiles)
// tile hasn't been drawn properly because we likely got some
// bogus data from the server
.filter((tile) => tile.drawnAtScale)
.forEach((tile) => {
tile.textBgGraphics.clear();
tile.textBgGraphics.beginFill(
typeof this.options.labelBackgroundColor !== 'undefined'
? colorToHex(this.options.labelBackgroundColor)
: WHITE_HEX,
);
// move the texts
const parentInFetched = this.parentInFetched(tile);
if (!tile.initialized) return;
tile.tileData.forEach((td) => {
// tile probably hasn't been initialized yet
if (!tile.texts) return;
if (td.type === 'filler') return;
const geneInfo = td.fields;
const geneName = geneInfo[3];
const geneId = this.geneId(geneInfo, td.type);
const text = tile.texts[geneId];
if (!text) return;
const chrOffset = +td.chrOffset;
const txStart = +geneInfo[1] + chrOffset;
const txEnd = +geneInfo[2] + chrOffset;
const txMiddle = (txStart + txEnd) / 2;
let textYMiddle = this.dimensions[1] / 2;
const fontRectPadding = (this.geneAreaHeight - this.fontSize) / 2;
if (geneInfo[5] === '+') {
// genes on the + strand drawn above and in a user-specified color or the
// default blue textYMiddle -= 10;
textYMiddle -=
this.geneLabelPos === 'inside'
? fontRectPadding + this.geneStrandSpacing - 2
: this.fontSize / 2 + this.geneAreaHeight - 2;
} else {
// genes on the - strand drawn below and in a user-specified color or the
// default red
textYMiddle +=
this.geneLabelPos === 'inside'
? this.fontSize +
this.geneStrandSpacing / 2 +
fontRectPadding +
1
: 1.5 * this.fontSize + this.geneAreaHeight + 2;
}
text.position.x = this._xScale(txMiddle);
text.position.y = textYMiddle;
if (!tile.textWidths[geneId]) {
// if we haven't measured the text's width in renderTile, do it now
// this can occur if the same gene is in more than one tile, so its
// dimensions are measured for the first tile and not for the second
const textWidth = text.getBounds().width;
const textHeight = text.getBounds().height;
tile.textHeights[geneId] = textHeight;
tile.textWidths[geneId] = textWidth;
}
if (!parentInFetched) {
text.visible = true;
const TEXT_MARGIN = 2;
if (this.flipText) {
// when flipText is set, that means that the track is being displayed
// vertically so we need to use the stored text height rather than width
this.allBoxes.push([
text.position.x - tile.textHeights[geneId] / 2 - TEXT_MARGIN,
textYMiddle - fontSizeHalf - 1,
text.position.x + tile.textHeights[geneId] / 2 + TEXT_MARGIN,
textYMiddle + fontSizeHalf - 1,
geneName,
]);
} else {
this.allBoxes.push([
text.position.x - tile.textWidths[geneId] / 2 - TEXT_MARGIN,
textYMiddle - fontSizeHalf - 1,
text.position.x + tile.textWidths[geneId] / 2 + TEXT_MARGIN,
textYMiddle + fontSizeHalf - 1,
geneName,
]);
}
this.allTexts.push({
importance: +geneInfo[4],
text,
caption: geneName,
strand: geneInfo[5],
});
allTiles.push(tile.textBgGraphics);
} else {
text.visible = false;
}
});
});
this.hideOverlaps(this.allBoxes, this.allTexts);
this.renderTextBg(this.allBoxes, this.allTexts, allTiles);
}
renderTextBg(allBoxes, allTexts, allTiles) {
allTexts.forEach((text, i) => {
if (text.text.visible && allBoxes[i] && allTiles[i]) {
const [minX, minY, maxX, maxY] = allBoxes[i];
const width = maxX - minX;
const height = maxY - minY;
allTiles[i].drawRect(
minX - width / 2,
minY - height / 2,
width,
height,
);
}
});
}
hideOverlaps(allBoxes, allTexts) {
boxIntersect(allBoxes, (i, j) => {
if (allTexts[i].importance > allTexts[j].importance) {
allTexts[j].text.visible = false;
} else {
allTexts[i].text.visible = false;
}
});
}
setPosition(newPosition) {
super.setPosition(newPosition);
[this.pMain.position.x, this.pMain.position.y] = this.position;
}
setDimensions(newDimensions) {
super.setDimensions(newDimensions);
this.halfRectHHeight = this.dimensions[1] / 2;
// redraw the contents
this.visibleAndFetchedTiles().forEach((tile) => {
this.renderTile(tile);
});
}
zoomed(newXScale, newYScale) {
this.xScale(newXScale);
this.yScale(newYScale);
this.refreshTiles();
this.draw();
}
getMouseOverHtml(trackX, trackY) {
if (!this.tilesetInfo) {
return '';
}
const point = [trackX, trackY];
for (const tile of this.visibleAndFetchedTiles()) {
for (let i = 0; i < tile.allRects.length; i++) {
// copy the visible rects array
if (tile.allRects[i][2].type === 'filler') {
continue;
}
const rect = tile.allRects[i][0].slice(0);
const newArr = [];
while (rect.length) {
const newPoint = rect.splice(0, 2);
newPoint[0] =
newPoint[0] * tile.rectGraphics.scale.x +
tile.rectGraphics.position.x;
newPoint[1] =
newPoint[1] * tile.rectGraphics.scale.y +
tile.rectGraphics.position.y;
newArr.push(newPoint);
}
const pc = classifyPoint(newArr, point);
if (pc === -1) {
const gene = tile.allRects[i][2];
return `
<div>
<b>${gene.fields[3]}</b><br>
<b>Position:</b> ${gene.fields[0]}:${gene.fields[1]}-${gene.fields[2]}<br>
<b>Strand:</b> ${gene.fields[5]}
</div>
`;
}
}
}
return '';
}
exportSVG() {
let track = null;
let base = null;
if (super.exportSVG) {
[base, track] = super.exportSVG();
} else {
base = document.createElement('g');
track = base;
}
const output = document.createElement('g');
output.setAttribute(
'transform',
`translate(${this.position[0]},${this.position[1]})`,
);
track.appendChild(output);
this.visibleAndFetchedTiles()
.filter((tile) => tile.allRects)
.forEach((tile) => {
const gTile = document.createElement('g');
gTile.setAttribute(
'transform',
`translate(${tile.rectGraphics.position.x},
${tile.rectGraphics.position.y})
scale(${tile.rectGraphics.scale.x},
${tile.rectGraphics.scale.y})`,
);
tile.allRects.forEach((rect) => {
const r = document.createElement('path');
const poly = rect[0];
let d = `M ${poly[0]} ${poly[1]}`;
for (let i = 2; i < poly.length; i += 2) {
d += ` L ${poly[i]} ${poly[i + 1]}`;
}
r.setAttribute('d', d);
if (rect[1] === '+') {
r.setAttribute('fill', this.options.plusStrandColor);
} else {
r.setAttribute('fill', this.options.minusStrandColor);
}
r.setAttribute('opacity', '0.3');
gTile.appendChild(r);
});
output.appendChild(gTile);
});
this.allTexts
.filter((text) => text.text.visible)
.forEach((text) => {
const g = document.createElement('g');
const t = document.createElement('text');
t.setAttribute('text-anchor', 'middle');
t.setAttribute('font-family', FONT_FAMILY);
t.setAttribute('font-size', `${this.fontSize}px`);
// this small adjustment of .2em is to place the text better
// in relation to the rectangles used for the genes and exons
t.setAttribute('dy', '-.2em');
g.setAttribute('transform', `scale(${text.text.scale.x},1)`);
if (text.strand === '+') {
// t.setAttribute('stroke', this.options.plusStrandColor);
t.setAttribute('fill', this.options.plusStrandColor);
} else {
// t.setAttribute('stroke', this.options.minusStrandColor);
t.setAttribute('fill', this.options.minusStrandColor);
}
t.innerHTML = text.text.text;
g.appendChild(t);
g.setAttribute(
'transform',
`translate(${text.text.x},${text.text.y})scale(${text.text.scale.x},1)`,
);
output.appendChild(g);
});
return [base, base];
}
}
export default HorizontalGeneAnnotationsTrack;