Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions frontend/src/components/ImportanceSequence.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React from 'react';
import OpenInterval from '../model/interval/OpenInterval';

const COMPLEMENT_BASE = {
A: 'T',
T: 'A',
G: 'C',
C: 'G'
};

function getReverseComplement(sequence: string): string {
let result = '';
for (let i = sequence.length - 1; i >= 0; i--) {
const char = sequence.charAt(i);
const complement = COMPLEMENT_BASE[ char.toUpperCase() ];
result += complement || char; // Default to the unmodified char if there is no complement
}
return result;
}

export const BASE_COLORS = {
G: '#3899c7', // Blue
C: '#e05144', // Red
T: '#9238c7', // Purple
A: '#89c738', // Green
N: '#858585' // Grey
};

export const BASE_SIZES = {
G: 1,
C: 1,
T: 1,
A: 1,
N: 1
};

const UNKNOWN_BASE_COLOR = 'black';

interface SequenceProps {
sequence: string;
xSpan: OpenInterval;
y?: number;
isDrawBackground?: boolean;
height?: number;
letterSize?: number;
isReverseComplement?: boolean;
xToValue?: Float32Array;
drawHeights?: Float32Array;
zeroLine?: number;
}

/**
* A set of SVG <text> elements representing a sequence, optionally backgrounded by <rect>s.
*
* @author Silas Hsu
*/
export class Sequence extends React.PureComponent<SequenceProps> {
static MIN_X_WIDTH_PER_BASE = 2;

static defaultProps = {
isDrawBackground: false,
height: 30,
letterSize: 0,
y: 0
};

render() {
const {sequence, xSpan, y, isDrawBackground, height, letterSize, isReverseComplement, xToValue, drawHeights, zeroLine} = this.props;
if (!sequence) {
return null;
}

const baseWidth = xSpan.getLength() / sequence.length;
if (baseWidth < Sequence.MIN_X_WIDTH_PER_BASE) {
return null;
}

const sequenceToDraw = isReverseComplement ? getReverseComplement(sequence) : sequence;

const rects = [];
if (isDrawBackground) {
let x = xSpan.start;
for (const base of sequenceToDraw) {
rects.push(<rect
key={x}
x={x}
y={y}
width={baseWidth}
height={BASE_SIZES[base.toUpperCase()]}
style={{fill: BASE_COLORS[base.toUpperCase()] || UNKNOWN_BASE_COLOR}}
/>);
x += baseWidth;
}
}

let i;
let x_mid;
let scale_fac;
console.debug("GONNA DRAW SEQ");
console.debug("BASE WIDTH " + baseWidth);
console.debug("HEIGHT " + height);
const letters = [];
if (baseWidth >= letterSize) {
let x = xSpan.start;
for (const base of sequenceToDraw) {
x_mid = x + baseWidth/2;
// this scale factor somehow miraculously works
// don't exactly know the height size fontsize = 1.4*baseWidth
scale_fac = drawHeights[Math.floor(x_mid)]/baseWidth;

letters.push(
<text
key={x}
dominantBaseline="baseline"
style={{textAnchor: "middle", fill: BASE_COLORS[base.toUpperCase()],
transform: `translate(${x_mid}px, ${y + zeroLine}px) scaleY(${scale_fac})`,
fontSize: 1.4*baseWidth}}
>
{base}
</text>
);
x += baseWidth;
}
}

return <React.Fragment>{rects}{letters}</React.Fragment>;
}
}
3 changes: 2 additions & 1 deletion frontend/src/components/TrackUpload.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import JSON5 from "json5";
import { readFileAsText, HELP_LINKS } from "../util";
import { TrackOptionsUI } from "./trackManagers/TrackOptionsUI";

const ONE_TRACK_FILE_LIST = ["bigwig", "bigbed", "hic", "biginteract", "g3d"]; // all lower case
const ONE_TRACK_FILE_LIST = ["bigwig", "bigbed", "hic", "biginteract", "g3d", "importance"]; // all lower case

/**
* handles local track file upload using FileReader API
Expand Down Expand Up @@ -172,6 +172,7 @@ export class TrackUpload extends React.Component {
<option value="bigBed">bigBed</option>
<option value="hic">HiC</option>
<option value="bigInteract">bigInteract</option>
<option value="importance">importance</option>
<option value="g3d">G3D</option>
</optgroup>
<optgroup label="select both the track file and index file (only select 1 pair)">
Expand Down
153 changes: 153 additions & 0 deletions frontend/src/components/genomeNavigator/ImportanceChromosomes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import React from 'react';
import PropTypes from 'prop-types';
import memoizeOne from 'memoize-one';
import _ from 'lodash';

import { Sequence } from '../ImportanceSequence';

import DisplayedRegionModel from '../../model/DisplayedRegionModel';
import LinearDrawingModel from '../../model/LinearDrawingModel';
import ChromosomeInterval from '../../model/interval/ChromosomeInterval';
import NavigationContext from '../../model/NavigationContext';
import { FeaturePlacer } from '../../model/FeaturePlacer';
import TwoBitSource from '../../dataSources/TwoBitSource';
import { TranslatableG } from '../TranslatableG';

const HEIGHT = 15;
const TOP_PADDING = 5;
const DEFAULT_LABEL_OFFSET = 70;
const FEATURE_LABEL_SIZES = [16, 12, 8];

const CYTOBAND_COLORS = {
'gneg': {bandColor: "white", textColor: "rgb(0,0,0)"},
'gpos': {bandColor: "rgb(180,180,180)", textColor: "rgb(0,0,0)"},
'gpos25': {bandColor: "rgb(180,180,180)", textColor: "rgb(0,0,0)"},
'gpos50': {bandColor: "rgb(120,120,120)", textColor: "rgb(255,255,255)"},
'gpos75': {bandColor: "rgb(60,60,60)", textColor: "rgb(255,255,255)"},
'gpos100': {bandColor: "rgb(0,0,0)", textColor: "rgb(255,255,255)"},
'gvar': {bandColor: "rgb(0,0,0)", textColor: "rgb(255,255,255)"},
'stalk': {bandColor: "rgb(180,180,180)", textColor: "rgb(0,0,0)"},
'gpos33': {bandColor: "rgb(142,142,142)", textColor: "rgb(255,255,255)"},
'gpos66': {bandColor: "rgb(57,57,57)", textColor: "rgb(255,255,255)"},
'acen': {bandColor: "rgb(141,64,52)", textColor: "rgb(255,255,255)"}, // Centromere
};
const CYTOBAND_LABEL_SIZE = 10;

/**
* Draws rectangles that represent features in a navigation context, and labels for the features. Called "Chromosomes"
* because at first, NavigationContexts only held chromosomes as features.
*
* @author Silas Hsu and Daofeng Li
*/
class Chromosomes extends React.PureComponent {
static propTypes = {
genomeConfig: PropTypes.shape({cytobands: PropTypes.object}).isRequired, // Object with cytoband data
viewRegion: PropTypes.instanceOf(DisplayedRegionModel).isRequired, // Region to visualize
width: PropTypes.number.isRequired, // Width with which to draw
labelOffset: PropTypes.number, // Y offset of feature labels
x: PropTypes.number, // X offset of the entire graphic
y: PropTypes.number, // Y offset of the entire graphic
xToValue: PropTypes.array.isRequired,
drawHeights: PropTypes.array.isRequired,
zeroLine: PropTypes.number.isRequired,
height: PropTypes.number
};

constructor(props){
super(props);
this.state = {
sequenceData: []
};
this.twoBitSource = props.genomeConfig.twoBitURL ? new TwoBitSource(props.genomeConfig.twoBitURL) : null;
this.fetchSequence = _.throttle(this.fetchSequence, 500);
this.fetchSequence(props);

this.featurePlacer = new FeaturePlacer();
this.featurePlacer.placeFeatures = memoizeOne(this.featurePlacer.placeFeatures);
}

/**
* Fetches sequence data for the view region stored in `props`, if zoomed in enough.
*
* @param {Object} props - props as specified by React
*/
async fetchSequence(props) {
if (!this.twoBitSource) {
return;
}

const drawModel = new LinearDrawingModel(props.viewRegion, props.width);
if (drawModel.basesToXWidth(1) > Sequence.MIN_X_WIDTH_PER_BASE) {
try {
const sequence = await this.twoBitSource.getData(props.viewRegion);
if (this.props.viewRegion === props.viewRegion) { // Check that when the data comes in, we still want it
this.setState({sequenceData: sequence});
}
} catch (error) {
console.error(error);
}
}
}

/**
* If zoomed in enough, fetches sequence.
*
* @param {Object} nextProps - props as specified by React
*/
componentWillReceiveProps(nextProps) {
if (this.props.viewRegion !== nextProps.viewRegion) {
const drawModel = new LinearDrawingModel(nextProps.viewRegion, nextProps.width);
if (drawModel.basesToXWidth(1) > Sequence.MIN_X_WIDTH_PER_BASE) {
this.fetchSequence(nextProps);
}
}
}

/**
* Tries to find a label size that fits within `maxWidth`. Returns `undefined` if it cannot find one.
*
* @param {string} label - the label contents
* @param {number} maxWidth - max requested width of the label
* @return {number | undefined} an appropriate width for the label, or undefined if there is none
*/
getSizeForFeatureLabel(label, maxWidth) {
return FEATURE_LABEL_SIZES.find(size => (label.length * size * 0.6) < maxWidth);
}

renderSequences() {
console.debug("TRYNA RENDER SEQ");
const {viewRegion, width, height} = this.props;
const placedSequences = this.featurePlacer.placeFeatures(this.state.sequenceData, viewRegion, width);
return placedSequences.map((placement, i) => {
const {feature, visiblePart, xSpan, isReverse} = placement;
const {relativeStart, relativeEnd} = visiblePart;
return <Sequence
key={i}
sequence={feature.sequence.substring(relativeStart, relativeEnd)}
xSpan={xSpan}
y={0}
isReverseComplement={isReverse}
xToValue={this.props.xToValue}
drawHeights={this.props.drawHeights}
zeroLine={this.props.zeroLine}
height={height}
/>;
});
}

/**
* Redraws all the feature boxes
*
* @override
*/
render() {
const {viewRegion, width, labelOffset, hideChromName} = this.props;
const drawModel = new LinearDrawingModel(viewRegion, width);

return <TranslatableG x={this.props.x} y={this.props.y}>
{drawModel.basesToXWidth(1) > Sequence.MIN_X_WIDTH_PER_BASE && this.renderSequences()}
</TranslatableG>;
}
}

export default Chromosomes;
53 changes: 53 additions & 0 deletions frontend/src/components/trackConfig/ImportanceTrackConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { TrackModel } from './../../model/TrackModel';
import { BigWigTrackConfig } from './BigWigTrackConfig';
import { BigWorker } from '../../dataSources/WorkerTSHook';
import LocalBigSource from '../../dataSources/big/LocalBigSource';
import WorkerSource from '../../dataSources/worker/WorkerSource';
import { NumericalFeature } from '../../model/Feature';
import ChromosomeInterval from '../../model/interval/ChromosomeInterval';
import ImportanceNumericalTrack, { DEFAULT_OPTIONS } from '../trackVis/commonComponents/numerical/ImportanceNumericalTrack';

export class ImportanceTrackConfig extends BigWigTrackConfig {
// constructor(trackModel: TrackModel) {
// super(trackModel);
// this.setDefaultOptions(DEFAULT_OPTIONS)
// }

// initDataSource() {
// if (this.trackModel.fileObj) {
// return new LocalBigSource(this.trackModel.fileObj);
// } else {
// return new WorkerSource(BigWorker, this.trackModel.url);
// }
// }

/*
Expected DASFeature schema

interface DASFeature {
max: number; // Chromosome base number, end
maxScore: number;
min: number; // Chromosome base number, start
score: number; // Value at the location
segment: string; // Chromosome name
type: string;
_chromId: number
*/
/**
* Converter of DASFeatures to NumericalFeature.
*
* @param {DASFeature[]} data - DASFeatures to convert
* @return {NumericalFeature[]} NumericalFeatures made from the input
*/
// formatData(data: any[]) {
// console.debug("IM HERE");
// return data.map(feature =>
// new NumericalFeature("", new ChromosomeInterval(feature.segment, feature.min, feature.max))
// .withValue(feature.score)
// );
// }

getComponent() {
return ImportanceNumericalTrack;
}
}
3 changes: 3 additions & 0 deletions frontend/src/components/trackConfig/getTrackConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import { DynamicHicTrackConfig } from "./DynamicHicTrackConfig";
import { DynamicLongrangeTrackConfig } from "./DynamicLongrangeTrackConfig";
import { OmeroidrTrackConfig } from "./OmeroidrTrackConfig";
import { Omero4dnTrackConfig } from "./Omero4dnTrackConfig";
import { ImportanceTrackConfig } from "./ImportanceTrackConfig";


export const INTERACTION_TYPES = ["hic", "longrange", "biginteract"];
export const DYNAMIC_TYPES = ["dynamic", "dbedgraph", "dynamichic", "dynamiclongrange"];
Expand Down Expand Up @@ -68,6 +70,7 @@ const TYPE_NAME_TO_CONFIG = {
dynamiclongrange: DynamicLongrangeTrackConfig,
omeroidr: OmeroidrTrackConfig,
omero4dn: Omero4dnTrackConfig,
importance: ImportanceTrackConfig,
};
const DefaultConfig = TrackConfig;

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/trackManagers/CustomTrackAdder.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const TRACK_TYPES = {
"3D Structure": ["g3d"],
Dynamic: ["dbedgraph"],
Image: ["omero4dn", "omeroidr"],
Importance: ["importance"],
};

export const NUMERRICAL_TRACK_TYPES = ["bigwig", "bedgraph"]; // the front UI we allow any case of types, in TrackModel only lower case
Expand All @@ -49,6 +50,7 @@ const TYPES_DESC = {
dbedgraph: "Dynamic bedgraph data",
omero4dn: "image data from 4DN (4D Nucleome Data Portal)",
omeroidr: "image data from IDR (Image Data Resource)",
importance: "importance tracks by snair",
};

/**
Expand Down
Loading