UNPKG

higlass

Version:

HiGlass Hi-C / genomic / large data viewer

386 lines (299 loc) 10.5 kB
// @ts-nocheck import { format } from 'd3-format'; // Configs import GLOBALS from './configs/globals'; import { THEME_DARK } from './configs/themes'; // Utils import colorToHex from './utils/color-to-hex'; const TICK_HEIGHT = 40; const TICK_MARGIN = 0; const TICK_LENGTH = 5; const TICK_LABEL_MARGIN = 4; class AxisPixi { constructor(track) { this.pAxis = new GLOBALS.PIXI.Graphics(); this.track = track; this.axisTexts = []; this.axisTextFontFamily = 'Arial'; this.axisTextFontSize = 10; } startAxis(axisHeight) { const graphics = this.pAxis; graphics.clear(); graphics.lineStyle( 1, this.track.getTheme() === THEME_DARK ? colorToHex('#ffffff') : 0x000000, 1, ); // draw the axis line graphics.moveTo(0, 0); graphics.lineTo(0, axisHeight); } createAxisTexts(valueScale, axisHeight) { this.tickValues = this.calculateAxisTickValues(valueScale, axisHeight); let i = 0; const color = this.track.getTheme() === THEME_DARK ? 'white' : 'black'; if ( !this.track.options || !this.track.options.axisLabelFormatting || this.track.options.axisLabelFormatting === 'scientific' ) { this.tickFormat = format('.2'); } else { this.tickFormat = (x) => x; } while (i < this.tickValues.length) { const tick = this.tickValues[i]; while (this.axisTexts.length <= i) { const newText = new GLOBALS.PIXI.Text(tick, { fontSize: `${this.axisTextFontSize}px`, fontFamily: this.axisTextFontFamily, fill: color, }); this.axisTexts.push(newText); this.pAxis.addChild(newText); } this.axisTexts[i].text = this.tickFormat(tick); this.axisTexts[i].anchor.y = 0.5; this.axisTexts[i].anchor.x = 0.5; i++; } while (this.axisTexts.length > this.tickValues.length) { const lastText = this.axisTexts.pop(); this.pAxis.removeChild(lastText); lastText.destroy(true); } } calculateAxisTickValues(valueScale, axisHeight) { const tickCount = Math.max(Math.ceil(axisHeight / TICK_HEIGHT), 1); // create scale ticks but not all the way to the top // tick values have not been formatted here let tickValues = valueScale.ticks(tickCount); if (tickValues.length < 1) { tickValues = valueScale.ticks(tickCount + 1); if (tickValues.length > 1) { // sometimes the ticks function will return 0 and then 2 // if it didn't return enough previously, we probably only want a single // tick tickValues = [tickValues[0]]; } } return tickValues; } drawAxisLeft(valueScale, axisHeight) { // Draw a left-oriented axis (ticks pointing to the right) this.startAxis(axisHeight); this.createAxisTexts(valueScale, axisHeight); const graphics = this.pAxis; if (this.track.getTheme() === THEME_DARK) { graphics.lineStyle( graphics.lineWidth || graphics._lineStyle.width, colorToHex('#ffffff'), ); } // draw the top, potentially unlabelled, ticke graphics.moveTo(0, 0); graphics.lineTo(-(TICK_MARGIN + TICK_LENGTH), 0); graphics.moveTo(0, axisHeight); graphics.lineTo(-(TICK_MARGIN + TICK_LENGTH), axisHeight); for (let i = 0; i < this.axisTexts.length; i++) { const tick = this.tickValues[i]; // draw ticks to the left of the axis this.axisTexts[i].x = -( TICK_MARGIN + TICK_LENGTH + TICK_LABEL_MARGIN + this.axisTexts[i].width / 2 ); this.axisTexts[i].y = valueScale(tick); graphics.moveTo(-TICK_MARGIN, valueScale(tick)); graphics.lineTo(-(TICK_MARGIN + TICK_LENGTH), valueScale(tick)); if (this.track?.flipText) { this.axisTexts[i].scale.x = -1; } } this.hideOverlappingAxisLabels(); } drawAxisRight(valueScale, axisHeight) { // Draw a right-oriented axis (ticks pointint to the left) this.startAxis(axisHeight); this.createAxisTexts(valueScale, axisHeight); const graphics = this.pAxis; if (this.track.getTheme() === THEME_DARK) { graphics.lineStyle( graphics.lineWidth || graphics._lineStyle.width, colorToHex('#ffffff'), ); } // draw the top, potentially unlabelled, ticke graphics.moveTo(0, 0); graphics.lineTo(TICK_MARGIN + TICK_LENGTH, 0); graphics.moveTo(0, axisHeight); graphics.lineTo(TICK_MARGIN + TICK_LENGTH, axisHeight); for (let i = 0; i < this.axisTexts.length; i++) { const tick = this.tickValues[i]; this.axisTexts[i].x = TICK_MARGIN + TICK_LENGTH + TICK_LABEL_MARGIN + this.axisTexts[i].width / 2; this.axisTexts[i].y = valueScale(tick); graphics.moveTo(TICK_MARGIN, valueScale(tick)); graphics.lineTo(TICK_MARGIN + TICK_LENGTH, valueScale(tick)); if (this.track?.flipText) { this.axisTexts[i].scale.x = -1; } } this.hideOverlappingAxisLabels(); } hideOverlappingAxisLabels() { // show all tick marks initially for (let i = this.axisTexts.length - 1; i >= 0; i--) { this.axisTexts[i].visible = true; } for (let i = this.axisTexts.length - 1; i >= 0; i--) { // if this tick mark is invisible, it's not going to // overlap with any others if (!this.axisTexts[i].visible) { continue; } let j = i - 1; while (j >= 0) { // go through and hide all overlapping tick marks if ( this.axisTexts[i].y + this.axisTexts[i].height / 2 > this.axisTexts[j].y - this.axisTexts[j].height / 2 ) { this.axisTexts[j].visible = false; } else { // because the tick marks are ordered from top to bottom, if this // one doesn't overlap, then the ones below it won't either, so // we can stop looking break; } j -= 1; } } } exportVerticalAxis(axisHeight) { const gAxis = document.createElement('g'); gAxis.setAttribute('class', 'axis-vertical'); let stroke = 'black'; if (this.track?.options.lineStrokeColor) { stroke = this.track.options.lineStrokeColor; } // TODO: On the canvas, there is no vertical line beside the scale, // but it also has the draggable control to the right. // Confirm that this difference between SVG and Canvas is intentional, // and if not, remove this. if (this.track.getTheme() === THEME_DARK) stroke = '#cccccc'; const line = document.createElement('path'); line.setAttribute('fill', 'transparent'); line.setAttribute('stroke', stroke); line.setAttribute('id', 'axis-line'); line.setAttribute('d', `M0,0 L0,${axisHeight}`); gAxis.appendChild(line); return gAxis; } createAxisSVGLine() { // factor out the styling for axis lines let stroke = 'black'; if (this.track?.options.lineStrokeColor) { stroke = this.track.options.lineStrokeColor; } if (this.track.getTheme() === THEME_DARK) stroke = '#cccccc'; const line = document.createElement('path'); line.setAttribute('id', 'tick-mark'); line.setAttribute('fill', 'transparent'); line.setAttribute('stroke', stroke); return line; } createAxisSVGText(text) { // factor out the creation of axis texts const t = document.createElement('text'); t.innerHTML = text; t.setAttribute('id', 'axis-text'); t.setAttribute('text-anchor', 'middle'); t.setAttribute('font-family', this.axisTextFontFamily); t.setAttribute('font-size', this.axisTextFontSize); t.setAttribute('dy', this.axisTextFontSize / 2 - 2); return t; } exportAxisLeftSVG(valueScale, axisHeight) { const gAxis = this.exportVerticalAxis(axisHeight); const topTickLine = this.createAxisSVGLine(); gAxis.appendChild(topTickLine); topTickLine.setAttribute('d', `M0,0 L${+(TICK_MARGIN + TICK_LENGTH)},0`); const bottomTickLine = this.createAxisSVGLine(); gAxis.appendChild(bottomTickLine); bottomTickLine.setAttribute( 'd', `M0,${axisHeight} L${+(TICK_MARGIN + TICK_LENGTH)},${axisHeight}`, ); for (let i = 0; i < this.axisTexts.length; i++) { const tick = this.tickValues[i]; const text = this.axisTexts[i]; const tickLine = this.createAxisSVGLine(); gAxis.appendChild(tickLine); tickLine.setAttribute( 'd', `M${+TICK_MARGIN},${valueScale(tick)} L${+(TICK_MARGIN + TICK_LENGTH)},${valueScale(tick)}`, ); const g = document.createElement('g'); gAxis.appendChild(g); if (text.visible) { const t = this.createAxisSVGText(text.text); g.appendChild(t); } g.setAttribute( 'transform', `translate(${text.position.x},${text.position.y}) scale(${text.scale.x},${text.scale.y})`, ); } return gAxis; } exportAxisRightSVG(valueScale, axisHeight) { const gAxis = this.exportVerticalAxis(axisHeight); const topTickLine = this.createAxisSVGLine(); gAxis.appendChild(topTickLine); topTickLine.setAttribute('d', `M0,0 L${-(TICK_MARGIN + TICK_LENGTH)},0`); const bottomTickLine = this.createAxisSVGLine(); gAxis.appendChild(bottomTickLine); bottomTickLine.setAttribute( 'd', `M0,${axisHeight} L${-(TICK_MARGIN + TICK_LENGTH)},${axisHeight}`, ); for (let i = 0; i < this.axisTexts.length; i++) { const tick = this.tickValues[i]; const text = this.axisTexts[i]; const tickLine = this.createAxisSVGLine(); gAxis.appendChild(tickLine); tickLine.setAttribute( 'd', `M${-TICK_MARGIN},${valueScale(tick)} L${-(TICK_MARGIN + TICK_LENGTH)},${valueScale(tick)}`, ); const g = document.createElement('g'); gAxis.appendChild(g); if (text.visible) { const t = this.createAxisSVGText(text.text); g.appendChild(t); } g.setAttribute( 'transform', `translate(${text.position.x},${text.position.y}) scale(${text.scale.x},${text.scale.y})`, ); } return gAxis; } clearAxis() { const graphics = this.pAxis; while (this.axisTexts.length) { const axisText = this.axisTexts.pop(); graphics.removeChild(axisText); } graphics.clear(); } } export default AxisPixi;