diff --git a/frontend/src/components/ImportanceSequence.tsx b/frontend/src/components/ImportanceSequence.tsx new file mode 100644 index 00000000..5f3c28f7 --- /dev/null +++ b/frontend/src/components/ImportanceSequence.tsx @@ -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 elements representing a sequence, optionally backgrounded by s. + * + * @author Silas Hsu + */ +export class Sequence extends React.PureComponent { + 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(); + 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( + + {base} + + ); + x += baseWidth; + } + } + + return {rects}{letters}; + } +} diff --git a/frontend/src/components/TrackUpload.js b/frontend/src/components/TrackUpload.js index 201e3697..2cf078da 100644 --- a/frontend/src/components/TrackUpload.js +++ b/frontend/src/components/TrackUpload.js @@ -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 @@ -172,6 +172,7 @@ export class TrackUpload extends React.Component { + diff --git a/frontend/src/components/genomeNavigator/ImportanceChromosomes.js b/frontend/src/components/genomeNavigator/ImportanceChromosomes.js new file mode 100644 index 00000000..925c53f9 --- /dev/null +++ b/frontend/src/components/genomeNavigator/ImportanceChromosomes.js @@ -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 ; + }); + } + + /** + * Redraws all the feature boxes + * + * @override + */ + render() { + const {viewRegion, width, labelOffset, hideChromName} = this.props; + const drawModel = new LinearDrawingModel(viewRegion, width); + + return + {drawModel.basesToXWidth(1) > Sequence.MIN_X_WIDTH_PER_BASE && this.renderSequences()} + ; + } +} + +export default Chromosomes; \ No newline at end of file diff --git a/frontend/src/components/trackConfig/ImportanceTrackConfig.ts b/frontend/src/components/trackConfig/ImportanceTrackConfig.ts new file mode 100644 index 00000000..60e85370 --- /dev/null +++ b/frontend/src/components/trackConfig/ImportanceTrackConfig.ts @@ -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; + } +} \ No newline at end of file diff --git a/frontend/src/components/trackConfig/getTrackConfig.ts b/frontend/src/components/trackConfig/getTrackConfig.ts index 807a2cce..84889764 100644 --- a/frontend/src/components/trackConfig/getTrackConfig.ts +++ b/frontend/src/components/trackConfig/getTrackConfig.ts @@ -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"]; @@ -68,6 +70,7 @@ const TYPE_NAME_TO_CONFIG = { dynamiclongrange: DynamicLongrangeTrackConfig, omeroidr: OmeroidrTrackConfig, omero4dn: Omero4dnTrackConfig, + importance: ImportanceTrackConfig, }; const DefaultConfig = TrackConfig; diff --git a/frontend/src/components/trackManagers/CustomTrackAdder.js b/frontend/src/components/trackManagers/CustomTrackAdder.js index a02c87c9..cd1b4a06 100644 --- a/frontend/src/components/trackManagers/CustomTrackAdder.js +++ b/frontend/src/components/trackManagers/CustomTrackAdder.js @@ -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 @@ -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", }; /** diff --git a/frontend/src/components/trackVis/ImportanceTrack.js b/frontend/src/components/trackVis/ImportanceTrack.js new file mode 100644 index 00000000..25c75411 --- /dev/null +++ b/frontend/src/components/trackVis/ImportanceTrack.js @@ -0,0 +1,124 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import memoizeOne from 'memoize-one'; +import Track from './commonComponents/Track'; +import Smooth from 'array-smooth'; +import HoverTooltipContext from './commonComponents/tooltip/HoverTooltipContext'; +import Chromosomes from '../genomeNavigator/ImportanceChromosomes'; +import GenomicCoordinates from './commonComponents/GenomicCoordinates'; +import TrackLegend from './commonComponents/TrackLegend'; +import withCurrentGenome from '../withCurrentGenome'; + +import DisplayedRegionModel from '../../model/DisplayedRegionModel'; +import { getGenomeConfig } from '../../model/genomes/allGenomes'; +import { TrackModel } from '../../model/TrackModel'; +import { NumericalDisplayModes } from '../../model/DisplayModes'; +import { FeatureAggregator, DefaultAggregators } from '../../model/FeatureAggregator'; +import { ScaleChoices } from '../../model/ScaleChoices'; + +const CHROMOSOMES_Y = 60; +const RULER_Y = 20; +const HEIGHT = 40; + +/** + * A ruler display. + * + * @author Silas Hsu + */ + + +class RulerVisualizer extends React.PureComponent { + static propTypes = Object.assign({}, Track.propsFromTrackContainer, + { + genomeConfig: PropTypes.shape({cytobands: PropTypes.object}).isRequired, // Object with cytoband data + data: PropTypes.array.isRequired, // PropTypes.arrayOf(Feature) + trackModel: PropTypes.instanceOf(TrackModel).isRequired, + viewRegion: PropTypes.instanceOf(DisplayedRegionModel).isRequired, + width: PropTypes.number.isRequired, + unit: PropTypes.string, // Unit to display after the number in tooltips + options: PropTypes.shape({ + aggregateMethod: PropTypes.oneOf(Object.values(DefaultAggregators.types)), + displayMode: PropTypes.oneOf(Object.values(NumericalDisplayModes)).isRequired, + height: PropTypes.number.isRequired, // Height of the track + scaleType: PropTypes.any, // Unused for now + scaleRange: PropTypes.array, // Unused for now + color: PropTypes.string, // Color to draw bars, if using the default getBarElement + }).isRequired, + xToValue: PropTypes.array.isRequired, + drawHeights: PropTypes.array.isRequired, + zeroLine: PropTypes.number.isRequired + }); + + constructor(props) { + super(props); + this.renderTooltip = this.renderTooltip.bind(this); + } + + /** + * Renders the default tooltip that is displayed on hover. + * + * @param {number} relativeX - x coordinate of hover relative to the visualizer + * @param {number} value - + * @return {JSX.Element} tooltip to render + */ + renderTooltip(relativeX) { + const {trackModel, viewRegion, width, unit, xToValue} = this.props; + const value = xToValue[Math.round(relativeX)]; + const stringValue = typeof value === "number" && !Number.isNaN(value) ? value.toFixed(2) : '(no data)'; + return ( +
+
+ + {this.hasReverse && "Forward: "} {stringValue} + {unit && {unit}} +
+
+ +
+
{trackModel.getDisplayLabel()}
+
+ ); + } + + render() { + console.debug("Well hello"); + const {data, trackModel, viewRegion, width, options, xToValue, zeroLine, drawHeights} = this.props; + const {height, color, color2, aggregateMethod, colorAboveMax, color2BelowMin, smooth} = options; + + const genomeConfig = getGenomeConfig(trackModel.getMetadata('genome')) || this.props.genomeConfig; + console.debug(genomeConfig); + return ( + + {/* display: block prevents svg from taking extra bottom space */ } + + + + + ); + } +} + +const RulerVisualizerWithGenome = withCurrentGenome(RulerVisualizer); + +function RulerTrack(props, xToValue, drawHeights, zeroLine, legend) { + return } + legend={legend} + visualizer={} + />; +} + +export default RulerTrack; diff --git a/frontend/src/components/trackVis/commonComponents/numerical/ImportanceNumericalTrack.js b/frontend/src/components/trackVis/commonComponents/numerical/ImportanceNumericalTrack.js new file mode 100644 index 00000000..8ac75243 --- /dev/null +++ b/frontend/src/components/trackVis/commonComponents/numerical/ImportanceNumericalTrack.js @@ -0,0 +1,304 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import { scaleLinear } from 'd3-scale'; +import memoizeOne from 'memoize-one'; +import { notify } from 'react-notify-toast'; +import Smooth from 'array-smooth'; +import Track from '../Track'; +import TrackLegend from '../TrackLegend'; +import GenomicCoordinates from '../GenomicCoordinates'; +import HoverTooltipContext from '../tooltip/HoverTooltipContext'; +import configOptionMerging from '../configOptionMerging'; +import RulerTrack from '../../ImportanceTrack'; +import Chromosomes from '../../../genomeNavigator/ImportanceChromosomes'; +import { Sequence } from '../../../ImportanceSequence'; +import { getGenomeConfig } from '../../../../model/genomes/allGenomes'; + +import DisplayedRegionModel from '../../../../model/DisplayedRegionModel'; +import LinearDrawingModel from '../../../../model/LinearDrawingModel'; +import withCurrentGenome from '../../../withCurrentGenome'; + + +import { RenderTypes, DesignRenderer } from '../../../../art/DesignRenderer'; +import { NumericalDisplayModes } from '../../../../model/DisplayModes'; +import { FeatureAggregator, DefaultAggregators } from '../../../../model/FeatureAggregator'; +import { ScaleChoices } from '../../../../model/ScaleChoices'; + +export const DEFAULT_OPTIONS = { + aggregateMethod: DefaultAggregators.types.MEAN, + displayMode: NumericalDisplayModes.AUTO, + height: 40, + color: "blue", + colorAboveMax: "red", + color2: "darkorange", + color2BelowMin: "darkgreen", + yScale: ScaleChoices.AUTO, + yMax: 10, + yMin: 0, + smooth: 0, +}; +const withDefaultOptions = configOptionMerging(DEFAULT_OPTIONS); +const CHROMOSOMES_Y = 60; +const AUTO_HEATMAP_THRESHOLD = 21; // If pixel height is less than this, automatically use heatmap +const TOP_PADDING = 2; +const THRESHOLD_HEIGHT = 3; // the bar tip height which represet value above max or below min + +/** + * Track specialized in showing numerical data. + * + * @author Silas Hsu + */ +class ImportanceNumericalTrack extends React.PureComponent { + /** + * Don't forget to look at NumericalFeatureProcessor's propTypes! + */ + static propTypes = Object.assign({}, Track.propsFromTrackContainer, + { + /** + * NumericalFeatureProcessor provides these. Parents should provide an array of NumericalFeature. + */ + data: PropTypes.array.isRequired, // PropTypes.arrayOf(Feature) + unit: PropTypes.string, // Unit to display after the number in tooltips + options: PropTypes.shape({ + aggregateMethod: PropTypes.oneOf(Object.values(DefaultAggregators.types)), + displayMode: PropTypes.oneOf(Object.values(NumericalDisplayModes)).isRequired, + height: PropTypes.number.isRequired, // Height of the track + scaleType: PropTypes.any, // Unused for now + scaleRange: PropTypes.array, // Unused for now + color: PropTypes.string, // Color to draw bars, if using the default getBarElement + }).isRequired, + width: PropTypes.number.isRequired, //Width of base letter + isLoading: PropTypes.bool, // If true, applies loading styling + error: PropTypes.any, // If present, applies error styling + viewRegion: PropTypes.instanceOf(DisplayedRegionModel).isRequired, // Region to visualize + }); + + constructor(props) { + super(props); + this.xToValue = null; + this.xToValue2 = null; + this.scales = null; + this.hasReverse = false; + console.debug("FROM CONSTRUCTOR IMPNUMSCOR"); + console.debug(props.data); + this.aggregateFeatures = memoizeOne(this.aggregateFeatures); + this.computeScales = memoizeOne(this.computeScales); + console.debug("DONE COMPUTE SCALES"); + } + + aggregateFeatures(data, viewRegion, width, aggregatorId) { + const aggregator = new FeatureAggregator(); + const xToFeatures = aggregator.makeXMap(data, viewRegion, width); + return xToFeatures.map( DefaultAggregators.fromId(aggregatorId) ); + } + + computeScales(xToValue, xToValue2, height) { + const {yScale, yMin, yMax} = this.props.options; + if (yMin > yMax) { + notify.show('Y-axis min must less than max', 'error', 2000); + } + + if (yMin > 0) { + notify.show('Y-axis min > 0 not supported', 'error', 2000); + } + /* + All tracks get `PropsFromTrackContainer` (see `Track.ts`). + + `props.viewWindow` contains the range of x that is visible when no dragging. + It comes directly from the `ViewExpansion` object from `RegionExpander.ts` + */ + const visibleValues = xToValue.slice(this.props.viewWindow.start, this.props.viewWindow.end); + let max = _.max(visibleValues) || 0; // in case undefined returned here, cause maxboth be undefined too + let min = (xToValue2.length > 0 ? _.min(xToValue2.slice(this.props.viewWindow.start, this.props.viewWindow.end)) : 0 ) || 0; + + // const maxBoth = Math.max(Math.abs(max), Math.abs(min)); + // max = maxBoth; + // min = xToValue2.length > 0 ? -maxBoth : 0; + + if (yScale === ScaleChoices.FIXED) { + max = yMax ? yMax : max; + min = yMin ? yMin : min; + } + if (min > max) { + min = max; + } + + // determines the distance of y=0 from the top + let zeroLine = min < 0 ? TOP_PADDING + (height-TOP_PADDING)*max/(max + Math.abs(min)) : height; + + if (xToValue2.length > 0) { + return { + valueToHeight: scaleLinear().domain([min, max]).range([zeroLine-height, zeroLine-TOP_PADDING]), + valueToY: scaleLinear().domain([max, 0]).range([TOP_PADDING, zeroLine]).clamp(true), + valueToYReverse: scaleLinear().domain([0, min]).range([0, zeroLine - TOP_PADDING]).clamp(true), + valueToOpacity: scaleLinear().domain([0, max]).range([0, 1]).clamp(true), + valueToOpacityReverse: scaleLinear().domain([0, min]).range([0, 1]).clamp(true), + min, + max, + zeroLine + }; + } else { + return { + valueToHeight: scaleLinear().domain([min, max]).range([zeroLine-height, zeroLine-TOP_PADDING]), + valueToY: scaleLinear().domain([max, min]).range([TOP_PADDING, height]).clamp(true), + valueToOpacity: scaleLinear().domain([min, max]).range([0, 1]).clamp(true), + min, + max, + zeroLine + }; + } + } + + + getEffectiveDisplayMode() { + const {displayMode, height} = this.props.options; + const drawModel = new LinearDrawingModel(this.props.viewRegion, this.props.width); + + if (displayMode === NumericalDisplayModes.AUTO) { + return drawModel.basesToXWidth(1) < Sequence.MIN_X_WIDTH_PER_BASE ? NumericalDisplayModes.HEATMAP : NumericalDisplayModes.BAR; + } else { + return displayMode; + } + } + + + + render() { + const {data, viewRegion, width, trackModel, unit, options, forceSvg} = this.props; + const {height, color, color2, aggregateMethod, colorAboveMax, color2BelowMin, smooth} = options; + + const halfHeight = height * 0.5; + const dataForward = data.filter(feature => feature.value === undefined || feature.value >= 0); // bed track to density mode + const dataReverse = data.filter(feature => feature.value < 0); + let xToValue2BeforeSmooth; + if (dataReverse.length > 0) { + this.hasReverse = true; + xToValue2BeforeSmooth = this.aggregateFeatures(dataReverse, viewRegion, width, aggregateMethod); + } else { + xToValue2BeforeSmooth = []; + } + this.xToValue2 = smooth === 0 ? xToValue2BeforeSmooth: Smooth(xToValue2BeforeSmooth, smooth); + const isDrawingBars = this.getEffectiveDisplayMode() === NumericalDisplayModes.BAR; // As opposed to heatmap + const xToValueBeforeSmooth = dataForward.length > 0 ? this.aggregateFeatures(dataForward, viewRegion, width, aggregateMethod) : []; + this.xToValue = smooth === 0 ? xToValueBeforeSmooth: Smooth(xToValueBeforeSmooth, smooth); + this.scales = this.computeScales(this.xToValue, this.xToValue2, height); + const legend = ; + if (!isDrawingBars) { + const visualizer = + + + + return ; + } + + // const visualizer = ; + // this converts to absolute value also + let drawHeights = this.xToValue.map(this.scales.valueToHeight); + let allValues = this.xToValue; + + if (this.xToValue2.length>0) { + let negHeights = this.xToValue2.map(this.scales.valueToHeight); + drawHeights = drawHeights.map(function(num, idx) { + return (num || 0) + (negHeights[idx] || 0); + }); + + let x2V2 = this.xToValue2; + allValues = allValues.map(function(num, idx) { + return (num || 0) + (x2V2[idx] || 0); + }) + } + + + return RulerTrack(this.props, allValues, drawHeights, this.scales.zeroLine, legend); + + } +} + +class ValuePlot extends React.PureComponent { + static propTypes = { + xToValue: PropTypes.array.isRequired, + scales: PropTypes.object.isRequired, + height: PropTypes.number.isRequired, + color: PropTypes.string, + isDrawingBars: PropTypes.bool, + } + + constructor(props) { + super(props); + this.renderPixel = this.renderPixel.bind(this); + } + + /** + * Gets an element to draw for a data record. + * + * @param {number} value + * @param {number} x + * @return {JSX.Element} bar element to render + */ + renderPixel(value, x) { + if (!value || Number.isNaN(value)) { + return null; + } + const {isDrawingBars, scales, height, color, colorOut} = this.props; + const y = value > 0 ? scales.valueToY(value) : scales.valueToYReverse(value); + let drawY = value > 0 ? y : 0 ; + let drawHeight = value > 0 ? height - y : y; + + if (isDrawingBars) { + // const y = scales.valueToY(value); + // const drawHeight = height - y; + if (drawHeight <= 0) { + return null; + } + let tipY; + if (value > scales.max || value < scales.min) { + drawHeight -= THRESHOLD_HEIGHT + if (value > scales.max) { + tipY = y; + drawY += THRESHOLD_HEIGHT; + } else { + tipY = drawHeight; + } + return + + + ; + } else { + return ; + } + + } else { // Assume HEATMAP + const opacity = value > 0 ? scales.valueToOpacity(value) : scales.valueToOpacityReverse(value); + return ; + } + } + + render() { + const {xToValue, height, forceSvg} = this.props; + return + {this.props.xToValue.map(this.renderPixel)} + + } +} + +export default withDefaultOptions(ImportanceNumericalTrack);